@kungfu-tech/buildchain 4.0.2-alpha.34 → 4.0.2-alpha.35
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 +9 -0
- package/CONTRIBUTING.md +24 -0
- package/README.md +2 -2
- package/actions/promote-buildchain-ref/README.md +1 -1
- package/architecture/agent-change-map.md +6 -4
- package/architecture/internal-capabilities.json +4 -4
- package/architecture/maintainability-debt.json +13 -11
- package/architecture/maintainability-policy.json +4 -4
- package/architecture/release-tail-contract-inventory.json +6 -6
- 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 +1 -1
- package/architecture/v4-next-development-parity.json +4 -4
- package/architecture/v4-platform-stage-checkpoints.json +1 -1
- package/architecture/v4-publication-facade-patches.json +53 -0
- package/architecture/v4-release-topology.json +46 -29
- package/architecture/v4-rust-wasm-production-authority.json +1 -0
- package/architecture/v4-stage-capsule-qualification.json +1 -1
- package/architecture/v4-universal-workflow-bootstrap.json +1 -1
- package/architecture/v4-universal-workflow-fault-campaign.json +1 -1
- package/architecture/v4-universal-workflow-train-admission.json +4 -4
- package/architecture/workflow-taxonomy.json +1437 -0
- package/contracts/buildchain-v2-residuals-v1.json +64 -1
- 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 +20 -20
- package/dist/site/buildchain-site.json +122 -56
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +5 -4
- package/dist/site/kfd-claims.json +2617 -964
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -10
- package/dist/site/node-api-registry.json +26 -21
- package/dist/site/page-registry.json +107 -41
- package/dist/site/public-surface-audit.json +3196 -1128
- package/dist/site/publication-authority-registry.json +1179 -417
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +14 -14
- package/dist/site/workflow-registry.json +5484 -1924
- 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/next-development-transition.md +1 -1
- package/docs/node-api-reference.md +30 -30
- 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/v4-adopter-delivery.md +1 -1
- package/docs/v4-production-release.md +45 -8
- package/docs/v4-stage-capsule.md +1 -1
- package/docs/workflow-catalog.md +151 -0
- package/package.json +8 -6
- package/packages/core/buildchain-contract.js +1 -1
- package/packages/core/buildchain-kfd-claims.js +2 -2
- package/packages/core/buildchain-publication-authority.js +40 -25
- package/packages/core/buildchain-v4-domain.wasm +0 -0
- package/packages/core/controller-evidence.js +4 -1
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/next-development-projection.js +1 -1
- package/packages/core/release-line-dry-run.js +1 -1
- package/packages/core/v4-canonical-contracts.js +1 -0
- package/packages/core/v4-domain-wasm-artifact.js +2 -2
- package/packages/core/v4-floating-consumer-evidence.js +1 -1
- package/packages/core/v4-floating-consumer-policy.js +1 -1
- package/scripts/binary-publication-evidence.mjs +135 -0
- package/scripts/check-ci-lane-change-budget.mjs +4 -1
- package/scripts/check-core-mechanism-inventory.mjs +4 -3
- package/scripts/check-inventory.mjs +22 -22
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +1 -1
- package/scripts/check-v4-public-dogfood-contract.mjs +18 -5
- package/scripts/check-v4-release-topology.mjs +5 -2
- package/scripts/check-v4-runtime-semantic-closure.mjs +4 -1
- package/scripts/check-v4-universal-workflow-bootstrap.mjs +5 -9
- package/scripts/check-workflow-taxonomy.mjs +6 -0
- package/scripts/generate-channel-build-workflow.mjs +2 -1
- package/scripts/generate-channel-promotion-workflow.mjs +4 -3
- package/scripts/generate-site-bundle.mjs +11 -11
- package/scripts/generate-v4-universal-workflow-facades.mjs +24 -6
- package/scripts/generate-workflow-taxonomy.mjs +25 -0
- package/scripts/maintainability-metrics.mjs +16 -5
- package/scripts/release-asset-client.mjs +90 -0
- package/scripts/release-candidate-resolver.mjs +1 -1
- package/scripts/source-verification-evidence.mjs +344 -0
- package/scripts/stable-candidate-qualification.mjs +1 -1
- package/scripts/v4-publication-settlement.mjs +207 -0
- package/scripts/v4-universal-workflow-self-dogfood.mjs +1 -1
- package/scripts/verify-version-state-delta.mjs +173 -0
- package/scripts/workflow-taxonomy.mjs +468 -0
|
@@ -102,7 +102,7 @@ export function createV4UniversalSelfDogfoodRequest({
|
|
|
102
102
|
},
|
|
103
103
|
consumer: {
|
|
104
104
|
repository: "kungfu-systems/buildchain",
|
|
105
|
-
workflow: ".github/workflows/
|
|
105
|
+
workflow: ".github/workflows/self-ops-bootstrap-dogfood.yml",
|
|
106
106
|
sourceSha,
|
|
107
107
|
},
|
|
108
108
|
capability: {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { invokeV4DomainWasm } from "../packages/core/v4-domain-wasm.js";
|
|
8
|
+
|
|
9
|
+
const execute = (command, args, options = {}) =>
|
|
10
|
+
execFileSync(command, args, {
|
|
11
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
12
|
+
...options,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Reconstruct with the protected base's generator. No candidate command runs.
|
|
16
|
+
export function verifyVersionStateDelta({
|
|
17
|
+
baseSha,
|
|
18
|
+
headSha,
|
|
19
|
+
cwd = process.cwd(),
|
|
20
|
+
nodeModules = path.resolve("node_modules"),
|
|
21
|
+
}) {
|
|
22
|
+
if (![baseSha, headSha].every((value) => /^[a-f0-9]{40}$/u.test(value || "")))
|
|
23
|
+
throw new Error("version-state verification requires exact commits");
|
|
24
|
+
const git = (...args) => execute("git", args, { cwd }).toString().trim();
|
|
25
|
+
const bytes = (revision, file) =>
|
|
26
|
+
execute("git", ["show", `${revision}:${file}`], { cwd });
|
|
27
|
+
git("merge-base", "--is-ancestor", baseSha, headSha);
|
|
28
|
+
const changes = git("diff", "--raw", "--no-renames", baseSha, headSha)
|
|
29
|
+
.split("\n")
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
if (!changes.length) throw new Error("no version-state delta");
|
|
32
|
+
const temporary = fs.mkdtempSync(
|
|
33
|
+
path.join(os.tmpdir(), "buildchain-version-state-"),
|
|
34
|
+
);
|
|
35
|
+
const source = path.join(temporary, "source");
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(source);
|
|
38
|
+
const archive = path.join(temporary, "base.tar");
|
|
39
|
+
git("-c", "core.autocrlf=false", "-c", "core.eol=lf", "archive", "--format=tar", `--output=${archive}`, baseSha);
|
|
40
|
+
execute("tar", ["-xf", "base.tar", "-C", "source"], { cwd: temporary });
|
|
41
|
+
fs.symlinkSync(nodeModules, path.join(source, "node_modules"), "junction");
|
|
42
|
+
const planSource = `
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import { loadBuildchainConfig, discoverConfiguredVersionStateFiles,
|
|
45
|
+
updateConfiguredVersionStateContents } from './packages/core/buildchain-config.js';
|
|
46
|
+
const config = loadBuildchainConfig(process.cwd());
|
|
47
|
+
const files = discoverConfiguredVersionStateFiles(process.cwd(), config);
|
|
48
|
+
const allowed = [...new Set([...files.map(f => f.path), ...config.config.version.derivedFiles])];
|
|
49
|
+
if (JSON.stringify(config.config.lifecycle['version-state'].commands) !== JSON.stringify(['node scripts/generate-site-bundle.mjs']))
|
|
50
|
+
throw new Error('unsupported version-state generator');
|
|
51
|
+
if (process.env.BUILDCHAIN_DELTA_VERSION)
|
|
52
|
+
for (const f of updateConfiguredVersionStateContents(files, process.env.BUILDCHAIN_DELTA_VERSION))
|
|
53
|
+
fs.writeFileSync(f.path, f.content);
|
|
54
|
+
console.log(JSON.stringify(allowed));
|
|
55
|
+
`;
|
|
56
|
+
const plan = (version = "") =>
|
|
57
|
+
JSON.parse(
|
|
58
|
+
execute(process.execPath, ["--input-type=module", "-e", planSource], {
|
|
59
|
+
cwd: source,
|
|
60
|
+
env: { ...process.env, BUILDCHAIN_DELTA_VERSION: version },
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
const allowed = plan();
|
|
64
|
+
for (const file of allowed)
|
|
65
|
+
if (path.isAbsolute(file) || file.split(/[\\/]/u).includes(".."))
|
|
66
|
+
throw new Error("unsafe declared version-state path");
|
|
67
|
+
for (const change of changes) {
|
|
68
|
+
const match = change.match(
|
|
69
|
+
/^:100644 100644 [a-f0-9]+ [a-f0-9]+ M\t(.+)$/u,
|
|
70
|
+
);
|
|
71
|
+
if (!match || !allowed.includes(match[1]))
|
|
72
|
+
throw new Error(
|
|
73
|
+
"delta includes undeclared source, lock, config or file mode changes",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const version = JSON.parse(bytes(headSha, "package.json")).version;
|
|
77
|
+
invokeV4DomainWasm("source-version-projection", {
|
|
78
|
+
baseVersion: JSON.parse(bytes(baseSha, "package.json")).version,
|
|
79
|
+
version,
|
|
80
|
+
});
|
|
81
|
+
const metadata = JSON.parse(bytes(headSha, "dist/site/site-manifest.json"));
|
|
82
|
+
if (
|
|
83
|
+
metadata.sourceRevision !== baseSha ||
|
|
84
|
+
metadata.package.version !== version ||
|
|
85
|
+
!Number.isFinite(Date.parse(metadata.generatedAt)) ||
|
|
86
|
+
metadata.generatedAt !== metadata.publishedAt
|
|
87
|
+
)
|
|
88
|
+
throw new Error(
|
|
89
|
+
"version-state projection source metadata is not bound to base",
|
|
90
|
+
);
|
|
91
|
+
plan(version);
|
|
92
|
+
execute(process.execPath, ["scripts/generate-site-bundle.mjs"], {
|
|
93
|
+
cwd: source,
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
BUILDCHAIN_SOURCE_SHA: baseSha,
|
|
97
|
+
BUILDCHAIN_SITE_GENERATED_AT: metadata.generatedAt,
|
|
98
|
+
BUILDCHAIN_SITE_PUBLISHED_AT: metadata.publishedAt,
|
|
99
|
+
BUILDCHAIN_SITE_TIMESTAMP_POLICY: "ci-injected",
|
|
100
|
+
BUILDCHAIN_SURFACE_GENERATED_AT: metadata.generatedAt,
|
|
101
|
+
BUILDCHAIN_SURFACE_PUBLISHED_AT: metadata.publishedAt,
|
|
102
|
+
SOURCE_DATE_EPOCH: "0",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
// Git blob hashes compare all tracked bytes without executing candidate code.
|
|
106
|
+
const expected = new Map(
|
|
107
|
+
git("ls-tree", "-r", headSha)
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map((line) => {
|
|
110
|
+
const [meta, file] = line.split("\t");
|
|
111
|
+
return [file, meta.split(" ")[2]];
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
for (const [file, digest] of expected) {
|
|
115
|
+
const generated = path.join(source, file);
|
|
116
|
+
const bytes = fs.lstatSync(generated).isSymbolicLink()
|
|
117
|
+
? Buffer.from(fs.readlinkSync(generated))
|
|
118
|
+
: fs.readFileSync(generated);
|
|
119
|
+
const actual = crypto
|
|
120
|
+
.createHash("sha1")
|
|
121
|
+
.update(`blob ${bytes.length}\0`)
|
|
122
|
+
.update(bytes)
|
|
123
|
+
.digest("hex");
|
|
124
|
+
if (actual !== digest)
|
|
125
|
+
throw new Error(`version-state regeneration mismatch: ${file}`);
|
|
126
|
+
}
|
|
127
|
+
const walk = (directory) => {
|
|
128
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
129
|
+
const absolute = path.join(directory, entry.name);
|
|
130
|
+
if (absolute === path.join(source, "node_modules")) continue;
|
|
131
|
+
if (entry.isDirectory()) walk(absolute);
|
|
132
|
+
else if (
|
|
133
|
+
!expected.has(
|
|
134
|
+
path.relative(source, absolute).split(path.sep).join("/"),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
throw new Error("version-state generator created undeclared output");
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
walk(source);
|
|
141
|
+
return {
|
|
142
|
+
validationKind: "version-state-projection",
|
|
143
|
+
baseSha,
|
|
144
|
+
headSha,
|
|
145
|
+
sourceTree: git("rev-parse", `${headSha}^{tree}`),
|
|
146
|
+
version,
|
|
147
|
+
changedPaths: changes.map((change) => change.split("\t")[1]),
|
|
148
|
+
generator: "scripts/generate-site-bundle.mjs",
|
|
149
|
+
generatedAt: metadata.generatedAt,
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
process.argv[1] &&
|
|
158
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
159
|
+
) {
|
|
160
|
+
try {
|
|
161
|
+
console.log(
|
|
162
|
+
JSON.stringify(
|
|
163
|
+
verifyVersionStateDelta({
|
|
164
|
+
baseSha: process.argv[2],
|
|
165
|
+
headSha: process.argv[3],
|
|
166
|
+
}),
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(error.message);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
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 PROTECTED = [
|
|
16
|
+
TAXONOMY_PATH,
|
|
17
|
+
"scripts/workflow-taxonomy.mjs",
|
|
18
|
+
"scripts/check-workflow-taxonomy.mjs",
|
|
19
|
+
"scripts/generate-workflow-taxonomy.mjs",
|
|
20
|
+
"scripts/check-workflows.sh",
|
|
21
|
+
"tests/workflow-taxonomy.test.mjs",
|
|
22
|
+
".github/workflows/self-build-verify.yml",
|
|
23
|
+
"package.json",
|
|
24
|
+
".buildchain/buildchain.toml",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function workflowPath(entry) {
|
|
28
|
+
const prefix = entry.role === "component" ? "." : `${entry.role}-`;
|
|
29
|
+
return `.github/workflows/${prefix}${entry.category}-${entry.purpose}.yml`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readWorkflowTaxonomy(root) {
|
|
33
|
+
const file = path.join(root, TAXONOMY_PATH);
|
|
34
|
+
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateEntryRole(entry, errors) {
|
|
38
|
+
if (!ROLES.includes(entry.role)) errors.push(`${entry.id}: invalid role`);
|
|
39
|
+
if (!CATEGORIES.includes(entry.category))
|
|
40
|
+
errors.push(`${entry.id}: invalid category`);
|
|
41
|
+
if (!SLUG.test(entry.purpose || ""))
|
|
42
|
+
errors.push(`${entry.id}: invalid purpose slug`);
|
|
43
|
+
for (const field of ["summary", "owner", "rationale"]) {
|
|
44
|
+
if (typeof entry[field] !== "string" || !entry[field].trim())
|
|
45
|
+
errors.push(`${entry.id}: missing ${field}`);
|
|
46
|
+
}
|
|
47
|
+
if (!["active", "preview", "compatibility", "retired"].includes(entry.status))
|
|
48
|
+
errors.push(`${entry.id}: invalid lifecycle status`);
|
|
49
|
+
if (
|
|
50
|
+
!["reusable", "dispatch-service", "repository"].includes(entry.invocation)
|
|
51
|
+
)
|
|
52
|
+
errors.push(`${entry.id}: invalid invocation`);
|
|
53
|
+
if ((entry.role === "self") !== (entry.invocation === "repository"))
|
|
54
|
+
errors.push(`${entry.id}: role and invocation disagree`);
|
|
55
|
+
if (entry.role === "component" && entry.invocation !== "reusable")
|
|
56
|
+
errors.push(`${entry.id}: component must be reusable`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateMigration(policy, entry, declared, errors) {
|
|
60
|
+
if (entry.migration) {
|
|
61
|
+
if (
|
|
62
|
+
!WORKFLOW.test(entry.migration.previousPath || "") ||
|
|
63
|
+
!/^[a-f0-9]{64}$/u.test(entry.migration.originalSha256 || "")
|
|
64
|
+
)
|
|
65
|
+
errors.push(`${entry.id}: invalid exact migration identity`);
|
|
66
|
+
if (
|
|
67
|
+
policy.migrationSources?.[entry.migration.previousPath] !==
|
|
68
|
+
entry.migration.originalSha256
|
|
69
|
+
)
|
|
70
|
+
errors.push(
|
|
71
|
+
`${entry.id}: migration source is not in the reviewed baseline`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (entry.compatibility) {
|
|
75
|
+
const alias = entry.compatibility;
|
|
76
|
+
if (entry.role === "self")
|
|
77
|
+
errors.push(
|
|
78
|
+
`${entry.id}: repository event wrappers cannot have duplicate aliases`,
|
|
79
|
+
);
|
|
80
|
+
if (
|
|
81
|
+
!WORKFLOW.test(alias.path || "") ||
|
|
82
|
+
alias.path !== entry.migration?.previousPath
|
|
83
|
+
)
|
|
84
|
+
errors.push(
|
|
85
|
+
`${entry.id}: compatibility must name one exact migrated path`,
|
|
86
|
+
);
|
|
87
|
+
if (!alias.reason?.trim() || !alias.removalCondition?.trim())
|
|
88
|
+
errors.push(
|
|
89
|
+
`${entry.id}: compatibility requires a reason and removal condition`,
|
|
90
|
+
);
|
|
91
|
+
declared.push(alias.path);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateEntries(policy, errors) {
|
|
96
|
+
if (policy?.schema !== "buildchain.workflow-taxonomy/v1")
|
|
97
|
+
errors.push("taxonomy schema is missing or unknown");
|
|
98
|
+
if (JSON.stringify(policy?.roles) !== JSON.stringify(ROLES))
|
|
99
|
+
errors.push("role vocabulary must be public/component/self");
|
|
100
|
+
if (JSON.stringify(policy?.categories) !== JSON.stringify(CATEGORIES))
|
|
101
|
+
errors.push("category vocabulary must be build/release/ops");
|
|
102
|
+
if (!/^[a-f0-9]{40}$/u.test(policy?.migrationBaseRevision || ""))
|
|
103
|
+
errors.push("migration baseline must be an exact commit");
|
|
104
|
+
if (!Array.isArray(policy?.entries) || !policy.entries.length) {
|
|
105
|
+
errors.push("taxonomy entries are missing");
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const ids = new Set();
|
|
109
|
+
const paths = new Set();
|
|
110
|
+
for (const entry of policy.entries) {
|
|
111
|
+
if (!entry.id || ids.has(entry.id))
|
|
112
|
+
errors.push(`duplicate or missing identity: ${entry.id}`);
|
|
113
|
+
ids.add(entry.id);
|
|
114
|
+
validateEntryRole(entry, errors);
|
|
115
|
+
const canonical = workflowPath(entry);
|
|
116
|
+
const declared = [canonical];
|
|
117
|
+
validateMigration(policy, entry, declared, errors);
|
|
118
|
+
for (const file of declared) {
|
|
119
|
+
if (!WORKFLOW.test(file) || paths.has(file))
|
|
120
|
+
errors.push(`${entry.id}: invalid or duplicate path ${file}`);
|
|
121
|
+
paths.add(file);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return policy.entries;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function discoverWorkflowFiles(root) {
|
|
128
|
+
const directory = path.join(root, ".github/workflows");
|
|
129
|
+
if (!fs.existsSync(directory)) return [];
|
|
130
|
+
const visit = (dir) =>
|
|
131
|
+
fs.readdirSync(dir, { withFileTypes: true }).flatMap((item) => {
|
|
132
|
+
const file = path.join(dir, item.name);
|
|
133
|
+
if (item.isDirectory()) return visit(file);
|
|
134
|
+
return /\.ya?ml$/iu.test(item.name)
|
|
135
|
+
? [path.relative(root, file).split(path.sep).join("/")]
|
|
136
|
+
: [];
|
|
137
|
+
});
|
|
138
|
+
return visit(directory).sort();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateRepositoryReferences(file, document, text, declared, errors) {
|
|
142
|
+
const references = document.callJobs.flatMap((job) =>
|
|
143
|
+
Object.entries(job.with)
|
|
144
|
+
.filter(
|
|
145
|
+
([key, value]) =>
|
|
146
|
+
/(?:source|handoff)-workflow-id$|(?:expected|candidate)-workflow-file$/u.test(
|
|
147
|
+
key,
|
|
148
|
+
) && value.kind === "string",
|
|
149
|
+
)
|
|
150
|
+
.map(([, value]) => value.value),
|
|
151
|
+
);
|
|
152
|
+
for (const match of text.matchAll(
|
|
153
|
+
/\bgh workflow run ([.a-z0-9-]+\.ya?ml)\b/gu,
|
|
154
|
+
))
|
|
155
|
+
references.push(match[1]);
|
|
156
|
+
for (const name of references) {
|
|
157
|
+
const target = name.startsWith(".github/")
|
|
158
|
+
? name
|
|
159
|
+
: `.github/workflows/${name}`;
|
|
160
|
+
if (!declared.has(target))
|
|
161
|
+
errors.push(`${file}: dangling repository workflow reference ${name}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateWorkflow(root, entry, declared, errors) {
|
|
166
|
+
const file = workflowPath(entry);
|
|
167
|
+
const absolute = path.join(root, file);
|
|
168
|
+
if (!fs.existsSync(absolute) || !fs.lstatSync(absolute).isFile()) return;
|
|
169
|
+
const text = fs.readFileSync(absolute, "utf8");
|
|
170
|
+
const document = parseWorkflowDocument(text);
|
|
171
|
+
if (entry.role === "self")
|
|
172
|
+
validateRepositoryReferences(file, document, text, declared, errors);
|
|
173
|
+
const reusable = document.triggers.includes("workflow_call");
|
|
174
|
+
if ((entry.invocation === "reusable") !== reusable)
|
|
175
|
+
errors.push(`${file}: workflow_call does not match registered invocation`);
|
|
176
|
+
if (
|
|
177
|
+
entry.invocation === "dispatch-service" &&
|
|
178
|
+
!document.triggers.includes("workflow_dispatch")
|
|
179
|
+
)
|
|
180
|
+
errors.push(`${file}: dispatch service lacks workflow_dispatch`);
|
|
181
|
+
if (
|
|
182
|
+
entry.role !== "self" &&
|
|
183
|
+
document.triggers.some(
|
|
184
|
+
(trigger) => !["workflow_call", "workflow_dispatch"].includes(trigger),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
errors.push(
|
|
188
|
+
`${file}: public/component entry has repository event triggers`,
|
|
189
|
+
);
|
|
190
|
+
if (entry.compatibility) {
|
|
191
|
+
const alias = path.join(root, entry.compatibility.path);
|
|
192
|
+
if (fs.existsSync(alias) && fs.readFileSync(alias, "utf8") !== text)
|
|
193
|
+
errors.push(
|
|
194
|
+
`${file}: compatibility implementation drift at ${entry.compatibility.path}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
for (const call of parseYamlUses(text)) {
|
|
198
|
+
const local = call.value.match(/^\.\/(\.github\/workflows\/[^@]+)$/u);
|
|
199
|
+
if (local && !declared.has(local[1]))
|
|
200
|
+
errors.push(
|
|
201
|
+
`${file}:${call.line}: dangling workflow reference ${local[1]}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function validateGateIntegration(root, errors) {
|
|
207
|
+
const read = (file) =>
|
|
208
|
+
fs.existsSync(path.join(root, file))
|
|
209
|
+
? fs.readFileSync(path.join(root, file), "utf8")
|
|
210
|
+
: "";
|
|
211
|
+
const pkg = JSON.parse(read("package.json") || "{}");
|
|
212
|
+
const command = pkg.scripts?.["check:workflows"];
|
|
213
|
+
if (
|
|
214
|
+
command !==
|
|
215
|
+
"node scripts/check-workflow-taxonomy.mjs && bash scripts/check-workflows.sh"
|
|
216
|
+
)
|
|
217
|
+
errors.push(
|
|
218
|
+
"check:workflows must execute taxonomy before actionlint without a bypass",
|
|
219
|
+
);
|
|
220
|
+
if (
|
|
221
|
+
!/(?:^|&&)\s*pnpm run check:workflows\s*(?:&&|$)/u.test(
|
|
222
|
+
pkg.scripts?.check || "",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
errors.push("required check chain does not enforce check:workflows");
|
|
226
|
+
const { parse: parseToml } = createRequire(import.meta.url)("smol-toml");
|
|
227
|
+
const config = parseToml(read(".buildchain/buildchain.toml"));
|
|
228
|
+
if (
|
|
229
|
+
!config.lifecycle?.verify?.commands?.includes(
|
|
230
|
+
"corepack pnpm@11.7.0 run check",
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
errors.push(
|
|
234
|
+
"declared verify lifecycle must execute the full required check",
|
|
235
|
+
);
|
|
236
|
+
const verify = read(".github/workflows/self-build-verify.yml");
|
|
237
|
+
const document = parseWorkflowDocument(verify);
|
|
238
|
+
for (const trigger of ["pull_request", "merge_group", "push"]) {
|
|
239
|
+
if (!document.triggers.some((item) => item.split(":")[0] === trigger))
|
|
240
|
+
errors.push(`required Verify workflow lacks ${trigger}`);
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
!verify.includes("Run declared verify lifecycle") ||
|
|
244
|
+
!verify.includes("--required")
|
|
245
|
+
)
|
|
246
|
+
errors.push("required Verify lifecycle integration is missing");
|
|
247
|
+
const owners = read(".github/CODEOWNERS")
|
|
248
|
+
.split(/\r?\n/u)
|
|
249
|
+
.map((line) => line.trim());
|
|
250
|
+
for (const file of PROTECTED) {
|
|
251
|
+
if (!owners.includes(`/${file} @kungfu-origin`))
|
|
252
|
+
errors.push(`independent review ownership missing for ${file}`);
|
|
253
|
+
}
|
|
254
|
+
if (
|
|
255
|
+
!owners.includes("/.github/workflows/* @kungfu-origin") ||
|
|
256
|
+
!owners.includes("/.github/CODEOWNERS @kungfu-origin")
|
|
257
|
+
)
|
|
258
|
+
errors.push(
|
|
259
|
+
"workflow and CODEOWNERS review ownership must remain explicit",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function checkWorkflowTaxonomy(
|
|
264
|
+
root,
|
|
265
|
+
{ integration = true, documentation = true } = {},
|
|
266
|
+
) {
|
|
267
|
+
const errors = [];
|
|
268
|
+
const policy = readWorkflowTaxonomy(root);
|
|
269
|
+
const entries = validateEntries(policy, errors);
|
|
270
|
+
if (errors.length) return { ok: false, errors };
|
|
271
|
+
const declared = new Set(
|
|
272
|
+
entries.flatMap((entry) => [
|
|
273
|
+
workflowPath(entry),
|
|
274
|
+
...(entry.compatibility ? [entry.compatibility.path] : []),
|
|
275
|
+
]),
|
|
276
|
+
);
|
|
277
|
+
const observed = new Set(discoverWorkflowFiles(root));
|
|
278
|
+
for (const file of observed) {
|
|
279
|
+
if (!declared.has(file)) errors.push(`unregistered workflow: ${file}`);
|
|
280
|
+
if (!fs.lstatSync(path.join(root, file)).isFile())
|
|
281
|
+
errors.push(`workflow must be a regular file: ${file}`);
|
|
282
|
+
}
|
|
283
|
+
for (const file of declared)
|
|
284
|
+
if (!observed.has(file))
|
|
285
|
+
errors.push(`registered workflow missing: ${file}`);
|
|
286
|
+
for (const entry of entries) validateWorkflow(root, entry, declared, errors);
|
|
287
|
+
if (integration) validateGateIntegration(root, errors);
|
|
288
|
+
if (documentation) {
|
|
289
|
+
const file = path.join(root, TAXONOMY_DOC);
|
|
290
|
+
if (
|
|
291
|
+
!fs.existsSync(file) ||
|
|
292
|
+
fs.readFileSync(file, "utf8") !== renderWorkflowCatalog(policy)
|
|
293
|
+
)
|
|
294
|
+
errors.push("generated workflow catalog is stale");
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
ok: errors.length === 0,
|
|
298
|
+
canonicalCount: entries.length,
|
|
299
|
+
compatibilityCount: entries.filter((entry) => entry.compatibility).length,
|
|
300
|
+
fileCount: observed.size,
|
|
301
|
+
errors,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Existing lane/debt baselines name immutable pre-migration identities. Count
|
|
306
|
+
// one logical implementation only after proving every physical alias identical.
|
|
307
|
+
export function projectWorkflowIdentities(root, workflows) {
|
|
308
|
+
const policy = readWorkflowTaxonomy(root);
|
|
309
|
+
if (!policy) return workflows;
|
|
310
|
+
const result = checkWorkflowTaxonomy(root, {
|
|
311
|
+
integration: false,
|
|
312
|
+
documentation: false,
|
|
313
|
+
});
|
|
314
|
+
if (!result.ok) throw new Error(result.errors.join("\n"));
|
|
315
|
+
const byPath = new Map(
|
|
316
|
+
workflows.map((workflow) => [workflow.path, workflow]),
|
|
317
|
+
);
|
|
318
|
+
return policy.entries
|
|
319
|
+
.map((entry) => {
|
|
320
|
+
const workflow = byPath.get(workflowPath(entry));
|
|
321
|
+
if (!workflow)
|
|
322
|
+
throw new Error(`missing canonical workflow: ${workflowPath(entry)}`);
|
|
323
|
+
return {
|
|
324
|
+
...workflow,
|
|
325
|
+
path: entry.migration?.previousPath || workflow.path,
|
|
326
|
+
};
|
|
327
|
+
})
|
|
328
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function writeWorkflowSource(root, relative, text) {
|
|
332
|
+
const policy = readWorkflowTaxonomy(root);
|
|
333
|
+
const entry = policy?.entries.find(
|
|
334
|
+
(item) =>
|
|
335
|
+
workflowPath(item) === relative || item.compatibility?.path === relative,
|
|
336
|
+
);
|
|
337
|
+
const outputs = entry
|
|
338
|
+
? [
|
|
339
|
+
workflowPath(entry),
|
|
340
|
+
...(entry.compatibility ? [entry.compatibility.path] : []),
|
|
341
|
+
]
|
|
342
|
+
: [relative];
|
|
343
|
+
for (const file of outputs) fs.writeFileSync(path.join(root, file), text);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function rewriteRepositoryWorkflowPaths(root, text) {
|
|
347
|
+
for (const entry of readWorkflowTaxonomy(root)?.entries || []) {
|
|
348
|
+
if (
|
|
349
|
+
["v4-adopter-delivery", "v4-stage-capsule-canary"].includes(entry.id) &&
|
|
350
|
+
entry.compatibility
|
|
351
|
+
) {
|
|
352
|
+
const canonical = workflowPath(entry);
|
|
353
|
+
const legacy = entry.compatibility.path;
|
|
354
|
+
text = text.replaceAll(
|
|
355
|
+
`BUILDCHAIN_INVOKED_WORKFLOW: ${legacy}\n`,
|
|
356
|
+
`BUILDCHAIN_INVOKED_WORKFLOW: \${{ startsWith(job.workflow_ref, 'kungfu-systems/buildchain/${canonical}@') && '${canonical}' || '${legacy}' }}\n`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (entry.role === "self" && entry.migration)
|
|
360
|
+
text = text.replaceAll(entry.migration.previousPath, workflowPath(entry));
|
|
361
|
+
if (entry.id === "build-surface-fixture")
|
|
362
|
+
text = text.replaceAll(
|
|
363
|
+
"build-surface-fixture.yml",
|
|
364
|
+
path.posix.basename(workflowPath(entry)),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return text;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function workflowCompatibilityIdentity(root, relative, text) {
|
|
371
|
+
const entry = readWorkflowTaxonomy(root)?.entries.find(
|
|
372
|
+
(item) => workflowPath(item) === relative && item.compatibility,
|
|
373
|
+
);
|
|
374
|
+
if (!entry) return relative;
|
|
375
|
+
const source = text ?? fs.readFileSync(path.join(root, relative), "utf8");
|
|
376
|
+
if (
|
|
377
|
+
source !==
|
|
378
|
+
fs.readFileSync(path.join(root, entry.compatibility.path), "utf8")
|
|
379
|
+
)
|
|
380
|
+
throw new Error(`${relative}: compatibility implementation drift`);
|
|
381
|
+
return entry.compatibility.path;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function renderWorkflowCatalog(policy) {
|
|
385
|
+
const lines = [
|
|
386
|
+
"---",
|
|
387
|
+
"status: active",
|
|
388
|
+
"period: ongoing",
|
|
389
|
+
"theme: workflow-taxonomy",
|
|
390
|
+
"doc_type: technical-reference",
|
|
391
|
+
"source_level: local-files",
|
|
392
|
+
"confidence: high",
|
|
393
|
+
"sensitivity: public",
|
|
394
|
+
"evidence_grade: B",
|
|
395
|
+
"review_state: unreviewed",
|
|
396
|
+
"last_reviewed: 2026-09-05",
|
|
397
|
+
"ai_provenance:",
|
|
398
|
+
" model_family: GPT-6",
|
|
399
|
+
" product: Codex",
|
|
400
|
+
" generated_at: 2026-09-05",
|
|
401
|
+
" visible_context: Repository workflow source and the approved naming scheme",
|
|
402
|
+
" invisible_context_boundary: No credentials or unpublished provider configuration inspected",
|
|
403
|
+
"---",
|
|
404
|
+
"",
|
|
405
|
+
"# Workflow catalog",
|
|
406
|
+
"",
|
|
407
|
+
"Generated from `architecture/workflow-taxonomy.json`; edit that source, then run",
|
|
408
|
+
"`pnpm run generate:workflows`. Every YAML file, including compatibility paths,",
|
|
409
|
+
"is checked by the existing required workflow check.",
|
|
410
|
+
"",
|
|
411
|
+
"## Naming and ownership",
|
|
412
|
+
"",
|
|
413
|
+
"`public-<build|release|ops>-<purpose>.yml` is a consumer entry;",
|
|
414
|
+
"`.<build|release|ops>-<purpose>.yml` is an advanced component;",
|
|
415
|
+
"`self-<build|release|ops>-<purpose>.yml` is Buildchain's own automation.",
|
|
416
|
+
"Public is a product role, not GitHub access control. Dispatch services are",
|
|
417
|
+
"marked separately from reusable workflows. Keep workflow names, job IDs,",
|
|
418
|
+
"permissions, and trigger semantics stable during path migration.",
|
|
419
|
+
"",
|
|
420
|
+
"## Channel rollout",
|
|
421
|
+
"",
|
|
422
|
+
"Canonical names become callable on a channel only after that channel publishes",
|
|
423
|
+
"them. Existing `@v4`/`@v4-alpha` consumers keep their exact registered legacy",
|
|
424
|
+
"paths until provider readback proves canonical availability. New examples below",
|
|
425
|
+
"describe the target surface, not a claim that both channels already publish it.",
|
|
426
|
+
"Compatibility files are generated byte-identical projections, not independently",
|
|
427
|
+
"editable implementations. They retain full jobs to preserve called-workflow",
|
|
428
|
+
"identity, permission envelopes, and required check names without extra nesting.",
|
|
429
|
+
"",
|
|
430
|
+
"The controller registry records the actual source path separately from a migrated",
|
|
431
|
+
"repository controller contractPath. This preserves the established logical",
|
|
432
|
+
"contract identity while exposing its current execution source. New workflows",
|
|
433
|
+
"do not acquire historical identities. Public controllers retain their real",
|
|
434
|
+
"compatibility paths while those paths remain supported.",
|
|
435
|
+
"",
|
|
436
|
+
"## Adding or changing a workflow",
|
|
437
|
+
"",
|
|
438
|
+
"Register a stable identity, role, category, purpose, owner, lifecycle status,",
|
|
439
|
+
"invocation and concrete summary before creating its derived path. A new entry",
|
|
440
|
+
"must explain its scope in review. New aliases cannot be invented: compatibility",
|
|
441
|
+
"must name an exact migration source with a reason and removal condition.",
|
|
442
|
+
"The policy, generator, checker, tests, required CI chain and CODEOWNERS require",
|
|
443
|
+
"independent `kungfu-origin` review. No wildcard compatibility admission is allowed.",
|
|
444
|
+
"",
|
|
445
|
+
"A role, category or implementation edit must preserve or deliberately update",
|
|
446
|
+
"all related declarations. Run `pnpm run generate:workflows` and",
|
|
447
|
+
"`pnpm run check:workflows`; the full `pnpm run check` remains required.",
|
|
448
|
+
"",
|
|
449
|
+
];
|
|
450
|
+
for (const role of ROLES) {
|
|
451
|
+
lines.push(
|
|
452
|
+
`## ${role}`,
|
|
453
|
+
"",
|
|
454
|
+
"| Canonical workflow | Category | Invocation | Status | Purpose | Compatibility path |",
|
|
455
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
456
|
+
);
|
|
457
|
+
for (const entry of policy.entries
|
|
458
|
+
.filter((item) => item.role === role)
|
|
459
|
+
.sort((a, b) => workflowPath(a).localeCompare(workflowPath(b)))) {
|
|
460
|
+
const name = path.posix.basename(workflowPath(entry));
|
|
461
|
+
lines.push(
|
|
462
|
+
`| [${name}](../${workflowPath(entry)}) | ${entry.category} | ${entry.invocation} | ${entry.status} | ${entry.summary.replaceAll("|", "\\|")} | ${entry.compatibility ? `\`${path.posix.basename(entry.compatibility.path)}\`` : "—"} |`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
lines.push("");
|
|
466
|
+
}
|
|
467
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
468
|
+
}
|