fix(skills): scan staged payload with SkillSpector (#264)

* fix(skills): scan staged payload with skillspector

* fix(skills): embed skillspector report in releases

* fix(skills): use body path for release notes
This commit is contained in:
davida-ps
2026-06-10 17:18:54 +03:00
committed by GitHub
parent 59d54ed778
commit 1b676fd42c
4 changed files with 132 additions and 30 deletions
+1 -1
View File
@@ -469,7 +469,7 @@ async function main() {
await runSkillSpector({
skillspectorBin: args.skillspectorBin,
skillDir: tempSkillDir,
skillDir: innerDir,
reportPath: path.join(releaseAssetsDir, "skillspector-report.md"),
});
+48
View File
@@ -88,6 +88,54 @@ assert.match(
'Skill release workflow must generate a SkillSpector report for each released skill',
);
assert.match(
workflow,
/### SkillSpector Security Report[\s\S]*\[skillspector-report\.md\]\(https:\/\/github\.com\/\$\{process\.env\.REPO\}\/releases\/download\/\$\{process\.env\.TAG\}\/skillspector-report\.md\)/,
'GitHub release notes must include a direct SkillSpector report link',
);
assert.match(
workflow,
/readFileSync\("release-assets\/skillspector-report\.md", "utf8"\)/,
'GitHub release notes must load the generated SkillSpector report content into the release body file',
);
assert.match(
workflow,
/body_path: \$\{\{ runner\.temp \}\}\/skill-release-body\.md/,
'GitHub release creation must use body_path for the generated release body file',
);
assert.doesNotMatch(
workflow,
/SKILLSPECTOR_REPORT_EOF|\$\{\{ steps\.skillspector_report\.outputs\.body \}\}|cat release-assets\/skillspector-report\.md[\s\S]*>> "\$GITHUB_OUTPUT"/,
'SkillSpector report content must not be sent through GitHub Actions step outputs',
);
assert.match(
workflow,
/generate_skillspector_report "\$\{inner_dir\}" "\$\{out_assets\}\/skillspector-report\.md"/,
'PR dry-run SkillSpector scan must target the staged release payload, not the source skill directory',
);
assert.doesNotMatch(
workflow,
/generate_skillspector_report "\$\{skill_dir\}" "\$\{out_assets\}\/skillspector-report\.md"/,
'PR dry-run SkillSpector scan must not include source-only test directories',
);
assert.match(
workflow,
/generate_skillspector_report "\$INNER_DIR" "release-assets\/skillspector-report\.md"/,
'Tag release SkillSpector scan must target the staged release payload, not the source skill directory',
);
assert.doesNotMatch(
workflow,
/generate_skillspector_report "\$SKILL_PATH" "release-assets\/skillspector-report\.md"/,
'Tag release SkillSpector scan must not include source-only test directories',
);
assert.match(
workflow,
/Generate release trust packet/,
+30 -1
View File
@@ -94,7 +94,36 @@ try {
await writeFile(
fakeSkillspector,
`#!/usr/bin/env node
import { writeFileSync } from "node:fs";
import { readdirSync, writeFileSync } from "node:fs";
import path from "node:path";
const scanIndex = process.argv.indexOf("scan");
if (scanIndex === -1 || !process.argv[scanIndex + 1]) {
console.error("missing scan target");
process.exit(2);
}
function containsTestDirectory(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const lowerName = entry.name.toLowerCase();
if (lowerName === "test" || lowerName === "tests") {
return true;
}
if (containsTestDirectory(path.join(dir, entry.name))) {
return true;
}
}
return false;
}
const scanTarget = process.argv[scanIndex + 1];
if (containsTestDirectory(scanTarget)) {
console.error("SkillSpector test fixture must scan the staged release payload, not source test directories.");
process.exit(42);
}
const outputIndex = process.argv.indexOf("--output");
if (outputIndex === -1 || !process.argv[outputIndex + 1]) {