build: unify preinstalled skills bundling across dev/package/release and harden SignPath validation (#524)

This commit is contained in:
Felix
2026-03-16 16:55:56 +08:00
committed by GitHub
Unverified
parent f6128ed743
commit 4e3f3c83f6
10 changed files with 143 additions and 36 deletions

View File

@@ -2,7 +2,7 @@
import 'zx/globals';
import { readFileSync, existsSync, mkdirSync, rmSync, cpSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -43,18 +43,61 @@ function createRepoDirName(repo, ref) {
return `${repo.replace(/[\\/]/g, '__')}__${ref.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
}
function toGitPath(inputPath) {
if (process.platform !== 'win32') return inputPath;
// Git on Windows accepts forward slashes and avoids backslash escape quirks.
return inputPath.replace(/\\/g, '/');
}
function normalizeRepoPath(repoPath) {
return repoPath.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
}
function shouldCopySkillFile(srcPath) {
const base = basename(srcPath);
if (base === '.git') return false;
if (base === '.subset.tar') return false;
return true;
}
async function extractArchive(archiveFileName, cwd) {
const prevCwd = $.cwd;
$.cwd = cwd;
try {
try {
await $`tar -xf ${archiveFileName}`;
return;
} catch (tarError) {
if (process.platform === 'win32') {
// Some Windows images expose bsdtar instead of tar.
await $`bsdtar -xf ${archiveFileName}`;
return;
}
throw tarError;
}
} finally {
$.cwd = prevCwd;
}
}
async function fetchSparseRepo(repo, ref, paths, checkoutDir) {
const remote = `https://github.com/${repo}.git`;
mkdirSync(checkoutDir, { recursive: true });
const gitCheckoutDir = toGitPath(checkoutDir);
const archiveFileName = '.subset.tar';
const archivePath = join(checkoutDir, archiveFileName);
const archivePaths = [...new Set(paths.map(normalizeRepoPath))];
await $`git init ${checkoutDir}`;
await $`git -C ${checkoutDir} remote add origin ${remote}`;
await $`git -C ${checkoutDir} sparse-checkout init --cone`;
await $`git -C ${checkoutDir} sparse-checkout set ${paths}`;
await $`git -C ${checkoutDir} fetch --depth 1 origin ${ref}`;
await $`git -C ${checkoutDir} checkout FETCH_HEAD`;
await $`git init ${gitCheckoutDir}`;
await $`git -C ${gitCheckoutDir} remote add origin ${remote}`;
await $`git -C ${gitCheckoutDir} fetch --depth 1 origin ${ref}`;
// Do not checkout working tree on Windows: upstream repos may contain
// Windows-invalid paths. Export only requested directories via git archive.
await $`git -C ${gitCheckoutDir} archive --format=tar --output ${archiveFileName} FETCH_HEAD ${archivePaths}`;
await extractArchive(archiveFileName, checkoutDir);
rmSync(archivePath, { force: true });
const commit = (await $`git -C ${checkoutDir} rev-parse HEAD`).stdout.trim();
const commit = (await $`git -C ${gitCheckoutDir} rev-parse FETCH_HEAD`).stdout.trim();
return commit;
}
@@ -89,7 +132,7 @@ for (const group of groups) {
}
rmSync(targetDir, { recursive: true, force: true });
cpSync(sourceDir, targetDir, { recursive: true, dereference: true });
cpSync(sourceDir, targetDir, { recursive: true, dereference: true, filter: shouldCopySkillFile });
const skillManifest = join(targetDir, 'SKILL.md');
if (!existsSync(skillManifest)) {

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env zx
import 'zx/globals';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const lockPath = join(ROOT, 'build', 'preinstalled-skills', '.preinstalled-lock.json');
const bundleScript = join(ROOT, 'scripts', 'bundle-preinstalled-skills.mjs');
if (process.env.CLAWX_SKIP_PREINSTALLED_SKILLS_PREPARE === '1') {
echo`Skipping preinstalled skills prepare (CLAWX_SKIP_PREINSTALLED_SKILLS_PREPARE=1).`;
process.exit(0);
}
if (existsSync(lockPath)) {
echo`Preinstalled skills bundle already exists, skipping prepare.`;
process.exit(0);
}
echo`Preinstalled skills bundle missing, preparing for dev startup...`;
try {
await $`zx ${bundleScript}`;
} catch (error) {
// Dev startup should remain available even if network-based skill fetching fails.
echo`Warning: failed to prepare preinstalled skills for dev startup: ${error?.message || error}`;
process.exit(0);
}