chore(aiwg): adopt shared BT6 maintainer workflow (#148)
* docs(policy): allow low-impact contributor courtesy cleanup * chore(aiwg): migrate to shared BT6 maintainer plugin * chore: reconcile delivery validation
This commit is contained in:
1 parent
8f658891f2
commit
afc9dad1b2
89 files changed
+4683
-1512
No files matched your search
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { basename, dirname, join, relative, resolve } from 'node:path';
|
||||
|
||||
const projectRoot = resolve(import.meta.dirname, '..');
|
||||
const lockPath = join(projectRoot, '.aiwg', 'bt6-maintainer.lock.json');
|
||||
const lock = JSON.parse(readFileSync(lockPath, 'utf8'));
|
||||
const checkOnly = process.argv.includes('--check');
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return execFileSync(command, args, {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
function capture(command, args) {
|
||||
return run(command, args, { capture: true });
|
||||
}
|
||||
|
||||
function filesUnder(root, current = root) {
|
||||
const files = [];
|
||||
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
||||
const path = join(current, entry.name);
|
||||
if (entry.isDirectory()) files.push(...filesUnder(root, path));
|
||||
if (entry.isFile()) files.push(relative(root, path));
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
function digest(path) {
|
||||
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
||||
}
|
||||
|
||||
function compareTrees(expectedRoot, actualRoot) {
|
||||
const expectedFiles = filesUnder(expectedRoot);
|
||||
const actualFiles = existsSync(actualRoot) ? filesUnder(actualRoot) : [];
|
||||
const missing = expectedFiles.filter((path) => !actualFiles.includes(path));
|
||||
const extra = actualFiles.filter((path) => !expectedFiles.includes(path));
|
||||
const changed = expectedFiles.filter(
|
||||
(path) => actualFiles.includes(path) && digest(join(expectedRoot, path)) !== digest(join(actualRoot, path)),
|
||||
);
|
||||
return { equal: missing.length === 0 && extra.length === 0 && changed.length === 0, missing, extra, changed };
|
||||
}
|
||||
|
||||
function assertContained(parent, child, label) {
|
||||
const parentPath = resolve(parent);
|
||||
const childPath = resolve(child);
|
||||
if (childPath !== parentPath && !childPath.startsWith(`${parentPath}/`)) {
|
||||
throw new Error(`${label} escapes ${parentPath}: ${childPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const installRef = `${lock.source}@${lock.ref}`;
|
||||
run('aiwg', ['install', installRef, '--refresh']);
|
||||
|
||||
const info = capture('aiwg', ['packages', 'info', lock.registryKey]);
|
||||
const plainInfo = info.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
const cacheMatch = plainInfo.match(/^\s*Cache:\s*(.+)$/m);
|
||||
if (!cacheMatch) throw new Error(`Unable to resolve AIWG package cache for ${lock.registryKey}`);
|
||||
|
||||
const cacheRoot = resolve(cacheMatch[1].trim());
|
||||
const cachedCommit = capture('git', ['-C', cacheRoot, 'rev-parse', 'HEAD']).trim();
|
||||
const cachedTree = capture('git', ['-C', cacheRoot, 'rev-parse', `HEAD:${lock.sourceWrapperPath}`]).trim();
|
||||
if (cachedCommit !== lock.commit) {
|
||||
throw new Error(`Pinned commit mismatch: expected ${lock.commit}, received ${cachedCommit}`);
|
||||
}
|
||||
if (cachedTree !== lock.wrapperTree) {
|
||||
throw new Error(`Pinned wrapper tree mismatch: expected ${lock.wrapperTree}, received ${cachedTree}`);
|
||||
}
|
||||
|
||||
const sourceWrapper = join(cacheRoot, lock.sourceWrapperPath);
|
||||
assertContained(cacheRoot, sourceWrapper, 'source wrapper');
|
||||
const targetWrapper = join(projectRoot, lock.wrapperPath);
|
||||
assertContained(join(projectRoot, '.aiwg', 'plugins'), targetWrapper, 'target wrapper');
|
||||
|
||||
const manifest = JSON.parse(readFileSync(join(sourceWrapper, 'manifest.json'), 'utf8'));
|
||||
if (manifest.id !== lock.package || manifest.type !== 'plugin' || manifest.version !== lock.version) {
|
||||
throw new Error(`Wrapper identity mismatch for ${lock.package}@${lock.version}`);
|
||||
}
|
||||
|
||||
const before = compareTrees(sourceWrapper, targetWrapper);
|
||||
if (checkOnly) {
|
||||
if (!before.equal) {
|
||||
throw new Error(`Vendored wrapper differs: ${JSON.stringify(before)}`);
|
||||
}
|
||||
console.log(`bt6-maintainer ${lock.version} matches ${lock.commit} (${lock.wrapperTree})`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const pluginsRoot = dirname(targetWrapper);
|
||||
const stage = mkdtempSync(join(pluginsRoot, '.bt6-maintainer-stage-'));
|
||||
const stagedWrapper = join(stage, basename(targetWrapper));
|
||||
cpSync(sourceWrapper, stagedWrapper, { recursive: true, errorOnExist: true });
|
||||
|
||||
const backupRoot = join(projectRoot, '.aiwg', 'working', 'bt6-maintainer-sync-backup');
|
||||
rmSync(backupRoot, { recursive: true, force: true });
|
||||
if (existsSync(targetWrapper)) {
|
||||
cpSync(targetWrapper, backupRoot, { recursive: true });
|
||||
rmSync(targetWrapper, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
try {
|
||||
renameSync(stagedWrapper, targetWrapper);
|
||||
rmSync(stage, { recursive: true, force: true });
|
||||
run('aiwg', ['use', lock.package]);
|
||||
run('aiwg', ['use', lock.package, '--provider', 'codex']);
|
||||
run('aiwg', ['index', 'build']);
|
||||
run('aiwg', ['doctor', '--project-local']);
|
||||
const after = compareTrees(sourceWrapper, targetWrapper);
|
||||
if (!after.equal) throw new Error(`Post-deploy wrapper differs: ${JSON.stringify(after)}`);
|
||||
rmSync(backupRoot, { recursive: true, force: true });
|
||||
console.log(`Synchronized ${lock.package} ${lock.version} from ${lock.commit}`);
|
||||
} catch (error) {
|
||||
rmSync(targetWrapper, { recursive: true, force: true });
|
||||
if (existsSync(backupRoot) && statSync(backupRoot).isDirectory()) {
|
||||
renameSync(backupRoot, targetWrapper);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
Reference in new issue
Block a user