@kungfu-tech/buildchain 4.0.2-alpha.4 → 4.0.2-alpha.41
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/AGENTS.md +10 -1
- package/CONTRIBUTING.md +24 -0
- package/README.md +2 -2
- package/actions/promote-buildchain-ref/README.md +1 -1
- package/actions/v4-release-candidate-promote/README.md +28 -0
- package/architecture/agent-change-map.md +66 -7
- package/architecture/ci-lane-change-budget.json +366 -0
- package/architecture/internal-capabilities.json +52 -8
- package/architecture/maintainability-debt.json +148 -92
- package/architecture/maintainability-policy.json +34 -30
- package/architecture/release-tail-contract-inventory.json +6 -6
- package/architecture/v4-architecture-constitution.md +23 -20
- package/architecture/v4-cross-platform-adopter-qualification.json +1 -1
- package/architecture/v4-delivery-authority-parity.json +2 -2
- package/architecture/v4-delivery-warrant-shadow-bootstrap-plan.json +1 -1
- package/architecture/v4-floating-consumer-policy.json +2 -2
- package/architecture/v4-next-development-parity.json +5 -5
- package/architecture/v4-partial-mutation-recovery-qualification.json +4 -1
- package/architecture/v4-platform-stage-checkpoints.json +1 -1
- package/architecture/v4-provider-operation-journal-contract.json +5 -2
- package/architecture/v4-publication-facade-patches.json +64 -0
- package/architecture/v4-release-activation-shadow-domain.json +5 -2
- package/architecture/v4-release-topology.json +143 -35
- package/architecture/v4-rust-wasm-production-authority.json +58 -0
- package/architecture/v4-stable-publication-fence.json +5 -2
- package/architecture/v4-stage-capsule-qualification.json +2 -2
- package/architecture/v4-tail-reseal-parity.json +8 -6
- package/architecture/v4-universal-workflow-bootstrap.json +171 -0
- package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
- package/architecture/v4-universal-workflow-train-admission.json +40 -0
- package/architecture/workflow-taxonomy.json +1451 -0
- package/bin/buildchain.mjs +7 -7
- package/contracts/buildchain-v2-residuals-v1.json +57 -48
- package/contracts/dev-delivery-authority-v2.schema.json +16 -2
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-canonical-contracts-v1.schema.json +1 -0
- package/dist/site/buildchain-contract.json +93 -48
- package/dist/site/buildchain-site.json +222 -69
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +50 -13
- package/dist/site/kfd-claims.json +2569 -982
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +13 -13
- package/dist/site/node-api-registry.json +88 -83
- package/dist/site/page-registry.json +204 -51
- package/dist/site/public-surface-audit.json +3207 -1219
- package/dist/site/publication-authority-registry.json +1124 -362
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +16 -2
- package/dist/site/site-manifest.json +17 -17
- package/dist/site/workflow-registry.json +5063 -1659
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +1 -1
- package/docs/cli.md +3 -3
- package/docs/dev-delivery-warrant.md +1 -1
- package/docs/engineering-housekeeper.md +3 -3
- package/docs/kfd-support.md +26 -2
- package/docs/next-development-transition.md +2 -2
- package/docs/node-api-reference.md +129 -129
- package/docs/publication-authority.md +2 -2
- package/docs/release-candidate.md +3 -3
- package/docs/release-flow.md +67 -19
- package/docs/release-governance.md +8 -8
- package/docs/release-passport.md +27 -2
- package/docs/reusable-build-surface.md +16 -0
- package/docs/v4-adopter-delivery.md +2 -2
- package/docs/v4-production-release.md +71 -8
- package/docs/v4-rust-wasm-production-authority.md +58 -0
- package/docs/v4-stage-capsule.md +2 -2
- package/docs/v4-tail-reseal.md +1 -1
- package/docs/workflow-catalog.md +154 -0
- package/docs/workflow-path-migration.md +109 -0
- package/package.json +10 -6
- package/packages/core/artifact-verification-envelope.js +5 -5
- package/packages/core/buildchain-contract.js +1 -1
- package/packages/core/buildchain-kfd-claims.js +2 -2
- package/packages/core/buildchain-publication-authority.js +42 -26
- package/packages/core/buildchain-v4-domain.wasm +0 -0
- package/packages/core/ci-lane-change-budget.js +14 -1
- package/packages/core/controller-evidence.js +4 -1
- package/packages/core/dev-delivery-candidate-identity.js +40 -6
- package/packages/core/dev-delivery-warrant-state.js +3 -4
- package/packages/core/dev-delivery-warrant.js +12 -2
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/kfd-adopter-manifest.js +46 -47
- package/packages/core/next-development-projection.js +2 -2
- package/packages/core/release-line-dry-run.js +1 -1
- package/packages/core/release-passport-contract.js +5 -5
- package/packages/core/release-passport.js +5 -4
- package/packages/core/release-tail-product-capabilities.js +29 -0
- package/packages/core/release-tail-provider-plane.js +68 -1048
- package/packages/core/v4-canonical-contracts.js +14 -75
- package/packages/core/v4-domain-wasm-artifact.js +7 -0
- package/packages/core/v4-domain-wasm.js +216 -0
- package/packages/core/v4-floating-consumer-evidence.js +1 -1
- package/packages/core/v4-floating-consumer-policy.js +1 -1
- package/packages/core/v4-partial-mutation-recovery-qualification.js +9 -386
- package/packages/core/v4-product-publication.js +57 -0
- package/packages/core/v4-protected-publication-source.js +19 -21
- package/packages/core/v4-provider-operation-journal.js +18 -487
- package/packages/core/v4-provider-readback-idempotency.js +12 -97
- package/packages/core/v4-release-activation-shadow.js +13 -500
- package/packages/core/v4-release-invocation.js +28 -389
- package/packages/core/v4-stable-publication-fence.js +13 -342
- package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
- package/scripts/binary-publication-evidence.mjs +135 -0
- package/scripts/build-contract-core.mjs +1 -14
- package/scripts/build-v4-domain-wasm.mjs +189 -0
- package/scripts/check-action-bundles.mjs +12 -2
- package/scripts/check-ci-lane-change-budget.mjs +4 -1
- package/scripts/check-core-mechanism-inventory.mjs +4 -3
- package/scripts/check-inventory.mjs +35 -44
- package/scripts/check-maintainability.mjs +16 -113
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +2 -2
- package/scripts/check-v4-public-dogfood-contract.mjs +22 -9
- package/scripts/check-v4-release-topology.mjs +214 -50
- package/scripts/check-v4-runtime-semantic-closure.mjs +4 -1
- package/scripts/check-v4-universal-workflow-bootstrap.mjs +266 -0
- package/scripts/check-workflow-taxonomy.mjs +6 -0
- package/scripts/copy-v4-domain-wasm.mjs +14 -0
- package/scripts/dev-delivery-authority-command-adapters.mjs +1 -1
- package/scripts/dev-delivery-authority.mjs +8 -8
- package/scripts/dev-delivery-source-proof-reuse.mjs +2 -1
- package/scripts/dev-delivery-warrant-options.mjs +14 -9
- package/scripts/dev-delivery-warrant-store.mjs +73 -7
- package/scripts/dev-delivery-warrant-transition.mjs +109 -0
- package/scripts/dev-delivery-warrant.mjs +76 -99
- package/scripts/dev-pr-auto-merge.mjs +15 -15
- package/scripts/generate-channel-build-workflow.mjs +7 -2
- package/scripts/generate-channel-promotion-workflow.mjs +7 -5
- package/scripts/generate-site-bundle.mjs +11 -11
- package/scripts/generate-v4-universal-workflow-facades.mjs +458 -0
- package/scripts/generate-workflow-taxonomy.mjs +25 -0
- package/scripts/github-output.mjs +31 -0
- package/scripts/maintainability-metrics.mjs +16 -5
- package/scripts/maintainability-public-surface.mjs +115 -0
- package/scripts/next-development-review.mjs +307 -0
- package/scripts/release-asset-client.mjs +91 -0
- package/scripts/release-candidate-resolver.mjs +17 -18
- package/scripts/resume-from-candidate-run.mjs +1 -1
- package/scripts/source-verification-evidence.mjs +344 -0
- package/scripts/stable-candidate-qualification.mjs +1 -1
- package/scripts/universal-facade-maintainability.mjs +82 -0
- package/scripts/v4-product-publication-intent.mjs +121 -0
- package/scripts/v4-publication-settlement.mjs +211 -0
- package/scripts/v4-universal-workflow-backflow.mjs +212 -0
- package/scripts/v4-universal-workflow-engine.mjs +618 -0
- package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
- package/scripts/verify-version-state-delta.mjs +173 -0
- package/scripts/workflow-taxonomy.mjs +507 -0
- package/templates/native-dev-delivery.yml +4 -7
- package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
- package/templates/universal-buildchain-bootstrap.yml +28 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
createV4ReleaseInvocation,
|
|
6
|
+
createV4ReleaseReceipt,
|
|
7
|
+
createV4ReleaseTransaction,
|
|
8
|
+
} from "../packages/core/v4-release-invocation.js";
|
|
9
|
+
import {
|
|
10
|
+
releaseTailRoot,
|
|
11
|
+
validateReleaseTailTransaction,
|
|
12
|
+
} from "../packages/core/release-tail-provider-plane.js";
|
|
13
|
+
|
|
14
|
+
export const SETTLEMENT_ASSET = "buildchain-publication-settlement.json";
|
|
15
|
+
|
|
16
|
+
export function verifyPublicationSettlement(
|
|
17
|
+
documents,
|
|
18
|
+
{ repository, tag, sourceSha, publicPassport, candidateSha } = {},
|
|
19
|
+
) {
|
|
20
|
+
const {
|
|
21
|
+
invocation,
|
|
22
|
+
transaction,
|
|
23
|
+
receipt,
|
|
24
|
+
passport,
|
|
25
|
+
product,
|
|
26
|
+
providerState,
|
|
27
|
+
productState,
|
|
28
|
+
} = documents;
|
|
29
|
+
const projected = createV4ReleaseInvocation(invocation);
|
|
30
|
+
const roots = createV4ReleaseTransaction({
|
|
31
|
+
invocationRoot: projected.roots.invocationRoot,
|
|
32
|
+
publisherRoot: projected.roots.publisherRoot,
|
|
33
|
+
runtimeRoot: projected.roots.runtimeRoot,
|
|
34
|
+
providerRoot: projected.roots.providerRoot,
|
|
35
|
+
parentRoot: projected.roots.parentRoot,
|
|
36
|
+
});
|
|
37
|
+
const { receiptRoot, ...receiptBody } = receipt;
|
|
38
|
+
const { passportRoot, ...passportBody } = passport;
|
|
39
|
+
const { root: productRoot, ...productBody } = product;
|
|
40
|
+
if (
|
|
41
|
+
!validateReleaseTailTransaction(providerState).valid ||
|
|
42
|
+
!validateReleaseTailTransaction(productState).valid
|
|
43
|
+
)
|
|
44
|
+
throw new Error("invalid provider transaction state");
|
|
45
|
+
if (
|
|
46
|
+
releaseTailRoot(transaction) !==
|
|
47
|
+
releaseTailRoot({
|
|
48
|
+
...roots.transaction,
|
|
49
|
+
transactionRoot: roots.transactionRoot,
|
|
50
|
+
}) ||
|
|
51
|
+
createV4ReleaseReceipt(receiptBody).receiptRoot !== receiptRoot ||
|
|
52
|
+
receipt.transactionRoot !== roots.transactionRoot ||
|
|
53
|
+
receipt.outcome !== "complete" ||
|
|
54
|
+
passportRoot !== releaseTailRoot(passportBody) ||
|
|
55
|
+
receipt.releasePassportRoot !== passportRoot ||
|
|
56
|
+
productRoot !== releaseTailRoot(productBody) ||
|
|
57
|
+
!receipt.providerReceiptRoots.includes(productRoot) ||
|
|
58
|
+
receipt.providerTransactionRoot !== providerState.transactionRoot ||
|
|
59
|
+
receipt.providerStateRoot !== providerState.stateRoot ||
|
|
60
|
+
providerState.state !== "complete" ||
|
|
61
|
+
productState.state !== "complete" ||
|
|
62
|
+
product.transaction.stateRoot !== productState.stateRoot ||
|
|
63
|
+
product.transaction.transactionRoot !== productState.transactionRoot ||
|
|
64
|
+
productState.transactionRoot !== roots.transactionRoot ||
|
|
65
|
+
providerState.receipts.some(
|
|
66
|
+
({ receiptRoot: root }) => !receipt.providerReceiptRoots.includes(root),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
throw new Error("terminal publication receipt lineage does not verify");
|
|
70
|
+
verifyPublicationCoordinates(documents, {
|
|
71
|
+
repository,
|
|
72
|
+
tag,
|
|
73
|
+
sourceSha,
|
|
74
|
+
publicPassport,
|
|
75
|
+
candidateSha,
|
|
76
|
+
});
|
|
77
|
+
return { publication: "complete", receiptRoot, sourceSha, tag, repository };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function verifyPublicationCoordinates(
|
|
81
|
+
{ invocation, passport, product },
|
|
82
|
+
{ repository, tag, sourceSha, publicPassport, candidateSha },
|
|
83
|
+
) {
|
|
84
|
+
if (
|
|
85
|
+
invocation.candidate.repository !== repository ||
|
|
86
|
+
(candidateSha && invocation.candidate.commit !== candidateSha) ||
|
|
87
|
+
passport.repository !== repository ||
|
|
88
|
+
invocation.provider.repository !== repository ||
|
|
89
|
+
invocation.target.tag !== tag ||
|
|
90
|
+
passport.release.tag !== tag ||
|
|
91
|
+
passport.release.version !== tag.replace(/^v/u, "") ||
|
|
92
|
+
product.publication.exactTag !== tag ||
|
|
93
|
+
product.publication.version !== passport.release.version ||
|
|
94
|
+
product.publication.releaseSha !== sourceSha ||
|
|
95
|
+
product.publication.state !== "complete" ||
|
|
96
|
+
product.publication.finalizationNeeded ||
|
|
97
|
+
passport.source.headSha !== invocation.candidate.commit ||
|
|
98
|
+
passport.source.treeHash !== invocation.candidate.tree ||
|
|
99
|
+
passport.policyDigest !== invocation.authority.policyRoot ||
|
|
100
|
+
passport.publicationQualificationRoot !==
|
|
101
|
+
invocation.authority.qualificationRoot ||
|
|
102
|
+
(publicPassport &&
|
|
103
|
+
releaseTailRoot(publicPassport) !== releaseTailRoot(passport))
|
|
104
|
+
)
|
|
105
|
+
throw new Error(
|
|
106
|
+
"publication evidence does not match exact provider release coordinates",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function readApplyDocuments(base) {
|
|
111
|
+
const read = (relative) =>
|
|
112
|
+
JSON.parse(fs.readFileSync(path.join(base, relative)));
|
|
113
|
+
return {
|
|
114
|
+
invocation: read("release-tail/release-invocation.json"),
|
|
115
|
+
transaction: read("release-tail/release-transaction.json"),
|
|
116
|
+
receipt: read("release-tail/release-receipt.json"),
|
|
117
|
+
product: read("release-tail/product-provider-result.json"),
|
|
118
|
+
providerState: read("release-tail/state.json"),
|
|
119
|
+
productState: read("release-tail/product-provider-transaction.json"),
|
|
120
|
+
passport: read("release-passport/buildchain.release.json"),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function settlePublication({
|
|
125
|
+
base,
|
|
126
|
+
client,
|
|
127
|
+
applyOutcome,
|
|
128
|
+
repository,
|
|
129
|
+
candidateSha,
|
|
130
|
+
}) {
|
|
131
|
+
const documents = readApplyDocuments(base);
|
|
132
|
+
if (
|
|
133
|
+
documents.invocation.candidate.repository !== repository ||
|
|
134
|
+
(candidateSha && documents.invocation.candidate.commit !== candidateSha)
|
|
135
|
+
)
|
|
136
|
+
throw new Error(
|
|
137
|
+
"publication settlement does not match the admitted repository and candidate",
|
|
138
|
+
);
|
|
139
|
+
const tag = documents.invocation.target.tag;
|
|
140
|
+
const release = client.release(tag);
|
|
141
|
+
const sourceSha = client.json(
|
|
142
|
+
`repos/${repository}/commits/${encodeURIComponent(tag)}`,
|
|
143
|
+
).sha;
|
|
144
|
+
const matches = release.assets.filter(
|
|
145
|
+
({ name }) => name === "buildchain.release.json",
|
|
146
|
+
);
|
|
147
|
+
if (matches.length !== 1)
|
|
148
|
+
throw new Error(
|
|
149
|
+
"exact public publication passport is missing or ambiguous",
|
|
150
|
+
);
|
|
151
|
+
const publicPassport = JSON.parse(client.assetBytes(matches[0]));
|
|
152
|
+
const status = verifyPublicationSettlement(documents, {
|
|
153
|
+
repository,
|
|
154
|
+
tag,
|
|
155
|
+
sourceSha,
|
|
156
|
+
publicPassport,
|
|
157
|
+
});
|
|
158
|
+
// Publication completion is immutable; next-development and distribution progress stay separate.
|
|
159
|
+
const settlement = {
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
contract: "buildchain-v4-publication-settlement/v1",
|
|
162
|
+
id: "v4-publication",
|
|
163
|
+
release: { sourceSha, tag, channel: documents.invocation.target.channel },
|
|
164
|
+
documents,
|
|
165
|
+
};
|
|
166
|
+
const file = path.join(base, "release-tail", SETTLEMENT_ASSET);
|
|
167
|
+
client.write(file, settlement);
|
|
168
|
+
await client.publish(release, [file]);
|
|
169
|
+
const summary = {
|
|
170
|
+
...status,
|
|
171
|
+
nextDevelopment: applyOutcome === "success" ? "advanced" : "incomplete",
|
|
172
|
+
binaryDistribution: "pending-provider-readback",
|
|
173
|
+
};
|
|
174
|
+
client.write(
|
|
175
|
+
path.join(base, "release-tail", "delivery-summary.json"),
|
|
176
|
+
summary,
|
|
177
|
+
);
|
|
178
|
+
return { summary, receipt: documents.receipt };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
const { releaseAssetClient } = await import("./release-asset-client.mjs");
|
|
183
|
+
const repository = process.env.GITHUB_REPOSITORY;
|
|
184
|
+
const result = await settlePublication({
|
|
185
|
+
base: process.argv[2],
|
|
186
|
+
client: releaseAssetClient(repository),
|
|
187
|
+
repository,
|
|
188
|
+
candidateSha: process.env.CANDIDATE_SHA,
|
|
189
|
+
applyOutcome: process.env.APPLY_OUTCOME,
|
|
190
|
+
});
|
|
191
|
+
if (process.env.GITHUB_OUTPUT)
|
|
192
|
+
fs.appendFileSync(
|
|
193
|
+
process.env.GITHUB_OUTPUT,
|
|
194
|
+
`receipt-json=${JSON.stringify(result.receipt)}\nreceipt-root=${result.receipt.receiptRoot}\nstatus=complete\n`,
|
|
195
|
+
);
|
|
196
|
+
if (process.env.GITHUB_STEP_SUMMARY)
|
|
197
|
+
fs.appendFileSync(
|
|
198
|
+
process.env.GITHUB_STEP_SUMMARY,
|
|
199
|
+
`Publication: **complete**, receipt \`${result.receipt.receiptRoot}\`. Next development: **${result.summary.nextDevelopment}**. Binary distribution: **pending provider readback**.\n`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
process.argv[1] &&
|
|
205
|
+
path.basename(process.argv[1]) === "v4-publication-settlement.mjs" &&
|
|
206
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
207
|
+
)
|
|
208
|
+
main().catch((error) => {
|
|
209
|
+
console.error(error.message);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
validateV4UniversalWorkflowRequest,
|
|
7
|
+
v4UniversalWorkflowRequestRoot,
|
|
8
|
+
} from "../packages/core/v4-universal-workflow-bootstrap.js";
|
|
9
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
10
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new Error(message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function canonical(value) {
|
|
17
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
18
|
+
if (value && typeof value === "object")
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.keys(value)
|
|
21
|
+
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right)))
|
|
22
|
+
.map((key) => [key, canonical(value[key])]),
|
|
23
|
+
);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function contentRoot(domain, value) {
|
|
28
|
+
const hash = crypto.createHash("sha256");
|
|
29
|
+
hash.update(domain, "utf8");
|
|
30
|
+
hash.update(Buffer.from([0]));
|
|
31
|
+
hash.update(`${JSON.stringify(canonical(value))}\n`, "utf8");
|
|
32
|
+
return `sha256:${hash.digest("hex")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateTerminalReceipt(value, request) {
|
|
36
|
+
if (
|
|
37
|
+
value?.schema !==
|
|
38
|
+
"kungfu-buildchain-v4-universal-workflow-terminal-receipt/v1" ||
|
|
39
|
+
value.status !== "succeeded" ||
|
|
40
|
+
value.runtime?.repository !== request.candidate.repository ||
|
|
41
|
+
value.runtime?.sha !== request.candidate.expectedSha ||
|
|
42
|
+
value.requestRoot !== v4UniversalWorkflowRequestRoot(request) ||
|
|
43
|
+
!SHA.test(value.runtime?.sha || "") ||
|
|
44
|
+
!ROOT.test(value.receiptRoot || "")
|
|
45
|
+
)
|
|
46
|
+
fail(
|
|
47
|
+
"successful exact-candidate terminal receipt is required for backflow",
|
|
48
|
+
);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createV4UniversalBackflowPlan({
|
|
53
|
+
request: requestValue,
|
|
54
|
+
receipt,
|
|
55
|
+
}) {
|
|
56
|
+
const request = validateV4UniversalWorkflowRequest(requestValue);
|
|
57
|
+
if (request.mode !== "train") fail("only Train delivery can create backflow");
|
|
58
|
+
if (request.capability.id !== "release-candidate-promote")
|
|
59
|
+
fail("only a release-candidate-promote delivery can create backflow");
|
|
60
|
+
if (request.payload?.inputs?.["dry-run"] !== false)
|
|
61
|
+
fail("only a non-dry-run release delivery can create backflow");
|
|
62
|
+
const terminal = validateTerminalReceipt(receipt, request);
|
|
63
|
+
const marker = `<!-- buildchain-universal-backflow:${request.candidate.expectedSha} -->`;
|
|
64
|
+
const binding = {
|
|
65
|
+
schema: "kungfu-buildchain-v4-universal-workflow-backflow/v1",
|
|
66
|
+
repository: request.candidate.repository,
|
|
67
|
+
pullRequest: request.candidate.reviewPullRequest,
|
|
68
|
+
baseRef: "dev/v4/v4.0",
|
|
69
|
+
trainSha: request.candidate.expectedSha,
|
|
70
|
+
consumerRepository: request.consumer.repository,
|
|
71
|
+
consumerSha: request.consumer.sourceSha,
|
|
72
|
+
requestRoot: terminal.requestRoot,
|
|
73
|
+
terminalReceiptRoot: terminal.receiptRoot,
|
|
74
|
+
};
|
|
75
|
+
const backflowRoot = contentRoot("universal-workflow-backflow", binding);
|
|
76
|
+
return {
|
|
77
|
+
...binding,
|
|
78
|
+
marker,
|
|
79
|
+
backflowRoot,
|
|
80
|
+
body: [
|
|
81
|
+
marker,
|
|
82
|
+
"## Universal Train backflow",
|
|
83
|
+
"",
|
|
84
|
+
`- Train SHA: \`${binding.trainSha}\``,
|
|
85
|
+
`- Consumer: \`${binding.consumerRepository}@${binding.consumerSha}\``,
|
|
86
|
+
`- Request root: \`${binding.requestRoot}\``,
|
|
87
|
+
`- Terminal receipt root: \`${binding.terminalReceiptRoot}\``,
|
|
88
|
+
`- Backflow root: \`${backflowRoot}\``,
|
|
89
|
+
"",
|
|
90
|
+
"This receipt binds a successful consumer delivery to the protected Train-to-dev backflow PR; it does not designate the Train candidate as a Buildchain release.",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function githubHeaders(token) {
|
|
96
|
+
return {
|
|
97
|
+
accept: "application/vnd.github+json",
|
|
98
|
+
authorization: `Bearer ${token}`,
|
|
99
|
+
"content-type": "application/json",
|
|
100
|
+
"user-agent": "buildchain-universal-backflow",
|
|
101
|
+
"x-github-api-version": "2022-11-28",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function githubJson({ path, method = "GET", token, body, fetchImpl }) {
|
|
106
|
+
const api = String(
|
|
107
|
+
process.env.GITHUB_API_URL || "https://api.github.com",
|
|
108
|
+
).replace(/\/+$/u, "");
|
|
109
|
+
const response = await fetchImpl(`${api}${path}`, {
|
|
110
|
+
method,
|
|
111
|
+
headers: githubHeaders(token),
|
|
112
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
113
|
+
});
|
|
114
|
+
const text = await response.text();
|
|
115
|
+
const value = text ? JSON.parse(text) : {};
|
|
116
|
+
if (!response.ok)
|
|
117
|
+
fail(
|
|
118
|
+
`GitHub API ${method} ${path} failed with ${response.status}: ${value.message || text}`,
|
|
119
|
+
);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function upsertV4UniversalBackflow({
|
|
124
|
+
request,
|
|
125
|
+
receipt,
|
|
126
|
+
token,
|
|
127
|
+
fetchImpl = globalThis.fetch,
|
|
128
|
+
}) {
|
|
129
|
+
const admitted = validateV4UniversalWorkflowRequest(request);
|
|
130
|
+
if (
|
|
131
|
+
admitted.mode !== "train" ||
|
|
132
|
+
admitted.capability.id !== "release-candidate-promote" ||
|
|
133
|
+
admitted.payload?.inputs?.["dry-run"] !== false
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
schema: "kungfu-buildchain-v4-universal-workflow-backflow-result/v1",
|
|
137
|
+
action: "skipped",
|
|
138
|
+
reason: "not-a-successful-non-dry-run-release-delivery",
|
|
139
|
+
backflowRoot: "",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (!token) fail("GH_TOKEN is required for protected backflow");
|
|
143
|
+
if (typeof fetchImpl !== "function") fail("fetch is required for backflow");
|
|
144
|
+
const plan = createV4UniversalBackflowPlan({ request: admitted, receipt });
|
|
145
|
+
const encodedRepository = plan.repository;
|
|
146
|
+
const pull = await githubJson({
|
|
147
|
+
path: `/repos/${encodedRepository}/pulls/${plan.pullRequest}`,
|
|
148
|
+
token,
|
|
149
|
+
fetchImpl,
|
|
150
|
+
});
|
|
151
|
+
if (
|
|
152
|
+
pull.state !== "open" ||
|
|
153
|
+
pull.base?.ref !== plan.baseRef ||
|
|
154
|
+
pull.head?.repo?.full_name !== plan.repository ||
|
|
155
|
+
pull.head?.sha !== plan.trainSha
|
|
156
|
+
)
|
|
157
|
+
fail("backflow PR no longer binds the exact admitted Train candidate");
|
|
158
|
+
const comments = await githubJson({
|
|
159
|
+
path: `/repos/${encodedRepository}/issues/${plan.pullRequest}/comments?per_page=100`,
|
|
160
|
+
token,
|
|
161
|
+
fetchImpl,
|
|
162
|
+
});
|
|
163
|
+
const existing = comments.find((comment) =>
|
|
164
|
+
String(comment.body || "").includes(plan.marker),
|
|
165
|
+
);
|
|
166
|
+
const comment = existing
|
|
167
|
+
? await githubJson({
|
|
168
|
+
path: `/repos/${encodedRepository}/issues/comments/${existing.id}`,
|
|
169
|
+
method: "PATCH",
|
|
170
|
+
token,
|
|
171
|
+
body: { body: plan.body },
|
|
172
|
+
fetchImpl,
|
|
173
|
+
})
|
|
174
|
+
: await githubJson({
|
|
175
|
+
path: `/repos/${encodedRepository}/issues/${plan.pullRequest}/comments`,
|
|
176
|
+
method: "POST",
|
|
177
|
+
token,
|
|
178
|
+
body: { body: plan.body },
|
|
179
|
+
fetchImpl,
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
schema: "kungfu-buildchain-v4-universal-workflow-backflow-result/v1",
|
|
183
|
+
action: existing ? "updated" : "created",
|
|
184
|
+
pullRequest: plan.pullRequest,
|
|
185
|
+
trainSha: plan.trainSha,
|
|
186
|
+
terminalReceiptRoot: plan.terminalReceiptRoot,
|
|
187
|
+
backflowRoot: plan.backflowRoot,
|
|
188
|
+
commentUrl: comment.html_url,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function main() {
|
|
193
|
+
const request = JSON.parse(
|
|
194
|
+
process.env.BUILDCHAIN_UNIVERSAL_REQUEST_JSON ||
|
|
195
|
+
fail("request is required"),
|
|
196
|
+
);
|
|
197
|
+
const receipt = JSON.parse(
|
|
198
|
+
process.env.BUILDCHAIN_UNIVERSAL_TERMINAL_RECEIPT_JSON ||
|
|
199
|
+
fail("terminal receipt is required"),
|
|
200
|
+
);
|
|
201
|
+
const result = await upsertV4UniversalBackflow({
|
|
202
|
+
request,
|
|
203
|
+
receipt,
|
|
204
|
+
token: process.env.GH_TOKEN || process.env.GITHUB_TOKEN,
|
|
205
|
+
});
|
|
206
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const isMain =
|
|
210
|
+
process.argv[1] &&
|
|
211
|
+
fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
212
|
+
if (isMain) await main();
|