@kungfu-tech/buildchain 4.0.2-alpha.8 → 4.0.2
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 +64 -7
- package/architecture/ci-lane-change-budget.json +366 -0
- package/architecture/internal-capabilities.json +53 -8
- package/architecture/maintainability-debt.json +147 -93
- package/architecture/maintainability-policy.json +32 -28
- 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 +74 -39
- 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 +2585 -1000
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +13 -13
- package/dist/site/node-api-registry.json +120 -115
- package/dist/site/page-registry.json +204 -51
- package/dist/site/public-surface-audit.json +3205 -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 +5061 -1659
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +1 -1
- package/docs/cli.md +21 -8
- 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 +190 -190
- 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/paper-agent-entry.js +30 -4
- package/packages/core/paper-fleet.js +1 -1
- package/packages/core/paper-runtime-channels.js +348 -0
- package/packages/core/paper-scaffold-content.js +10 -0
- package/packages/core/paper.js +37 -71
- 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-provider-plane.js +68 -1048
- package/packages/core/v4-canonical-contracts.js +10 -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 +19 -349
- 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 +90 -33
- 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/paper-work-fleet-cli.mjs +2 -0
- package/scripts/release-asset-client.mjs +91 -0
- package/scripts/release-candidate-resolver.mjs +9 -9
- 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 +13 -6
- 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,507 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
parseWorkflowDocument,
|
|
6
|
+
parseYamlUses,
|
|
7
|
+
} from "../packages/core/workflow-yaml-contract.js";
|
|
8
|
+
|
|
9
|
+
export const TAXONOMY_PATH = "architecture/workflow-taxonomy.json";
|
|
10
|
+
export const TAXONOMY_DOC = "docs/workflow-catalog.md";
|
|
11
|
+
const ROLES = ["public", "component", "self"];
|
|
12
|
+
const CATEGORIES = ["build", "release", "ops"];
|
|
13
|
+
const SLUG = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
14
|
+
const WORKFLOW = /^\.github\/workflows\/[.a-z0-9-]+\.ya?ml$/u;
|
|
15
|
+
const VERSION_TOKEN = /(?:^|[.-])v\d+(?:[.-]|$)/iu;
|
|
16
|
+
const PROTECTED = [
|
|
17
|
+
TAXONOMY_PATH,
|
|
18
|
+
"scripts/workflow-taxonomy.mjs",
|
|
19
|
+
"scripts/check-workflow-taxonomy.mjs",
|
|
20
|
+
"scripts/generate-workflow-taxonomy.mjs",
|
|
21
|
+
"scripts/check-workflows.sh",
|
|
22
|
+
"tests/workflow-taxonomy.test.mjs",
|
|
23
|
+
".github/workflows/self-build-verify.yml",
|
|
24
|
+
"package.json",
|
|
25
|
+
".buildchain/buildchain.toml",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function workflowPath(entry) {
|
|
29
|
+
const prefix = entry.role === "component" ? "." : `${entry.role}-`;
|
|
30
|
+
return `.github/workflows/${prefix}${entry.category}-${entry.purpose}.yml`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readWorkflowTaxonomy(root) {
|
|
34
|
+
const file = path.join(root, TAXONOMY_PATH);
|
|
35
|
+
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateEntryRole(entry, errors) {
|
|
39
|
+
if (!ROLES.includes(entry.role)) errors.push(`${entry.id}: invalid role`);
|
|
40
|
+
if (!CATEGORIES.includes(entry.category))
|
|
41
|
+
errors.push(`${entry.id}: invalid category`);
|
|
42
|
+
if (!SLUG.test(entry.purpose || ""))
|
|
43
|
+
errors.push(`${entry.id}: invalid purpose slug`);
|
|
44
|
+
if (VERSION_TOKEN.test(path.posix.basename(workflowPath(entry))))
|
|
45
|
+
errors.push(
|
|
46
|
+
`${entry.id}: workflow filenames cannot contain version tokens`,
|
|
47
|
+
);
|
|
48
|
+
for (const field of ["summary", "owner", "rationale"]) {
|
|
49
|
+
if (typeof entry[field] !== "string" || !entry[field].trim())
|
|
50
|
+
errors.push(`${entry.id}: missing ${field}`);
|
|
51
|
+
}
|
|
52
|
+
if (!["active", "preview", "compatibility", "retired"].includes(entry.status))
|
|
53
|
+
errors.push(`${entry.id}: invalid lifecycle status`);
|
|
54
|
+
if (
|
|
55
|
+
!["reusable", "dispatch-service", "repository"].includes(entry.invocation)
|
|
56
|
+
)
|
|
57
|
+
errors.push(`${entry.id}: invalid invocation`);
|
|
58
|
+
if ((entry.role === "self") !== (entry.invocation === "repository"))
|
|
59
|
+
errors.push(`${entry.id}: role and invocation disagree`);
|
|
60
|
+
if (entry.role === "component" && entry.invocation !== "reusable")
|
|
61
|
+
errors.push(`${entry.id}: component must be reusable`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateMigration(policy, entry, declared, errors) {
|
|
65
|
+
if (entry.retiredAlias) {
|
|
66
|
+
if (
|
|
67
|
+
entry.compatibility ||
|
|
68
|
+
entry.retiredAlias.path !== entry.migration?.previousPath ||
|
|
69
|
+
!entry.retiredAlias.reason?.trim() ||
|
|
70
|
+
entry.retiredAlias.migrationGuide !== "docs/workflow-path-migration.md"
|
|
71
|
+
)
|
|
72
|
+
errors.push(`${entry.id}: invalid alias retirement record`);
|
|
73
|
+
}
|
|
74
|
+
if (entry.migration) {
|
|
75
|
+
if (
|
|
76
|
+
!WORKFLOW.test(entry.migration.previousPath || "") ||
|
|
77
|
+
!/^[a-f0-9]{64}$/u.test(entry.migration.originalSha256 || "")
|
|
78
|
+
)
|
|
79
|
+
errors.push(`${entry.id}: invalid exact migration identity`);
|
|
80
|
+
if (
|
|
81
|
+
policy.migrationSources?.[entry.migration.previousPath] !==
|
|
82
|
+
entry.migration.originalSha256
|
|
83
|
+
)
|
|
84
|
+
errors.push(
|
|
85
|
+
`${entry.id}: migration source is not in the reviewed baseline`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (entry.compatibility) {
|
|
89
|
+
const alias = entry.compatibility;
|
|
90
|
+
if (VERSION_TOKEN.test(path.posix.basename(alias.path || "")))
|
|
91
|
+
errors.push(
|
|
92
|
+
`${entry.id}: version-prefixed compatibility aliases are retired`,
|
|
93
|
+
);
|
|
94
|
+
if (entry.role === "self")
|
|
95
|
+
errors.push(
|
|
96
|
+
`${entry.id}: repository event wrappers cannot have duplicate aliases`,
|
|
97
|
+
);
|
|
98
|
+
if (
|
|
99
|
+
!WORKFLOW.test(alias.path || "") ||
|
|
100
|
+
alias.path !== entry.migration?.previousPath
|
|
101
|
+
)
|
|
102
|
+
errors.push(
|
|
103
|
+
`${entry.id}: compatibility must name one exact migrated path`,
|
|
104
|
+
);
|
|
105
|
+
if (!alias.reason?.trim() || !alias.removalCondition?.trim())
|
|
106
|
+
errors.push(
|
|
107
|
+
`${entry.id}: compatibility requires a reason and removal condition`,
|
|
108
|
+
);
|
|
109
|
+
declared.push(alias.path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateEntries(policy, errors) {
|
|
114
|
+
if (policy?.schema !== "buildchain.workflow-taxonomy/v1")
|
|
115
|
+
errors.push("taxonomy schema is missing or unknown");
|
|
116
|
+
if (JSON.stringify(policy?.roles) !== JSON.stringify(ROLES))
|
|
117
|
+
errors.push("role vocabulary must be public/component/self");
|
|
118
|
+
if (JSON.stringify(policy?.categories) !== JSON.stringify(CATEGORIES))
|
|
119
|
+
errors.push("category vocabulary must be build/release/ops");
|
|
120
|
+
if (!/^[a-f0-9]{40}$/u.test(policy?.migrationBaseRevision || ""))
|
|
121
|
+
errors.push("migration baseline must be an exact commit");
|
|
122
|
+
if (!Array.isArray(policy?.entries) || !policy.entries.length) {
|
|
123
|
+
errors.push("taxonomy entries are missing");
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const ids = new Set();
|
|
127
|
+
const paths = new Set();
|
|
128
|
+
for (const entry of policy.entries) {
|
|
129
|
+
if (!entry.id || ids.has(entry.id))
|
|
130
|
+
errors.push(`duplicate or missing identity: ${entry.id}`);
|
|
131
|
+
ids.add(entry.id);
|
|
132
|
+
validateEntryRole(entry, errors);
|
|
133
|
+
const canonical = workflowPath(entry);
|
|
134
|
+
const declared = [canonical];
|
|
135
|
+
validateMigration(policy, entry, declared, errors);
|
|
136
|
+
for (const file of declared) {
|
|
137
|
+
if (!WORKFLOW.test(file) || paths.has(file))
|
|
138
|
+
errors.push(`${entry.id}: invalid or duplicate path ${file}`);
|
|
139
|
+
paths.add(file);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return policy.entries;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function discoverWorkflowFiles(root) {
|
|
146
|
+
const directory = path.join(root, ".github/workflows");
|
|
147
|
+
if (!fs.existsSync(directory)) return [];
|
|
148
|
+
const visit = (dir) =>
|
|
149
|
+
fs.readdirSync(dir, { withFileTypes: true }).flatMap((item) => {
|
|
150
|
+
const file = path.join(dir, item.name);
|
|
151
|
+
if (item.isDirectory()) return visit(file);
|
|
152
|
+
return /\.ya?ml$/iu.test(item.name)
|
|
153
|
+
? [path.relative(root, file).split(path.sep).join("/")]
|
|
154
|
+
: [];
|
|
155
|
+
});
|
|
156
|
+
return visit(directory).sort();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function validateRepositoryReferences(file, document, text, declared, errors) {
|
|
160
|
+
const references = document.callJobs.flatMap((job) =>
|
|
161
|
+
Object.entries(job.with)
|
|
162
|
+
.filter(
|
|
163
|
+
([key, value]) =>
|
|
164
|
+
/(?:source|handoff)-workflow-id$|(?:expected|candidate)-workflow-file$/u.test(
|
|
165
|
+
key,
|
|
166
|
+
) && value.kind === "string",
|
|
167
|
+
)
|
|
168
|
+
.map(([, value]) => value.value),
|
|
169
|
+
);
|
|
170
|
+
for (const match of text.matchAll(
|
|
171
|
+
/\bgh workflow run ([.a-z0-9-]+\.ya?ml)\b/gu,
|
|
172
|
+
))
|
|
173
|
+
references.push(match[1]);
|
|
174
|
+
for (const name of references) {
|
|
175
|
+
const target = name.startsWith(".github/")
|
|
176
|
+
? name
|
|
177
|
+
: `.github/workflows/${name}`;
|
|
178
|
+
if (!declared.has(target))
|
|
179
|
+
errors.push(`${file}: dangling repository workflow reference ${name}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function validateWorkflow(root, entry, declared, errors) {
|
|
184
|
+
const file = workflowPath(entry);
|
|
185
|
+
const absolute = path.join(root, file);
|
|
186
|
+
if (!fs.existsSync(absolute) || !fs.lstatSync(absolute).isFile()) return;
|
|
187
|
+
const text = fs.readFileSync(absolute, "utf8");
|
|
188
|
+
if (
|
|
189
|
+
entry.retiredAlias &&
|
|
190
|
+
text.includes("BUILDCHAIN_INVOKED_WORKFLOW:") &&
|
|
191
|
+
!text.includes(`BUILDCHAIN_INVOKED_WORKFLOW: ${file}\n`)
|
|
192
|
+
)
|
|
193
|
+
errors.push(
|
|
194
|
+
`${file}: consumer admission must bind the canonical invoked workflow`,
|
|
195
|
+
);
|
|
196
|
+
const document = parseWorkflowDocument(text);
|
|
197
|
+
if (entry.role === "self")
|
|
198
|
+
validateRepositoryReferences(file, document, text, declared, errors);
|
|
199
|
+
const reusable = document.triggers.includes("workflow_call");
|
|
200
|
+
if ((entry.invocation === "reusable") !== reusable)
|
|
201
|
+
errors.push(`${file}: workflow_call does not match registered invocation`);
|
|
202
|
+
if (
|
|
203
|
+
entry.invocation === "dispatch-service" &&
|
|
204
|
+
!document.triggers.includes("workflow_dispatch")
|
|
205
|
+
)
|
|
206
|
+
errors.push(`${file}: dispatch service lacks workflow_dispatch`);
|
|
207
|
+
if (
|
|
208
|
+
entry.role !== "self" &&
|
|
209
|
+
document.triggers.some(
|
|
210
|
+
(trigger) => !["workflow_call", "workflow_dispatch"].includes(trigger),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
errors.push(
|
|
214
|
+
`${file}: public/component entry has repository event triggers`,
|
|
215
|
+
);
|
|
216
|
+
if (entry.compatibility) {
|
|
217
|
+
const alias = path.join(root, entry.compatibility.path);
|
|
218
|
+
if (fs.existsSync(alias) && fs.readFileSync(alias, "utf8") !== text)
|
|
219
|
+
errors.push(
|
|
220
|
+
`${file}: compatibility implementation drift at ${entry.compatibility.path}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
for (const call of parseYamlUses(text)) {
|
|
224
|
+
const remote = call.value.match(
|
|
225
|
+
/^kungfu-systems\/buildchain\/(\.github\/workflows\/[^@]+)@/u,
|
|
226
|
+
);
|
|
227
|
+
if (remote && VERSION_TOKEN.test(path.posix.basename(remote[1])))
|
|
228
|
+
errors.push(
|
|
229
|
+
`${file}:${call.line}: retired version-prefixed workflow call ${remote[1]}`,
|
|
230
|
+
);
|
|
231
|
+
const local = call.value.match(/^\.\/(\.github\/workflows\/[^@]+)$/u);
|
|
232
|
+
if (local && !declared.has(local[1]))
|
|
233
|
+
errors.push(
|
|
234
|
+
`${file}:${call.line}: dangling workflow reference ${local[1]}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function validateGateIntegration(root, errors) {
|
|
240
|
+
const read = (file) =>
|
|
241
|
+
fs.existsSync(path.join(root, file))
|
|
242
|
+
? fs.readFileSync(path.join(root, file), "utf8")
|
|
243
|
+
: "";
|
|
244
|
+
const pkg = JSON.parse(read("package.json") || "{}");
|
|
245
|
+
const command = pkg.scripts?.["check:workflows"];
|
|
246
|
+
if (
|
|
247
|
+
command !==
|
|
248
|
+
"node scripts/check-workflow-taxonomy.mjs && bash scripts/check-workflows.sh"
|
|
249
|
+
)
|
|
250
|
+
errors.push(
|
|
251
|
+
"check:workflows must execute taxonomy before actionlint without a bypass",
|
|
252
|
+
);
|
|
253
|
+
if (
|
|
254
|
+
!/(?:^|&&)\s*pnpm run check:workflows\s*(?:&&|$)/u.test(
|
|
255
|
+
pkg.scripts?.check || "",
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
errors.push("required check chain does not enforce check:workflows");
|
|
259
|
+
const { parse: parseToml } = createRequire(import.meta.url)("smol-toml");
|
|
260
|
+
const config = parseToml(read(".buildchain/buildchain.toml"));
|
|
261
|
+
if (
|
|
262
|
+
!config.lifecycle?.verify?.commands?.includes(
|
|
263
|
+
"corepack pnpm@11.7.0 run check",
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
errors.push(
|
|
267
|
+
"declared verify lifecycle must execute the full required check",
|
|
268
|
+
);
|
|
269
|
+
const verify = read(".github/workflows/self-build-verify.yml");
|
|
270
|
+
const document = parseWorkflowDocument(verify);
|
|
271
|
+
for (const trigger of ["pull_request", "merge_group", "push"]) {
|
|
272
|
+
if (!document.triggers.some((item) => item.split(":")[0] === trigger))
|
|
273
|
+
errors.push(`required Verify workflow lacks ${trigger}`);
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
!verify.includes("Run declared verify lifecycle") ||
|
|
277
|
+
!verify.includes("--required")
|
|
278
|
+
)
|
|
279
|
+
errors.push("required Verify lifecycle integration is missing");
|
|
280
|
+
const owners = read(".github/CODEOWNERS")
|
|
281
|
+
.split(/\r?\n/u)
|
|
282
|
+
.map((line) => line.trim());
|
|
283
|
+
for (const file of PROTECTED) {
|
|
284
|
+
if (!owners.includes(`/${file} @kungfu-origin`))
|
|
285
|
+
errors.push(`independent review ownership missing for ${file}`);
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
!owners.includes("/.github/workflows/* @kungfu-origin") ||
|
|
289
|
+
!owners.includes("/.github/CODEOWNERS @kungfu-origin")
|
|
290
|
+
)
|
|
291
|
+
errors.push(
|
|
292
|
+
"workflow and CODEOWNERS review ownership must remain explicit",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function checkWorkflowTaxonomy(
|
|
297
|
+
root,
|
|
298
|
+
{ integration = true, documentation = true } = {},
|
|
299
|
+
) {
|
|
300
|
+
const errors = [];
|
|
301
|
+
const policy = readWorkflowTaxonomy(root);
|
|
302
|
+
const entries = validateEntries(policy, errors);
|
|
303
|
+
if (errors.length) return { ok: false, errors };
|
|
304
|
+
const declared = new Set(
|
|
305
|
+
entries.flatMap((entry) => [
|
|
306
|
+
workflowPath(entry),
|
|
307
|
+
...(entry.compatibility ? [entry.compatibility.path] : []),
|
|
308
|
+
]),
|
|
309
|
+
);
|
|
310
|
+
const observed = new Set(discoverWorkflowFiles(root));
|
|
311
|
+
for (const file of observed) {
|
|
312
|
+
if (VERSION_TOKEN.test(path.posix.basename(file)))
|
|
313
|
+
errors.push(`workflow filenames cannot contain version tokens: ${file}`);
|
|
314
|
+
if (!declared.has(file)) errors.push(`unregistered workflow: ${file}`);
|
|
315
|
+
if (!fs.lstatSync(path.join(root, file)).isFile())
|
|
316
|
+
errors.push(`workflow must be a regular file: ${file}`);
|
|
317
|
+
}
|
|
318
|
+
for (const file of declared)
|
|
319
|
+
if (!observed.has(file))
|
|
320
|
+
errors.push(`registered workflow missing: ${file}`);
|
|
321
|
+
for (const entry of entries) validateWorkflow(root, entry, declared, errors);
|
|
322
|
+
if (integration) validateGateIntegration(root, errors);
|
|
323
|
+
if (documentation) {
|
|
324
|
+
const file = path.join(root, TAXONOMY_DOC);
|
|
325
|
+
if (
|
|
326
|
+
!fs.existsSync(file) ||
|
|
327
|
+
fs.readFileSync(file, "utf8") !== renderWorkflowCatalog(policy)
|
|
328
|
+
)
|
|
329
|
+
errors.push("generated workflow catalog is stale");
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
ok: errors.length === 0,
|
|
333
|
+
canonicalCount: entries.length,
|
|
334
|
+
compatibilityCount: entries.filter((entry) => entry.compatibility).length,
|
|
335
|
+
fileCount: observed.size,
|
|
336
|
+
errors,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Existing lane/debt baselines name immutable pre-migration identities. Count
|
|
341
|
+
// one logical implementation only after proving every physical alias identical.
|
|
342
|
+
export function projectWorkflowIdentities(root, workflows) {
|
|
343
|
+
const policy = readWorkflowTaxonomy(root);
|
|
344
|
+
if (!policy) return workflows;
|
|
345
|
+
const result = checkWorkflowTaxonomy(root, {
|
|
346
|
+
integration: false,
|
|
347
|
+
documentation: false,
|
|
348
|
+
});
|
|
349
|
+
if (!result.ok) throw new Error(result.errors.join("\n"));
|
|
350
|
+
const byPath = new Map(
|
|
351
|
+
workflows.map((workflow) => [workflow.path, workflow]),
|
|
352
|
+
);
|
|
353
|
+
return policy.entries
|
|
354
|
+
.map((entry) => {
|
|
355
|
+
const workflow = byPath.get(workflowPath(entry));
|
|
356
|
+
if (!workflow)
|
|
357
|
+
throw new Error(`missing canonical workflow: ${workflowPath(entry)}`);
|
|
358
|
+
return {
|
|
359
|
+
...workflow,
|
|
360
|
+
path: entry.migration?.previousPath || workflow.path,
|
|
361
|
+
};
|
|
362
|
+
})
|
|
363
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function writeWorkflowSource(root, relative, text) {
|
|
367
|
+
const policy = readWorkflowTaxonomy(root);
|
|
368
|
+
const entry = policy?.entries.find(
|
|
369
|
+
(item) =>
|
|
370
|
+
workflowPath(item) === relative || item.compatibility?.path === relative,
|
|
371
|
+
);
|
|
372
|
+
if (policy?.entries.some((item) => item.retiredAlias?.path === relative))
|
|
373
|
+
throw new Error(`${relative}: cannot regenerate a retired workflow alias`);
|
|
374
|
+
const outputs = entry
|
|
375
|
+
? [
|
|
376
|
+
workflowPath(entry),
|
|
377
|
+
...(entry.compatibility ? [entry.compatibility.path] : []),
|
|
378
|
+
]
|
|
379
|
+
: [relative];
|
|
380
|
+
for (const file of outputs) fs.writeFileSync(path.join(root, file), text);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function rewriteRepositoryWorkflowPaths(root, text) {
|
|
384
|
+
for (const entry of readWorkflowTaxonomy(root)?.entries || []) {
|
|
385
|
+
if (entry.role === "self" && entry.migration)
|
|
386
|
+
text = text.replaceAll(entry.migration.previousPath, workflowPath(entry));
|
|
387
|
+
if (entry.retiredAlias)
|
|
388
|
+
text = text.replaceAll(entry.retiredAlias.path, workflowPath(entry));
|
|
389
|
+
if (entry.id === "build-surface-fixture")
|
|
390
|
+
text = text.replaceAll(
|
|
391
|
+
"build-surface-fixture.yml",
|
|
392
|
+
path.posix.basename(workflowPath(entry)),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return text;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Historical inventories and debt ledgers retain their original identity. File
|
|
399
|
+
// access resolves only explicitly retired aliases to the registered implementation.
|
|
400
|
+
export function currentWorkflowPath(root, relative) {
|
|
401
|
+
const entry = readWorkflowTaxonomy(root)?.entries.find(
|
|
402
|
+
(item) => item.retiredAlias?.path === relative,
|
|
403
|
+
);
|
|
404
|
+
return entry ? workflowPath(entry) : relative;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function workflowCompatibilityIdentity(root, relative, text) {
|
|
408
|
+
const entry = readWorkflowTaxonomy(root)?.entries.find(
|
|
409
|
+
(item) => workflowPath(item) === relative && item.compatibility,
|
|
410
|
+
);
|
|
411
|
+
if (!entry) return relative;
|
|
412
|
+
const source = text ?? fs.readFileSync(path.join(root, relative), "utf8");
|
|
413
|
+
if (
|
|
414
|
+
source !==
|
|
415
|
+
fs.readFileSync(path.join(root, entry.compatibility.path), "utf8")
|
|
416
|
+
)
|
|
417
|
+
throw new Error(`${relative}: compatibility implementation drift`);
|
|
418
|
+
return entry.compatibility.path;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function renderWorkflowCatalog(policy) {
|
|
422
|
+
const lines = [
|
|
423
|
+
"---",
|
|
424
|
+
"status: active",
|
|
425
|
+
"period: ongoing",
|
|
426
|
+
"theme: workflow-taxonomy",
|
|
427
|
+
"doc_type: technical-reference",
|
|
428
|
+
"source_level: local-files",
|
|
429
|
+
"confidence: high",
|
|
430
|
+
"sensitivity: public",
|
|
431
|
+
"evidence_grade: B",
|
|
432
|
+
"review_state: unreviewed",
|
|
433
|
+
"last_reviewed: 2026-09-05",
|
|
434
|
+
"ai_provenance:",
|
|
435
|
+
" model_family: GPT-6",
|
|
436
|
+
" product: Codex",
|
|
437
|
+
" generated_at: 2026-09-05",
|
|
438
|
+
" visible_context: Repository workflow source and the approved naming scheme",
|
|
439
|
+
" invisible_context_boundary: No credentials or unpublished provider configuration inspected",
|
|
440
|
+
"---",
|
|
441
|
+
"",
|
|
442
|
+
"# Workflow catalog",
|
|
443
|
+
"",
|
|
444
|
+
"Generated from `architecture/workflow-taxonomy.json`; edit that source, then run",
|
|
445
|
+
"`pnpm run generate:workflows`. Every YAML file, including compatibility paths,",
|
|
446
|
+
"is checked by the existing required workflow check.",
|
|
447
|
+
"",
|
|
448
|
+
"## Naming and ownership",
|
|
449
|
+
"",
|
|
450
|
+
"`public-<build|release|ops>-<purpose>.yml` is a consumer entry;",
|
|
451
|
+
"`.<build|release|ops>-<purpose>.yml` is an advanced component;",
|
|
452
|
+
"`self-<build|release|ops>-<purpose>.yml` is Buildchain's own automation.",
|
|
453
|
+
"Public is a product role, not GitHub access control. Dispatch services are",
|
|
454
|
+
"marked separately from reusable workflows. Keep workflow names, job IDs,",
|
|
455
|
+
"permissions, and trigger semantics stable during path migration.",
|
|
456
|
+
"",
|
|
457
|
+
"## Channel rollout",
|
|
458
|
+
"",
|
|
459
|
+
"Canonical names become callable on a channel only after that channel publishes",
|
|
460
|
+
"them. Existing `@v4`/`@v4-alpha` consumers keep their exact registered legacy",
|
|
461
|
+
"paths until provider readback proves canonical availability. New examples below",
|
|
462
|
+
"describe the target surface, not a claim that both channels already publish it.",
|
|
463
|
+
"Compatibility files are generated byte-identical projections, not independently",
|
|
464
|
+
"editable implementations. They retain full jobs to preserve called-workflow",
|
|
465
|
+
"identity, permission envelopes, and required check names without extra nesting.",
|
|
466
|
+
"",
|
|
467
|
+
"The controller registry records the actual source path separately from a migrated",
|
|
468
|
+
"repository controller contractPath. This preserves the established logical",
|
|
469
|
+
"contract identity while exposing its current execution source. New workflows",
|
|
470
|
+
"do not acquire historical identities. Public controllers retain their real",
|
|
471
|
+
"compatibility paths while those paths remain supported.",
|
|
472
|
+
"",
|
|
473
|
+
"## Adding or changing a workflow",
|
|
474
|
+
"",
|
|
475
|
+
"Register a stable identity, role, category, purpose, owner, lifecycle status,",
|
|
476
|
+
"invocation and concrete summary before creating its derived path. A new entry",
|
|
477
|
+
"must explain its scope in review. New aliases cannot be invented: compatibility",
|
|
478
|
+
"must name an exact migration source with a reason and removal condition.",
|
|
479
|
+
"The policy, generator, checker, tests, required CI chain and CODEOWNERS require",
|
|
480
|
+
"independent `kungfu-origin` review. No wildcard compatibility admission is allowed.",
|
|
481
|
+
"Workflow filenames cannot contain version tokens such as `v4` or `v5`.",
|
|
482
|
+
"Retired aliases cannot be recreated or called; see [path migration](workflow-path-migration.md).",
|
|
483
|
+
"",
|
|
484
|
+
"A role, category or implementation edit must preserve or deliberately update",
|
|
485
|
+
"all related declarations. Run `pnpm run generate:workflows` and",
|
|
486
|
+
"`pnpm run check:workflows`; the full `pnpm run check` remains required.",
|
|
487
|
+
"",
|
|
488
|
+
];
|
|
489
|
+
for (const role of ROLES) {
|
|
490
|
+
lines.push(
|
|
491
|
+
`## ${role}`,
|
|
492
|
+
"",
|
|
493
|
+
"| Canonical workflow | Category | Invocation | Status | Purpose | Compatibility path |",
|
|
494
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
495
|
+
);
|
|
496
|
+
for (const entry of policy.entries
|
|
497
|
+
.filter((item) => item.role === role)
|
|
498
|
+
.sort((a, b) => workflowPath(a).localeCompare(workflowPath(b)))) {
|
|
499
|
+
const name = path.posix.basename(workflowPath(entry));
|
|
500
|
+
lines.push(
|
|
501
|
+
`| [${name}](../${workflowPath(entry)}) | ${entry.category} | ${entry.invocation} | ${entry.status} | ${entry.summary.replaceAll("|", "\\|")} | ${entry.compatibility ? `\`${path.posix.basename(entry.compatibility.path)}\`` : "—"} |`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
lines.push("");
|
|
505
|
+
}
|
|
506
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
507
|
+
}
|
|
@@ -38,11 +38,8 @@ on:
|
|
|
38
38
|
source-patch-root:
|
|
39
39
|
description: "Semantic patch root"
|
|
40
40
|
required: true
|
|
41
|
-
|
|
42
|
-
description: "
|
|
43
|
-
required: true
|
|
44
|
-
initiative-root:
|
|
45
|
-
description: "Parent Initiative root"
|
|
41
|
+
native-roots-json:
|
|
42
|
+
description: "Machine-supplied Work sourceRoot or historical root pair"
|
|
46
43
|
required: true
|
|
47
44
|
plan-root:
|
|
48
45
|
description: "Native qualification plan root"
|
|
@@ -117,8 +114,8 @@ jobs:
|
|
|
117
114
|
delivery-warrant-mode: required
|
|
118
115
|
handoff-workflow-id: native-dev-delivery.yml
|
|
119
116
|
source-workflow-id: ${{ inputs.source-workflow-id || '' }}
|
|
120
|
-
assignment-root: ${{ github.event.client_payload.candidate.assignmentRoot || inputs.
|
|
121
|
-
initiative-root: ${{ github.event.client_payload.candidate.initiativeRoot || inputs.
|
|
117
|
+
assignment-root: ${{ github.event.client_payload.candidate.assignmentRoot || fromJSON(inputs.native-roots-json || '{}').assignmentRoot || '' }}
|
|
118
|
+
initiative-root: ${{ github.event.client_payload.candidate.initiativeRoot || fromJSON(inputs.native-roots-json || '{}').initiativeRoot || '' }}
|
|
122
119
|
source-identity-root: ${{ github.event.client_payload.candidate.sourceIdentityRoot || inputs.source-identity-root }}
|
|
123
120
|
source-patch-root: ${{ github.event.client_payload.candidate.sourcePatchRoot || inputs.source-patch-root }}
|
|
124
121
|
plan-root: ${{ github.event.client_payload.candidate.planRoot || inputs.plan-root }}
|