mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
1b676fd42c
* fix(skills): scan staged payload with skillspector * fix(skills): embed skillspector report in releases * fix(skills): use body path for release notes
260 lines
8.6 KiB
JavaScript
260 lines
8.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
|
|
const workflowPath = new URL('../.github/workflows/skill-release.yml', import.meta.url);
|
|
const ciWorkflowPath = new URL('../.github/workflows/ci.yml', import.meta.url);
|
|
const workflow = await readFile(workflowPath, 'utf8');
|
|
const ciWorkflow = await readFile(ciWorkflowPath, 'utf8');
|
|
|
|
assert.match(
|
|
workflow,
|
|
/pull_request:[\s\S]*paths:[\s\S]*- 'skills\/\*\*'/,
|
|
'Skill release workflow must run when any skill package file changes',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/pull_request:[\s\S]*paths:[\s\S]*- '\.github\/workflows\/skill-release\.yml'[\s\S]*- 'scripts\/ci\/\*\*'/,
|
|
'Skill release workflow must also run when the release pipeline itself changes',
|
|
);
|
|
|
|
assert.ok(
|
|
ciWorkflow.includes(` - name: Skill Release Tooling Tests
|
|
run: |
|
|
set -euo pipefail
|
|
for test_file in scripts/test-skill-*.mjs; do
|
|
node "$test_file"
|
|
done`),
|
|
'CI must run every scripts/test-skill-*.mjs file so new skill release tests are not orphaned',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/git diff --name-only "\$\{BASE_SHA\}\.\.\.\$\{HEAD_SHA\}" --[\s\S]*'skills\/\*\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/test\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/tests\/\*\*'/,
|
|
'Skill release validation must ignore test-only skill changes while inspecting release-relevant skill files',
|
|
);
|
|
|
|
assert.doesNotMatch(
|
|
workflow,
|
|
/No version bump detected for \$\{skill_dir\}; skipping\./,
|
|
'Changed skill directories without a version bump must not be skipped without release-tag validation',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/skill_release_name="\$\(basename "\$\{skill_dir\}"\)"/,
|
|
'Skill release validation must derive the release tag prefix from the skill package directory',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/release_tag="\$\{skill_release_name\}-v\$\{head_json_version\}"/,
|
|
'Skill release validation must use the skill package directory name for release tag checks',
|
|
);
|
|
|
|
assert.doesNotMatch(
|
|
workflow,
|
|
/release_tag="\$\{head_skill_name\}-v\$\{head_json_version\}"/,
|
|
'Skill release validation must not use skill.json name for release tag checks because release tags resolve to skill directories',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/git show-ref --verify --quiet "refs\/tags\/\$\{release_tag\}"/,
|
|
'Skill release validation must check whether the current skill version has already been tagged',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/No version bump detected for \$\{skill_dir\}, but release tag \$\{release_tag\} does not exist; treating \$\{head_json_version\} as unreleased\./,
|
|
'Skill release validation must allow edits to an unchanged version when that release tag does not exist yet',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/::error file=\$\{skill_dir\}::Changed skill package has no version bump and release tag \$\{release_tag\} already exists\./,
|
|
'Skill release validation must still fail unchanged versions after their release tag exists',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/Install SkillSpector/,
|
|
'Skill release workflow must install SkillSpector before publishing release evidence',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/Generate SkillSpector report/,
|
|
'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/,
|
|
'Skill release workflow must generate skill cards, permission summaries, and npx install instructions',
|
|
);
|
|
|
|
for (const artifact of ['skill-card.md', 'permissions.json', 'install.md', 'skillspector-report.md']) {
|
|
assert.match(
|
|
workflow,
|
|
new RegExp(`release-assets/${artifact.replace('.', '\\.')}`),
|
|
`Skill release workflow must publish ${artifact} in release assets`,
|
|
);
|
|
}
|
|
|
|
const escapeRegExp = (literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
for (const artifact of ['skill-card.md', 'permissions.json', 'install.md', 'skillspector-report.md']) {
|
|
assert.match(
|
|
workflow,
|
|
new RegExp(
|
|
String.raw`if ! add_release_asset_checksum "\$\{out_assets\}" "${escapeRegExp(artifact)}"; then` +
|
|
String.raw`[\s\S]*?failures=\$\(\(failures \+ 1\)\)[\s\S]*?continue[\s\S]*?fi`,
|
|
),
|
|
`PR dry-run validation must aggregate and continue when ${artifact} cannot be checksummed`,
|
|
);
|
|
}
|
|
|
|
assert.match(
|
|
workflow,
|
|
/add_release_asset_checksum "skill-card\.md"/,
|
|
'Skill card must be included in the signed checksums manifest',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/add_release_asset_checksum "permissions\.json"/,
|
|
'Permissions summary must be included in the signed checksums manifest',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/add_release_asset_checksum "install\.md"/,
|
|
'npx install/update instructions must be included in the signed checksums manifest',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/add_release_asset_checksum "skillspector-report\.md"/,
|
|
'SkillSpector report must be included in the signed checksums manifest',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/Simulate tag release build/,
|
|
'Skill release workflow must simulate a tag release build during PR validation',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/simulate_skill_tag_release\.mjs/,
|
|
'Skill release workflow must call the tag release simulation script',
|
|
);
|
|
|
|
assert.ok(
|
|
workflow.includes('simulated_version | test("^[0-9]+\\\\.[0-9]+\\\\.[0-9]+(-[a-zA-Z0-9]+)?$")'),
|
|
'Skill release workflow must accept every prerelease version format that release-skill.sh accepts',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/clawhub_slug: \$\{\{ steps\.publishable\.outputs\.clawhub_slug \}\}/,
|
|
'Skill release workflow must expose the resolved ClawHub slug from release-tag outputs',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/CLAWHUB_SLUG=\$\(node scripts\/ci\/resolve_clawhub_slug\.mjs "\$SKILL_PATH"\)/,
|
|
'Skill release workflow must resolve the ClawHub slug from the skill package path',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/cp scripts\/ci\/resolve_clawhub_slug\.mjs "\$RUNNER_TEMP\/resolve_clawhub_slug\.mjs"/,
|
|
'Manual ClawHub republish must preserve the current slug helper before checking out an older release tag',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/CLAWHUB_SLUG=\$\(node "\$RUNNER_TEMP\/resolve_clawhub_slug\.mjs" "\$SKILL_PATH"\)/,
|
|
'Manual ClawHub republish must resolve slugs with the preserved helper against the checked-out tag metadata',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/npx clawhub@latest install \$\{CLAWHUB_SLUG\}/,
|
|
'GitHub release quick install instructions must use the resolved ClawHub slug',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/clawhub inspect "\$CLAWHUB_SLUG" --version "\$VERSION" --json/,
|
|
'Duplicate ClawHub version guard must inspect the resolved ClawHub slug',
|
|
);
|
|
|
|
assert.match(
|
|
workflow,
|
|
/--slug "\$CLAWHUB_SLUG"/,
|
|
'ClawHub publish must use the resolved ClawHub slug',
|
|
);
|
|
|
|
assert.doesNotMatch(
|
|
workflow,
|
|
/clawhub inspect "\$SKILL_NAME" --version "\$VERSION" --json/,
|
|
'Duplicate ClawHub version guard must not inspect the raw skill package name',
|
|
);
|
|
|
|
assert.doesNotMatch(
|
|
workflow,
|
|
/--slug "\$SKILL_NAME"/,
|
|
'ClawHub publish must not use the raw skill package name as the ClawHub slug',
|
|
);
|