@kungfu-tech/buildchain 4.0.1-alpha.9 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/actions/v4-release-candidate-promote/README.md +3 -0
- package/architecture/agent-change-map.md +1015 -0
- package/architecture/ci-lane-change-budget.json +70 -1
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +156 -11
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +480 -129
- package/architecture/v3-core-mechanism-inventory.json +4 -3
- package/architecture/v3-v4-capability-closure.md +59 -0
- package/architecture/v3-v4-live-capability-inventory.json +102404 -0
- package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +123 -32
- package/dist/site/buildchain-site.json +96 -28
- package/dist/site/capability-registry.json +6 -6
- package/dist/site/cli-registry.json +5 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +170 -21
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +1474 -215
- package/dist/site/page-registry.json +85 -17
- package/dist/site/public-surface-audit.json +85 -20
- package/dist/site/publication-authority-registry.json +28 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +5 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +94 -18
- package/docs/MAP.md +1 -1
- package/docs/cli-reference.md +4 -4
- package/docs/dev-delivery-warrant.md +12 -9
- package/docs/node-api-reference.md +291 -206
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/v4-adopter-delivery.md +23 -4
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/package.json +8 -3
- package/packages/core/adopter-delivery-passport.js +36 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +15 -2
- package/packages/core/dev-delivery-warrant.js +21 -4
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/index.js +1 -0
- package/packages/core/paper-scaffold-content.js +401 -0
- package/packages/core/paper.js +12 -378
- package/packages/core/publication-control-plane-audit.js +135 -111
- package/packages/core/publication-rehearsal-projection.js +104 -0
- package/packages/core/publication-rehearsal-runtime.js +190 -0
- package/packages/core/release-candidate-recovery.js +6 -5
- package/packages/core/release-passport.js +82 -184
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-floating-consumer-policy.js +3 -2
- package/packages/core/v4-floating-consumer-release-passport.js +134 -15
- package/packages/core/v4-publication-qualification.js +222 -0
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +3 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +23 -5
- package/scripts/check-maintainability.mjs +145 -4
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -3
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant.mjs +45 -5
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +44 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-release-candidate-passport.mjs +26 -1
- package/scripts/generate-site-bundle.mjs +6 -0
- package/scripts/maintainability-domain-metrics.mjs +237 -0
- package/scripts/maintainability-governance.mjs +473 -0
- package/scripts/maintainability-metrics.mjs +28 -6
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +19 -2
- package/scripts/resume-from-candidate-run.mjs +106 -103
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +4 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- package/scripts/v4-declarative-promotion-admission.mjs +42 -0
|
@@ -72,77 +72,72 @@ function fact(id, pass, observed) {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
function evaluatePublisherPolicy({
|
|
76
|
+
publisher,
|
|
77
|
+
publisherMode,
|
|
78
|
+
packageName,
|
|
76
79
|
repository,
|
|
77
80
|
workflowPath,
|
|
78
|
-
publisherWorkflowPath
|
|
81
|
+
publisherWorkflowPath,
|
|
82
|
+
workflowFilename,
|
|
79
83
|
environment,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
publisherMode
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const actions = snapshot?.actions || {};
|
|
91
|
-
const branchPolicy = snapshot?.branch || {};
|
|
92
|
-
const environmentPolicy = snapshot?.environment || {};
|
|
93
|
-
const oidc = snapshot?.oidc || {};
|
|
94
|
-
const publisher = snapshot?.publisher || {};
|
|
95
|
-
const runner = snapshot?.runner || {};
|
|
96
|
-
const npmIdentityPass = publisher.packageName === packageName &&
|
|
97
|
-
publisher.provider === "github" &&
|
|
98
|
-
publisher.repository === repository &&
|
|
99
|
-
publisher.workflowFilename === workflowFilename &&
|
|
100
|
-
publisher.environment === providerEnvironment &&
|
|
101
|
-
publisher.longLivedWorkflowCredentialPresent === false;
|
|
102
|
-
const publisherPass = publisherMode === "npm-trusted-publisher"
|
|
103
|
-
? npmIdentityPass && (
|
|
84
|
+
providerEnvironment,
|
|
85
|
+
}) {
|
|
86
|
+
if (publisherMode === "npm-trusted-publisher") {
|
|
87
|
+
const identityPass = publisher.packageName === packageName &&
|
|
88
|
+
publisher.provider === "github" &&
|
|
89
|
+
publisher.repository === repository &&
|
|
90
|
+
publisher.workflowFilename === workflowFilename &&
|
|
91
|
+
publisher.environment === providerEnvironment &&
|
|
92
|
+
publisher.longLivedWorkflowCredentialPresent === false;
|
|
93
|
+
return identityPass && (
|
|
104
94
|
(publisher.enforcement === "audited-control-plane" && publisher.allowPublish === true) ||
|
|
105
95
|
(publisher.enforcement === "provider-at-transaction" &&
|
|
106
96
|
publisher.authorizationDeferred === true &&
|
|
107
97
|
publisher.configurationRead === false)
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (publisherMode === "github-token") {
|
|
101
|
+
return publisher.provider === "github-token" &&
|
|
102
|
+
publisher.repository === repository &&
|
|
103
|
+
publisher.workflowPath === workflowPath &&
|
|
104
|
+
publisher.permissionScoped === true &&
|
|
105
|
+
publisher.longLivedWorkflowCredentialPresent === false;
|
|
106
|
+
}
|
|
107
|
+
if (publisherMode !== "oidc-role") return false;
|
|
108
|
+
return publisher.provider !== "" &&
|
|
109
|
+
publisher.repository === repository &&
|
|
110
|
+
publisher.workflowPath === publisherWorkflowPath &&
|
|
111
|
+
publisher.environment === environment &&
|
|
112
|
+
publisher.trustQualifying === true &&
|
|
113
|
+
/^[0-9a-f]{64}$/i.test(String(publisher.roleDigest || "").replace(/^sha256:/, "")) &&
|
|
114
|
+
publisher.longLivedWorkflowCredentialPresent === false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function evaluateCredentialIsolation({ oidc, publisherMode, publisherWorkflowPath, providerEnvironment }) {
|
|
118
|
+
return publisherMode === "github-token"
|
|
125
119
|
? oidc.githubTokenJobScoped === true && oidc.longLivedCredentialPresent === false
|
|
126
120
|
: oidc.workflowPath === publisherWorkflowPath &&
|
|
127
121
|
oidc.environment === providerEnvironment &&
|
|
128
122
|
oidc.idTokenJobScoped === true &&
|
|
129
123
|
oidc.longLivedCredentialPresent === false;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function requiredStatusCheckBinding(branchPolicy, requiredStatusCheck) {
|
|
127
|
+
const declared = branchPolicy.declaredRequiredStatusCheck || branchPolicy.requiredStatusCheck;
|
|
128
|
+
const resolved = branchPolicy.requiredStatusCheck;
|
|
129
|
+
return {
|
|
130
|
+
resolved,
|
|
131
|
+
pass: declared === requiredStatusCheck && (
|
|
132
|
+
resolved === declared ||
|
|
133
|
+
(resolved.startsWith(`${declared} / `) && branchPolicy.requiredStatusCheckMatchCount === 1)
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sourceAuthorizationPass(branchPolicy) {
|
|
144
139
|
const authorizationSha = branchPolicy.authorizationSha || branchPolicy.sourceSha;
|
|
145
|
-
|
|
140
|
+
return branchPolicy.sourceSha === authorizationSha || (
|
|
146
141
|
branchPolicy.releaseReconciliation?.qualifying === true &&
|
|
147
142
|
branchPolicy.releaseReconciliation.parentSha === authorizationSha &&
|
|
148
143
|
/^\d+\.\d+\.\d+$/.test(String(branchPolicy.releaseReconciliation.version || "")) &&
|
|
@@ -150,74 +145,103 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
150
145
|
Array.isArray(branchPolicy.releaseReconciliation.changedPaths) &&
|
|
151
146
|
branchPolicy.releaseReconciliation.changedPaths.length > 0
|
|
152
147
|
);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function evaluateConfiguredBranchPolicy(branchPolicy, branch) {
|
|
151
|
+
return branchPolicy.ref === branch &&
|
|
152
|
+
branchPolicy.strict === true &&
|
|
153
|
+
Number(branchPolicy.requiredApprovals || 0) >= 1 &&
|
|
154
|
+
branchPolicy.requireConversationResolution === true &&
|
|
155
|
+
branchPolicy.enforceAdmins === true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function evaluateProviderBranchPolicy(branchPolicy, branch, requiredCheck) {
|
|
159
|
+
return branchPolicy.ref === branch &&
|
|
156
160
|
branchPolicy.policyMode === "provider-enforced-transaction" &&
|
|
157
161
|
branchPolicy.protected === true &&
|
|
158
162
|
branchPolicy.enforcementLevel === "everyone" &&
|
|
159
163
|
Array.isArray(branchPolicy.requiredStatusChecks) &&
|
|
160
|
-
|
|
161
|
-
branchPolicy.requiredStatusChecks.includes(
|
|
164
|
+
requiredCheck.pass &&
|
|
165
|
+
branchPolicy.requiredStatusChecks.includes(requiredCheck.resolved) &&
|
|
162
166
|
branchPolicy.requiredCheckPassed === true &&
|
|
163
167
|
/^[0-9a-f]{40}$/i.test(String(branchPolicy.requiredCheckSha || "")) &&
|
|
164
|
-
branchPolicy.requiredCheckSha === branchPolicy.pullRequestHeadSha
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
branchPolicy.requiredCheckSha === branchPolicy.pullRequestHeadSha;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function evaluateProviderPullRequest(branchPolicy, branch, repository) {
|
|
172
|
+
return (branchPolicy.sourceSha === branchPolicy.headSha || branchPolicy.sourceContainedInBranch === true) &&
|
|
173
|
+
sourceAuthorizationPass(branchPolicy) &&
|
|
167
174
|
branchPolicy.mergedPullRequest === true &&
|
|
168
175
|
branchPolicy.baseRef === branch &&
|
|
169
176
|
branchPolicy.headRepository === repository &&
|
|
170
177
|
Number(branchPolicy.approvalCount || 0) >= 1 &&
|
|
171
178
|
branchPolicy.independentApproval === true;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
publisherPass,
|
|
203
|
-
publisher,
|
|
204
|
-
),
|
|
205
|
-
fact(
|
|
206
|
-
"runner-policy",
|
|
207
|
-
runner.class === "ephemeral" &&
|
|
208
|
-
runner.label === "ubuntu-24.04" &&
|
|
209
|
-
runner.githubHosted === true &&
|
|
210
|
-
runner.selfHostedAuthorized === false,
|
|
211
|
-
runner,
|
|
212
|
-
),
|
|
213
|
-
];
|
|
214
|
-
return createPublicationControlPlaneAudit({
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function evaluateBranchPolicy({ branchPolicy, branch, repository, requiredStatusCheck }) {
|
|
182
|
+
const requiredCheck = requiredStatusCheckBinding(branchPolicy, requiredStatusCheck);
|
|
183
|
+
return evaluateConfiguredBranchPolicy(branchPolicy, branch) || (
|
|
184
|
+
evaluateProviderBranchPolicy(branchPolicy, branch, requiredCheck) &&
|
|
185
|
+
evaluateProviderPullRequest(branchPolicy, branch, repository)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function evaluateEnvironmentPolicy(environmentPolicy, environment) {
|
|
190
|
+
return environment === "none"
|
|
191
|
+
? environmentPolicy.declared === false && environmentPolicy.exists === false
|
|
192
|
+
: environmentPolicy.name === environment &&
|
|
193
|
+
environmentPolicy.declared === true &&
|
|
194
|
+
environmentPolicy.exists === true &&
|
|
195
|
+
environmentPolicy.protected === true &&
|
|
196
|
+
environmentPolicy.branchAuthorized === true &&
|
|
197
|
+
(environmentPolicy.reviewRequired !== true || environmentPolicy.preventSelfReview === true);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function evaluateRunnerPolicy(runner) {
|
|
201
|
+
return runner.class === "ephemeral" &&
|
|
202
|
+
runner.label === "ubuntu-24.04" &&
|
|
203
|
+
runner.githubHosted === true &&
|
|
204
|
+
runner.selfHostedAuthorized === false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function evaluatePublicationControlPlaneSnapshotOptions(options) {
|
|
208
|
+
const {
|
|
215
209
|
repository,
|
|
216
210
|
workflowPath,
|
|
217
|
-
publisherWorkflowPath,
|
|
211
|
+
publisherWorkflowPath = workflowPath,
|
|
218
212
|
environment,
|
|
219
|
-
|
|
213
|
+
branch,
|
|
214
|
+
packageName,
|
|
215
|
+
publisherMode = "npm-trusted-publisher",
|
|
216
|
+
requiredStatusCheck = "check",
|
|
217
|
+
snapshot,
|
|
220
218
|
observedAt,
|
|
221
219
|
expiresAt,
|
|
220
|
+
} = options;
|
|
221
|
+
const workflowFilename = String(publisherWorkflowPath || "").split("/").pop();
|
|
222
|
+
const providerEnvironment = environment === "none" ? "" : environment;
|
|
223
|
+
const actions = snapshot?.actions || {};
|
|
224
|
+
const branchPolicy = snapshot?.branch || {};
|
|
225
|
+
const environmentPolicy = snapshot?.environment || {};
|
|
226
|
+
const oidc = snapshot?.oidc || {};
|
|
227
|
+
const publisher = snapshot?.publisher || {};
|
|
228
|
+
const runner = snapshot?.runner || {};
|
|
229
|
+
const facts = [
|
|
230
|
+
fact("actions-policy", actions.defaultWorkflowPermissions === "read" && actions.canApprovePullRequestReviews === false, actions),
|
|
231
|
+
fact("branch-policy", evaluateBranchPolicy({ branchPolicy, branch, repository, requiredStatusCheck }), branchPolicy),
|
|
232
|
+
fact("environment-policy", evaluateEnvironmentPolicy(environmentPolicy, environment), environmentPolicy),
|
|
233
|
+
fact("oidc-policy", evaluateCredentialIsolation({ oidc, publisherMode, publisherWorkflowPath, providerEnvironment }), oidc),
|
|
234
|
+
fact("publisher-policy", evaluatePublisherPolicy({
|
|
235
|
+
publisher, publisherMode, packageName, repository, workflowPath,
|
|
236
|
+
publisherWorkflowPath, workflowFilename, environment, providerEnvironment,
|
|
237
|
+
}), publisher),
|
|
238
|
+
fact("runner-policy", evaluateRunnerPolicy(runner), runner),
|
|
239
|
+
];
|
|
240
|
+
return createPublicationControlPlaneAudit({
|
|
241
|
+
repository, workflowPath, publisherWorkflowPath, environment, facts, observedAt, expiresAt,
|
|
222
242
|
});
|
|
223
243
|
}
|
|
244
|
+
|
|
245
|
+
export function evaluatePublicationControlPlaneSnapshot({ repository, workflowPath, publisherWorkflowPath = workflowPath, environment, branch, packageName, publisherMode = "npm-trusted-publisher", requiredStatusCheck = "check", snapshot, observedAt, expiresAt, } = {}) {
|
|
246
|
+
return evaluatePublicationControlPlaneSnapshotOptions({ repository, workflowPath, publisherWorkflowPath, environment, branch, packageName, publisherMode, requiredStatusCheck, snapshot, observedAt, expiresAt });
|
|
247
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
3
|
+
PUBLICATION_REHEARSAL_COMMAND,
|
|
4
|
+
RELEASE_LOCAL_CONSTRUCTIBILITY_ADR,
|
|
5
|
+
RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT,
|
|
6
|
+
} from "./publication-rehearsal-runtime.js";
|
|
7
|
+
|
|
8
|
+
export const PUBLICATION_REHEARSAL_AGENT_SECTION_START =
|
|
9
|
+
"<!-- buildchain:publication-rehearsal:v4:start -->";
|
|
10
|
+
export const PUBLICATION_REHEARSAL_AGENT_SECTION_END =
|
|
11
|
+
"<!-- buildchain:publication-rehearsal:v4:end -->";
|
|
12
|
+
export const PUBLICATION_REHEARSAL_WORKFLOW_PATH =
|
|
13
|
+
".github/workflows/publication-rehearsal.yml";
|
|
14
|
+
|
|
15
|
+
export function publicationRehearsalToml() {
|
|
16
|
+
return `[publication_rehearsal]\ncontract = ${JSON.stringify(PUBLICATION_REHEARSAL_CAPSULE_CONTRACT)}\nadr = ${JSON.stringify(RELEASE_LOCAL_CONSTRUCTIBILITY_ADR)}\ninvariant = ${JSON.stringify(RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT)}\ncommand = ${JSON.stringify(PUBLICATION_REHEARSAL_COMMAND)}\ncapsule = ".buildchain/publication-rehearsal/capsule.json"\ncandidate_root = ".buildchain/publication-rehearsal/candidate"\nstate = ".buildchain/publication-rehearsal/state.json"\nevidence = ".buildchain/publication-rehearsal/evidence.json"\n`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function assertPublicationRehearsalConfig(config) {
|
|
20
|
+
const section = config?.publication_rehearsal;
|
|
21
|
+
if (!section || typeof section !== "object" || Array.isArray(section))
|
|
22
|
+
throw new Error("publication_rehearsal table is missing");
|
|
23
|
+
const expected = {
|
|
24
|
+
contract: PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
25
|
+
adr: RELEASE_LOCAL_CONSTRUCTIBILITY_ADR,
|
|
26
|
+
invariant: RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT,
|
|
27
|
+
command: PUBLICATION_REHEARSAL_COMMAND,
|
|
28
|
+
capsule: ".buildchain/publication-rehearsal/capsule.json",
|
|
29
|
+
candidate_root: ".buildchain/publication-rehearsal/candidate",
|
|
30
|
+
state: ".buildchain/publication-rehearsal/state.json",
|
|
31
|
+
evidence: ".buildchain/publication-rehearsal/evidence.json",
|
|
32
|
+
};
|
|
33
|
+
if (
|
|
34
|
+
JSON.stringify(Object.keys(section).sort()) !==
|
|
35
|
+
JSON.stringify(Object.keys(expected).sort())
|
|
36
|
+
)
|
|
37
|
+
throw new Error(
|
|
38
|
+
`publication_rehearsal fields must be exactly: ${Object.keys(expected).sort().join(", ")}`,
|
|
39
|
+
);
|
|
40
|
+
for (const [key, value] of Object.entries(expected))
|
|
41
|
+
if (section[key] !== value)
|
|
42
|
+
throw new Error(`publication_rehearsal.${key} is stale or unsupported`);
|
|
43
|
+
return structuredClone(section);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function appendPublicationRehearsalToml(source) {
|
|
47
|
+
const current = String(source || "").trimEnd();
|
|
48
|
+
if (/^\[publication_rehearsal\]$/mu.test(current))
|
|
49
|
+
throw new Error(
|
|
50
|
+
"buildchain config already contains a publication_rehearsal table",
|
|
51
|
+
);
|
|
52
|
+
return `${current}\n\n${publicationRehearsalToml()}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function projectPublicationRehearsalToml(source) {
|
|
56
|
+
const current = String(source || "");
|
|
57
|
+
const start = current.search(/^\[publication_rehearsal\]\s*$/mu);
|
|
58
|
+
if (start === -1) return appendPublicationRehearsalToml(current);
|
|
59
|
+
const remainder = current.slice(start);
|
|
60
|
+
const newline = remainder.indexOf("\n");
|
|
61
|
+
const nextTable = remainder.slice(newline + 1).search(/^\[[^\n]+\]\s*$/mu);
|
|
62
|
+
const end =
|
|
63
|
+
nextTable === -1 ? current.length : start + newline + 1 + nextTable;
|
|
64
|
+
const prefix = current.slice(0, start).trimEnd();
|
|
65
|
+
const suffix = current.slice(end).trimStart();
|
|
66
|
+
return `${prefix}\n\n${publicationRehearsalToml()}${suffix ? `\n${suffix}` : ""}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function publicationRehearsalAgentInstructions() {
|
|
70
|
+
return `${PUBLICATION_REHEARSAL_AGENT_SECTION_START}\n## Publication rehearsal (required)\n\nBuildchain v4 rehearsal follows \`${RELEASE_LOCAL_CONSTRUCTIBILITY_ADR}\`.\n${RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT}\n\nRun the source-bound capsule with:\n\n\`\`\`sh\n${PUBLICATION_REHEARSAL_COMMAND}\n\`\`\`\n\nSimulation and replay evidence never claim production publication authority.\n${PUBLICATION_REHEARSAL_AGENT_SECTION_END}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function mergePublicationRehearsalAgentInstructions(current = "") {
|
|
74
|
+
const source = String(current || "");
|
|
75
|
+
const section = publicationRehearsalAgentInstructions();
|
|
76
|
+
const start = source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_START);
|
|
77
|
+
const end = source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_END);
|
|
78
|
+
if ((start === -1) !== (end === -1) || end < start)
|
|
79
|
+
throw new Error(
|
|
80
|
+
"AGENTS.md has an incomplete Buildchain publication rehearsal section",
|
|
81
|
+
);
|
|
82
|
+
if (start === -1)
|
|
83
|
+
return source.trim()
|
|
84
|
+
? `${source.trimEnd()}\n\n${section}\n`
|
|
85
|
+
: `# AGENTS.md\n\n${section}\n`;
|
|
86
|
+
if (
|
|
87
|
+
source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_START, start + 1) !==
|
|
88
|
+
-1 ||
|
|
89
|
+
source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_END, end + 1) !== -1
|
|
90
|
+
)
|
|
91
|
+
throw new Error(
|
|
92
|
+
"AGENTS.md has ambiguous Buildchain publication rehearsal sections",
|
|
93
|
+
);
|
|
94
|
+
return `${source.slice(0, start)}${section}${source.slice(
|
|
95
|
+
end + PUBLICATION_REHEARSAL_AGENT_SECTION_END.length,
|
|
96
|
+
)}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function publicationRehearsalWorkflow(buildchainRef) {
|
|
100
|
+
const ref = String(buildchainRef || "").trim();
|
|
101
|
+
if (!ref)
|
|
102
|
+
throw new Error("publication rehearsal workflow requires a Buildchain ref");
|
|
103
|
+
return `name: Publication Rehearsal\n\non:\n workflow_dispatch:\n inputs:\n capsule-path:\n description: "Source-bound v4 publication rehearsal capsule"\n required: true\n default: ".buildchain/publication-rehearsal/capsule.json"\n candidate-root:\n description: "Repository-relative candidate root"\n required: true\n default: ".buildchain/publication-rehearsal/candidate"\n\npermissions:\n contents: read\n\njobs:\n rehearsal:\n uses: kungfu-systems/buildchain/.github/workflows/release-tail.yml@${ref}\n with:\n buildchain-ref: ${ref}\n rehearsal-capsule-path: \${{ inputs.capsule-path }}\n candidate-root: \${{ inputs.candidate-root }}\n rehearsal-mode: simulate\n state-path: ".buildchain/publication-rehearsal/state.json"\n rehearsal-evidence-path: ".buildchain/publication-rehearsal/evidence.json"\n`;
|
|
104
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { releaseTailRoot } from "./release-tail-provider-plane.js";
|
|
5
|
+
import {
|
|
6
|
+
V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
7
|
+
V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
8
|
+
createV4PublicationRehearsalCapsule,
|
|
9
|
+
executeV4PublicationRehearsal,
|
|
10
|
+
validateV4PublicationRehearsalCapsule,
|
|
11
|
+
verifyV4PublicationRehearsalCapsule,
|
|
12
|
+
} from "./v4-publication-rehearsal.js";
|
|
13
|
+
|
|
14
|
+
export const PUBLICATION_REHEARSAL_CAPSULE_CONTRACT =
|
|
15
|
+
V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT;
|
|
16
|
+
export const PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT =
|
|
17
|
+
V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT;
|
|
18
|
+
export const PUBLICATION_REHEARSAL_DIAGNOSTIC_CONTRACT =
|
|
19
|
+
"buildchain-v4-publication-rehearsal-compatibility-diagnostic/v1";
|
|
20
|
+
export const RELEASE_LOCAL_CONSTRUCTIBILITY_ADR =
|
|
21
|
+
"docs/v4-publication-rehearsal.md";
|
|
22
|
+
export const RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT =
|
|
23
|
+
"Publication rehearsal is source-, candidate-, manifest-, config-, policy-, observation-, and shared-core-bound; rehearsal authority is effect-disabled by default and never grants production authority.";
|
|
24
|
+
export const PUBLICATION_REHEARSAL_COMMAND =
|
|
25
|
+
'buildchain release-tail rehearse --capsule "$PWD/.buildchain/publication-rehearsal/capsule.json" --candidate-root "$PWD/.buildchain/publication-rehearsal/candidate" --mode simulate --state "$PWD/.buildchain/publication-rehearsal/state.json" --evidence "$PWD/.buildchain/publication-rehearsal/evidence.json"';
|
|
26
|
+
|
|
27
|
+
const REQUIRED_V4_INPUTS = Object.freeze([
|
|
28
|
+
"source.repository",
|
|
29
|
+
"source.revision",
|
|
30
|
+
"manifest.path",
|
|
31
|
+
"manifest.root",
|
|
32
|
+
"config.path",
|
|
33
|
+
"config.root",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function migrationReceipt(surface, reasonCode) {
|
|
37
|
+
const body = {
|
|
38
|
+
schema: "buildchain-v3-v4-public-surface-migration/v1",
|
|
39
|
+
surface,
|
|
40
|
+
status: "migration-required",
|
|
41
|
+
reasonCode,
|
|
42
|
+
replacement: {
|
|
43
|
+
module: "@kungfu-tech/buildchain/v4-publication-rehearsal",
|
|
44
|
+
capsuleContract: V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
45
|
+
requiredInputs: REQUIRED_V4_INPUTS,
|
|
46
|
+
},
|
|
47
|
+
productionAuthority: false,
|
|
48
|
+
};
|
|
49
|
+
return { ...body, migrationRoot: releaseTailRoot(body) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class PublicationRehearsalError extends Error {
|
|
53
|
+
constructor(
|
|
54
|
+
message,
|
|
55
|
+
{
|
|
56
|
+
code = "publication-rehearsal-v4-migration-required",
|
|
57
|
+
classification = "migration",
|
|
58
|
+
migration = null,
|
|
59
|
+
cause,
|
|
60
|
+
} = {},
|
|
61
|
+
) {
|
|
62
|
+
super(message, cause ? { cause } : undefined);
|
|
63
|
+
this.name = "PublicationRehearsalError";
|
|
64
|
+
this.rehearsalCode = code;
|
|
65
|
+
this.rehearsalClass = classification;
|
|
66
|
+
this.migration = migration;
|
|
67
|
+
this.bindingRoot = "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasV4CreationBindings(input) {
|
|
72
|
+
return Boolean(
|
|
73
|
+
input?.source?.repository &&
|
|
74
|
+
input?.source?.revision &&
|
|
75
|
+
input?.manifest?.path &&
|
|
76
|
+
input?.manifest?.root &&
|
|
77
|
+
input?.config?.path &&
|
|
78
|
+
input?.config?.root,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requireV4CreationBindings(input, surface) {
|
|
83
|
+
if (hasV4CreationBindings(input)) return;
|
|
84
|
+
const migration = migrationReceipt(surface, "v4-source-binding-required");
|
|
85
|
+
throw new PublicationRehearsalError(
|
|
86
|
+
"v3 rehearsal capsules cannot be upgraded implicitly; provide the v4 source, manifest, and config bindings",
|
|
87
|
+
{ migration },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createPublicationRehearsalCapsule(input = {}) {
|
|
92
|
+
requireV4CreationBindings(
|
|
93
|
+
input,
|
|
94
|
+
"@kungfu-tech/buildchain/publication-rehearsal-runtime#createPublicationRehearsalCapsule",
|
|
95
|
+
);
|
|
96
|
+
return createV4PublicationRehearsalCapsule(input);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function normalizePublicationRehearsalCapsule(input) {
|
|
100
|
+
return validateV4PublicationRehearsalCapsule(input);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function verifyPublicationRehearsalCapsule({
|
|
104
|
+
capsule,
|
|
105
|
+
capsuleRoot,
|
|
106
|
+
candidateRoot = capsuleRoot,
|
|
107
|
+
} = {}) {
|
|
108
|
+
return verifyV4PublicationRehearsalCapsule({ capsule, candidateRoot });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function publicationRehearsalBindingRoot(capsule) {
|
|
112
|
+
return validateV4PublicationRehearsalCapsule(capsule).capsuleRoot;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function publicationRehearsalDiagnostic(error, { capsule } = {}) {
|
|
116
|
+
const migration = error?.migration || null;
|
|
117
|
+
const body = {
|
|
118
|
+
contract: PUBLICATION_REHEARSAL_DIAGNOSTIC_CONTRACT,
|
|
119
|
+
status: "rejected",
|
|
120
|
+
errorClass: error?.rehearsalClass || (migration ? "migration" : "input"),
|
|
121
|
+
code:
|
|
122
|
+
error?.rehearsalCode ||
|
|
123
|
+
error?.code ||
|
|
124
|
+
"publication-rehearsal-runtime-error",
|
|
125
|
+
message: String(error?.message || "publication rehearsal failed"),
|
|
126
|
+
bindingRoot: capsule?.capsuleRoot || "",
|
|
127
|
+
migration,
|
|
128
|
+
productionAuthority: false,
|
|
129
|
+
};
|
|
130
|
+
return { ...body, diagnosticRoot: releaseTailRoot(body) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function executePublicationRehearsal({
|
|
134
|
+
capsule,
|
|
135
|
+
capsuleRoot,
|
|
136
|
+
candidateRoot = capsuleRoot,
|
|
137
|
+
mode = "simulate",
|
|
138
|
+
environment = {},
|
|
139
|
+
adapters = {},
|
|
140
|
+
authority = null,
|
|
141
|
+
transaction = null,
|
|
142
|
+
checkpoint,
|
|
143
|
+
} = {}) {
|
|
144
|
+
if (
|
|
145
|
+
!environment ||
|
|
146
|
+
typeof environment !== "object" ||
|
|
147
|
+
Array.isArray(environment) ||
|
|
148
|
+
Object.keys(environment).length > 0
|
|
149
|
+
) {
|
|
150
|
+
const migration = migrationReceipt(
|
|
151
|
+
"@kungfu-tech/buildchain/publication-rehearsal-runtime#executePublicationRehearsal",
|
|
152
|
+
"ambient-environment-forbidden",
|
|
153
|
+
);
|
|
154
|
+
throw new PublicationRehearsalError(
|
|
155
|
+
"v4 rehearsal accepts no ambient environment; bind every semantic input into the capsule",
|
|
156
|
+
{ code: "undeclared-environment", classification: "input", migration },
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return executeV4PublicationRehearsal({
|
|
160
|
+
capsule,
|
|
161
|
+
candidateRoot,
|
|
162
|
+
mode,
|
|
163
|
+
adapters,
|
|
164
|
+
authority,
|
|
165
|
+
transaction,
|
|
166
|
+
checkpoint,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolvePublicationRehearsalFile(capsuleRoot, relativePath) {
|
|
171
|
+
if (!path.isAbsolute(capsuleRoot))
|
|
172
|
+
throw new PublicationRehearsalError(
|
|
173
|
+
"capsuleRoot must be an explicit absolute path",
|
|
174
|
+
{ code: "implicit-workspace-forbidden", classification: "input" },
|
|
175
|
+
);
|
|
176
|
+
const base = fs.realpathSync(capsuleRoot);
|
|
177
|
+
const resolved = path.resolve(base, String(relativePath || ""));
|
|
178
|
+
if (
|
|
179
|
+
!resolved.startsWith(`${base}${path.sep}`) ||
|
|
180
|
+
!fs.existsSync(resolved) ||
|
|
181
|
+
!fs.statSync(resolved).isFile() ||
|
|
182
|
+
fs.lstatSync(resolved).isSymbolicLink() ||
|
|
183
|
+
!fs.realpathSync(resolved).startsWith(`${base}${path.sep}`)
|
|
184
|
+
)
|
|
185
|
+
throw new PublicationRehearsalError(
|
|
186
|
+
"publication rehearsal file must be a regular file inside the explicit candidate root",
|
|
187
|
+
{ code: "capsule-path-ambiguous", classification: "input" },
|
|
188
|
+
);
|
|
189
|
+
return resolved;
|
|
190
|
+
}
|
|
@@ -47,6 +47,7 @@ const TRUSTED_ASSOCIATIONS = new Set(["COLLABORATOR", "MEMBER", "OWNER"]);
|
|
|
47
47
|
const RESUMABLE_TRANSACTION_STATES = new Set([
|
|
48
48
|
"sealed",
|
|
49
49
|
"publishing",
|
|
50
|
+
"publish_failed",
|
|
50
51
|
"published",
|
|
51
52
|
"package-published",
|
|
52
53
|
"finalizing",
|
|
@@ -354,7 +355,7 @@ function validateCandidateProvenance({
|
|
|
354
355
|
run,
|
|
355
356
|
workflow,
|
|
356
357
|
pullRequest,
|
|
357
|
-
ancestry,
|
|
358
|
+
ancestry, targetTree,
|
|
358
359
|
passport,
|
|
359
360
|
anchorProvenance,
|
|
360
361
|
expectedTransactionId,
|
|
@@ -383,7 +384,7 @@ function validateCandidateProvenance({
|
|
|
383
384
|
const boundByNumber = Array.isArray(provenanceRun?.pullRequestNumbers) && provenanceRun.pullRequestNumbers.includes(Number(pullRequest?.number));
|
|
384
385
|
const boundByHead = provenanceRun?.headSha === pullRequest?.headSha && normalizedRef(provenanceRun?.headBranch) === normalizedRef(pullRequest?.headRef);
|
|
385
386
|
if (!boundByNumber && !boundByHead) fail("pr-identity-invalid", "candidate run is not bound to the merged candidate PR", "Select the exact PR-stage Build run.");
|
|
386
|
-
if (!ancestry?.mergeIsAncestor && ancestry?.status !== "identical") fail("ancestry-invalid", "candidate merge is
|
|
387
|
+
if (!ancestry?.mergeIsAncestor && ancestry?.status !== "identical" && exactSha(targetTree, "targetTree") !== exactSha(passport?.source?.treeHash, "passport.source.treeHash")) fail("ancestry-invalid", "candidate merge is neither an ancestor nor tree-equivalent to the promotion SHA", "Promote a descendant of the verified merged candidate identity or its exact tree-equivalent protected merge result.");
|
|
387
388
|
return { repository, workflowFile };
|
|
388
389
|
}
|
|
389
390
|
|
|
@@ -428,11 +429,11 @@ function validateRecoveryTransaction({ existingTransaction, expectedTransactionI
|
|
|
428
429
|
if (expectedTransactionId && !actualTransactionId) fail("transaction-identity-conflict", `expected transaction ${expectedTransactionId} does not exist`, "Remove the stale transaction identity only if no durable transaction was ever sealed; otherwise preserve evidence and enter repair_required.");
|
|
429
430
|
if (expectedTransactionId && expectedTransactionId !== actualTransactionId) fail("transaction-identity-conflict", `existing transaction ${actualTransactionId} conflicts with expected ${expectedTransactionId}`, "Enter repair_required and inspect the durable transaction before any retry.");
|
|
430
431
|
if (existingTransaction) {
|
|
431
|
-
const expectedIdentity = releaseTransactionId({ repository, version, sourceSha:
|
|
432
|
+
const expectedIdentity = releaseTransactionId({ repository, version, sourceSha: exactSha(existingTransaction.source_sha, "existingTransaction.source_sha"), targetRef: normalizedRef(targetRef) });
|
|
432
433
|
assertEqual(existingTransaction.id, expectedIdentity, "transaction-identity-conflict", "durable transaction identity", "Enter repair_required; the durable transaction does not belong to this exact publication target.");
|
|
433
434
|
assertEqual(existingTransaction.repository, repository, "transaction-identity-conflict", "durable transaction repository", "Enter repair_required; never cross repository transaction boundaries.");
|
|
434
435
|
assertEqual(normalizedRef(existingTransaction.target_ref), normalizedRef(targetRef), "transaction-identity-conflict", "durable transaction target ref", "Enter repair_required; never retarget a sealed transaction.");
|
|
435
|
-
|
|
436
|
+
if (![existingTransaction.source_sha, existingTransaction.release_sha, existingTransaction.release_material_sha].filter(Boolean).includes(sha)) fail("transaction-identity-conflict", "recovery promotion SHA is not bound to the durable transaction", "Resume with the transaction's exact source, release, or release material SHA, or enter repair_required.");
|
|
436
437
|
assertEqual(existingTransaction.version, version, "transaction-identity-conflict", "durable transaction version", "Enter repair_required; never change a sealed publication version.");
|
|
437
438
|
assertEqual(existingTransaction.channel, channel || passport.target.channel, "transaction-identity-conflict", "durable transaction channel", "Enter repair_required; never move a transaction between channels.");
|
|
438
439
|
}
|
|
@@ -476,7 +477,7 @@ export function verifyReleaseCandidateRecovery({
|
|
|
476
477
|
const { repository, workflowFile } = validateCandidateProvenance({
|
|
477
478
|
candidateRepository, targetRepository, expectedRunId, expectedWorkflowFile,
|
|
478
479
|
expectedWorkflowName, targetRef, run, workflow, pullRequest, ancestry,
|
|
479
|
-
passport, anchorProvenance, expectedTransactionId,
|
|
480
|
+
targetTree, passport, anchorProvenance, expectedTransactionId,
|
|
480
481
|
});
|
|
481
482
|
const { sha, tree, runtimeSha, toolingSha } = validateCandidateIdentity({
|
|
482
483
|
repository, channel, targetSha, targetTree, expectedSourceTree,
|