chore(flake): simplify nix build and add ci validation (#257)

* chore(flake): simplify nix build and add ci validation

- Replace dynamic pnpm-workspace.yaml parsing with hardcoded workspacePaths
  and workspaceNames to reduce format assumptions
- Remove update-pnpm-deps script and kimi-code-pnpm-deps package; use
  lib.fakeHash for standard hash mismatch workflow
- Remove nodejs_latest fallback in nodejsFor, hardcode to nodejs_24
- Add nix-build CI workflow that posts hash-mismatch details to PR comments
- Remove unused Nix installation step from release.yml
- Add workspace maintenance note to AGENTS.md
This commit is contained in:
Haozhe 2026-06-01 11:47:38 +08:00 committed by GitHub
parent ee69d0ac29
commit 817ad1fe9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 492 additions and 490 deletions

View file

@ -0,0 +1,241 @@
#!/usr/bin/env node
/**
* Recursively resolve workspace dependencies starting from apps/kimi-code
* and verify they are all present in flake.nix workspaceNames/workspacePaths.
*
* Exit code 0 if everything is in sync, 1 otherwise.
*/
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { resolve, join } from "node:path";
const ROOT = resolve(import.meta.dirname, "..");
const FLAKE_NIX = join(ROOT, "flake.nix");
const START_PKG = "@moonshot-ai/kimi-code";
/**
* Parse pnpm-workspace.yaml to get workspace directory globs.
*/
function getWorkspaceGlobs() {
const yamlPath = join(ROOT, "pnpm-workspace.yaml");
const content = readFileSync(yamlPath, "utf8");
const lines = content.split("\n");
const globs = [];
let inPackages = false;
for (const line of lines) {
if (line.startsWith("packages:")) {
inPackages = true;
continue;
}
if (inPackages) {
const match = line.match(/^\s+-\s+(.+)$/);
if (match) {
globs.push(match[1]);
} else if (line.trim() !== "" && !line.startsWith(" ")) {
break;
}
}
}
return globs;
}
/**
* Expand globs like "packages/*" into actual directories.
*/
function expandGlobsSafe(globs) {
const dirs = [];
for (const g of globs) {
if (g.endsWith("/*")) {
const base = g.slice(0, -2);
const basePath = join(ROOT, base);
if (!existsSync(basePath)) continue;
for (const entry of readdirSync(basePath, { withFileTypes: true })) {
if (entry.isDirectory()) {
dirs.push(join(base, entry.name));
}
}
} else {
const p = join(ROOT, g);
if (existsSync(p)) {
dirs.push(g);
}
}
}
return dirs;
}
/**
* Build a map of package name -> relative directory for all workspace packages.
*/
function buildWorkspaceMap(dirs) {
const map = new Map();
for (const dir of dirs) {
const pkgPath = join(ROOT, dir, "package.json");
if (!existsSync(pkgPath)) continue;
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
if (pkg.name) {
map.set(pkg.name, dir);
}
}
return map;
}
/**
* Recursively collect all workspace dependencies (transitive closure).
*/
function resolveWorkspaceDeps(workspaceMap, startName) {
const visited = new Set();
const closure = new Set();
function visit(name) {
if (visited.has(name)) return;
visited.add(name);
const dir = workspaceMap.get(name);
if (!dir) return;
const pkgPath = join(ROOT, dir, "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
const depSections = [
pkg.dependencies,
pkg.devDependencies,
pkg.peerDependencies,
];
for (const section of depSections) {
if (!section) continue;
for (const [depName, specifier] of Object.entries(section)) {
if (
typeof specifier === "string" &&
(specifier.includes("workspace") || specifier.startsWith("link:"))
) {
closure.add(depName);
visit(depName);
}
}
}
}
visit(startName);
return closure;
}
/**
* Parse workspaceNames and workspacePaths from flake.nix.
*/
function parseFlakeNix() {
const content = readFileSync(FLAKE_NIX, "utf8");
function extractArray(label) {
const regex = new RegExp(
`${label}\\s*=\\s*\\[(.*?)\\]`,
"s"
);
const match = content.match(regex);
if (!match) {
throw new Error(`Could not find ${label} in flake.nix`);
}
const items = [];
// workspaceNames uses quoted strings, workspacePaths uses bare Nix paths
const itemRegex = label === "workspacePaths" ? /\.\/[^\s\]]+/g : /"([^"]+)"/g;
let m;
if (label === "workspacePaths") {
while ((m = itemRegex.exec(match[1])) !== null) {
items.push(m[0]);
}
} else {
while ((m = itemRegex.exec(match[1])) !== null) {
items.push(m[1]);
}
}
return items;
}
return {
names: extractArray("workspaceNames"),
paths: extractArray("workspacePaths"),
};
}
function main() {
const globs = getWorkspaceGlobs();
const dirs = expandGlobsSafe(globs);
const workspaceMap = buildWorkspaceMap(dirs);
if (!workspaceMap.has(START_PKG)) {
console.error(`Start package ${START_PKG} not found in workspace.`);
process.exit(1);
}
const closure = resolveWorkspaceDeps(workspaceMap, START_PKG);
/** @type {string[]} */
const closureNames = [...closure].sort((a, b) => a.localeCompare(b));
const flake = parseFlakeNix();
const flakeNameSet = new Set(flake.names);
const flakePathSet = new Set(flake.paths);
const missingNames = closureNames.filter((n) => !flakeNameSet.has(n));
/** @type {Array<{name: string, path: string}>} */
const missingPaths = [];
for (const name of closureNames) {
const dir = workspaceMap.get(name);
if (dir && !flakePathSet.has(`./${dir}`)) {
missingPaths.push({ name, path: `./${dir}` });
}
}
// Also check that the start package itself is in flake.nix
if (!flakeNameSet.has(START_PKG)) {
missingNames.unshift(START_PKG);
}
const startDir = workspaceMap.get(START_PKG);
if (startDir && !flakePathSet.has(`./${startDir}`)) {
missingPaths.unshift({ name: START_PKG, path: `./${startDir}` });
}
const ok = missingNames.length === 0 && missingPaths.length === 0;
if (!ok) {
console.error("❌ flake.nix workspace lists are out of sync.\n");
if (missingNames.length > 0) {
console.error(
"The following workspace packages are missing from flake.nix workspaceNames:"
);
for (const n of missingNames) {
console.error(` - ${n}`);
}
console.error("");
}
if (missingPaths.length > 0) {
console.error(
"The following workspace paths are missing from flake.nix workspacePaths:"
);
for (const { name, path } of missingPaths) {
console.error(` - ${path} (${name})`);
}
console.error("");
}
console.error(
"Please add the missing entries to both workspaceNames and workspacePaths in flake.nix."
);
console.error(
`\nExpected workspaceNames (${flake.names.length + missingNames.length} total):`
);
const expectedNames = new Set([...flake.names, ...missingNames.map((m) => m)]);
for (const n of [...expectedNames].sort((a, b) => a.localeCompare(b))) {
console.error(` ${n}`);
}
process.exit(1);
}
console.log(
`✅ All ${closureNames.length} recursive workspace dependencies are present in flake.nix.`
);
}
main();