Files
rea/scripts/verify-repository-skill.mjs
T
Kaoru0822 89268f4a45 fix(ci): run the lockfile-pinned Skills CLI in package verification (#6235)
* fix(ci): run the lockfile-pinned Skills CLI in package verification

The repository skill stage fetched skills@1.7.2 and its unlocked
dependencies from the npm registry on every run, taking 5-120 s and
intermittently exceeding the 120 s stage timeout on main and PRs.
Pin skills as a devDependency and run its locked bin directly.

Fixes #6234

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): lock the Skills CLI outside the root dependency tree

skills 1.6+ requires Node >=22.20.0, so pinning it as a root
devDependency made `npm ci` fail under engine-strict on the supported
Node 22.19.x. Keep the root install unchanged and give the verifier its
own package.json and lockfile, installed with `npm ci --prefer-offline`
in the verification workspace. The lockfile still removes all registry
metadata resolution and pins every transitive dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:19:45 -07:00

116 lines
3.8 KiB
JavaScript

import { cp, mkdir, readFile, readdir } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import { exec, execNpm, pathExists } from "./lib/verify-package-core.mjs";
/** Install the separately locked Skills CLI so verification resolves no registry metadata. */
const lockedSkillsCli = async (root, workspace) => {
const tool = join(workspace, "skills-cli");
await mkdir(tool, { recursive: true });
for (const file of ["package.json", "package-lock.json"])
await cp(join(root, "scripts/verify-tools/skills", file), join(tool, file));
await execNpm(
["ci", "--prefer-offline", "--ignore-scripts", "--no-audit", "--no-fund"],
{ cwd: tool, timeout: 120_000 },
);
const installed = join(tool, "node_modules/skills");
const manifest = JSON.parse(
await readFile(join(installed, "package.json"), "utf8"),
);
return {
version: manifest.version,
entry: join(installed, manifest.bin.skills),
};
};
/** Exercise Skills' default root discovery without ignored build outputs. */
export async function verifyRepositorySkill({ root, workspace }) {
const snapshot = join(workspace, "repository");
const consumer = join(workspace, "skill-consumer");
const home = join(workspace, "skill-consumer-home");
const tracked = (
await exec(
"git",
["ls-files", "--cached", "--format=%(objectmode) %(path)", "-z"],
{ cwd: root },
)
).stdout
.split("\0")
.filter(Boolean);
for (const entry of tracked) {
const mode = entry.slice(0, 6);
const path = entry.slice(7);
const destination = join(snapshot, path);
// A GitHub clone without submodule initialization has only these directories.
if (mode === "160000") {
await mkdir(destination, { recursive: true });
continue;
}
await mkdir(dirname(destination), { recursive: true });
await cp(join(root, path), destination, { verbatimSymlinks: true });
}
if (await pathExists(join(snapshot, "skills")))
throw new Error("repository skill check must run without generated skills");
await mkdir(consumer, { recursive: true });
await mkdir(home, { recursive: true });
const { version: skillsVersion, entry } = await lockedSkillsCli(
root,
workspace,
);
await exec(
process.execPath,
[
entry,
"add",
snapshot,
"--skill",
"reverse-engineer-anything",
"--agent",
"codex",
"--yes",
],
{
cwd: consumer,
env: {
...process.env,
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
CODEX_HOME: join(home, ".codex"),
DO_NOT_TRACK: "1",
SKILLS_NO_TELEMETRY: "1",
CI: "1",
},
timeout: 120_000,
},
);
const authoredRoot = join(
snapshot,
".agents/skills/reverse-engineer-anything",
);
const installedSkills = join(consumer, ".agents/skills");
const installedRoot = join(installedSkills, "reverse-engineer-anything");
const files = await filePaths(authoredRoot);
if (
JSON.stringify(await readdir(installedSkills)) !==
JSON.stringify(["reverse-engineer-anything"]) ||
JSON.stringify(await filePaths(installedRoot)) !== JSON.stringify(files)
)
throw new Error(
"Skills did not install exactly the selected public bundle",
);
for (const path of files) {
const authored = await readFile(join(authoredRoot, path));
if (!authored.equals(await readFile(join(installedRoot, path))))
throw new Error(`Skills changed authored skill content: ${path}`);
}
return { skillsVersion, files };
}
async function filePaths(root) {
return (await readdir(root, { recursive: true, withFileTypes: true }))
.filter((entry) => entry.isFile())
.map((entry) => relative(root, join(entry.parentPath, entry.name)))
.sort();
}