From 4762010da6da4d70631df6ad63fcaa56bdbfbdcd Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 10 Jun 2026 16:58:45 +0300 Subject: [PATCH 1/3] fix(skills): scan staged payload with skillspector --- .github/workflows/skill-release.yml | 8 +++-- scripts/ci/simulate_skill_tag_release.mjs | 2 +- scripts/test-skill-release-workflow.mjs | 30 ++++++++++++++++++ scripts/test-skill-tag-release-simulation.mjs | 31 ++++++++++++++++++- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/.github/workflows/skill-release.yml b/.github/workflows/skill-release.yml index 521790c8..99076319 100644 --- a/.github/workflows/skill-release.yml +++ b/.github/workflows/skill-release.yml @@ -710,7 +710,7 @@ jobs: --source-ref "${HEAD_SHA}" # --- Generate SkillSpector report --- - if ! generate_skillspector_report "${skill_dir}" "${out_assets}/skillspector-report.md"; then + if ! generate_skillspector_report "${inner_dir}" "${out_assets}/skillspector-report.md"; then failures=$((failures + 1)) rm -rf "${staging_dir}" echo "::endgroup::" @@ -1221,7 +1221,7 @@ jobs: --source-ref "$TAG" # --- Generate SkillSpector report --- - generate_skillspector_report "$SKILL_PATH" "release-assets/skillspector-report.md" + generate_skillspector_report "$INNER_DIR" "release-assets/skillspector-report.md" test -s release-assets/skill-card.md test -s release-assets/permissions.json @@ -1416,6 +1416,10 @@ jobs: ${{ steps.install.outputs.quick_install }} + ### SkillSpector Security Report + + Review the generated release-payload scan: [skillspector-report.md](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/skillspector-report.md) + ### Verification `checksums.json` is cryptographically signed (`checksums.sig`) using the ClawSec CI signing key. diff --git a/scripts/ci/simulate_skill_tag_release.mjs b/scripts/ci/simulate_skill_tag_release.mjs index 00a88c12..dad8d5d3 100644 --- a/scripts/ci/simulate_skill_tag_release.mjs +++ b/scripts/ci/simulate_skill_tag_release.mjs @@ -469,7 +469,7 @@ async function main() { await runSkillSpector({ skillspectorBin: args.skillspectorBin, - skillDir: tempSkillDir, + skillDir: innerDir, reportPath: path.join(releaseAssetsDir, "skillspector-report.md"), }); diff --git a/scripts/test-skill-release-workflow.mjs b/scripts/test-skill-release-workflow.mjs index 5670374a..e4fa3ff4 100644 --- a/scripts/test-skill-release-workflow.mjs +++ b/scripts/test-skill-release-workflow.mjs @@ -88,6 +88,36 @@ 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\/\$\{\{ github\.repository \}\}\/releases\/download\/\$\{\{ github\.ref_name \}\}\/skillspector-report\.md\)/, + 'GitHub release notes must display a direct SkillSpector report link', +); + +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/, diff --git a/scripts/test-skill-tag-release-simulation.mjs b/scripts/test-skill-tag-release-simulation.mjs index 0d12166a..fb10a75a 100644 --- a/scripts/test-skill-tag-release-simulation.mjs +++ b/scripts/test-skill-tag-release-simulation.mjs @@ -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]) { From fb5e5dd12c0697ae0c196ccec56cec9086b91ad9 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 10 Jun 2026 17:03:44 +0300 Subject: [PATCH 2/3] fix(skills): embed skillspector report in releases --- .github/workflows/skill-release.yml | 14 +++++++++++++- scripts/test-skill-release-workflow.mjs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/skill-release.yml b/.github/workflows/skill-release.yml index 99076319..5a7dfc2a 100644 --- a/.github/workflows/skill-release.yml +++ b/.github/workflows/skill-release.yml @@ -1403,6 +1403,16 @@ jobs: echo "INSTALL_EOF" } >> "$GITHUB_OUTPUT" + - name: Prepare SkillSpector report for release notes + id: skillspector_report + run: | + set -euo pipefail + { + echo "body<> "$GITHUB_OUTPUT" + - name: Create GitHub Release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: @@ -1418,7 +1428,9 @@ jobs: ### SkillSpector Security Report - Review the generated release-payload scan: [skillspector-report.md](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/skillspector-report.md) + ${{ steps.skillspector_report.outputs.body }} + + Download the generated release-payload scan: [skillspector-report.md](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/skillspector-report.md) ### Verification diff --git a/scripts/test-skill-release-workflow.mjs b/scripts/test-skill-release-workflow.mjs index e4fa3ff4..7f917796 100644 --- a/scripts/test-skill-release-workflow.mjs +++ b/scripts/test-skill-release-workflow.mjs @@ -91,7 +91,19 @@ assert.match( assert.match( workflow, /### SkillSpector Security Report[\s\S]*\[skillspector-report\.md\]\(https:\/\/github\.com\/\$\{\{ github\.repository \}\}\/releases\/download\/\$\{\{ github\.ref_name \}\}\/skillspector-report\.md\)/, - 'GitHub release notes must display a direct SkillSpector report link', + 'GitHub release notes must include a direct SkillSpector report link', +); + +assert.match( + workflow, + /id: skillspector_report[\s\S]*cat release-assets\/skillspector-report\.md[\s\S]*>> "\$GITHUB_OUTPUT"/, + 'GitHub release notes must load the generated SkillSpector report content', +); + +assert.match( + workflow, + /### SkillSpector Security Report[\s\S]*\$\{\{ steps\.skillspector_report\.outputs\.body \}\}/, + 'GitHub release notes must display the generated SkillSpector report content inline', ); assert.match( From 220b60765f36121fa04d1aca424ad37d0cd867eb Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 10 Jun 2026 17:16:13 +0300 Subject: [PATCH 3/3] fix(skills): use body path for release notes --- .github/workflows/skill-release.yml | 87 ++++++++++++++----------- scripts/test-skill-release-workflow.mjs | 16 +++-- 2 files changed, 59 insertions(+), 44 deletions(-) diff --git a/.github/workflows/skill-release.yml b/.github/workflows/skill-release.yml index 5a7dfc2a..d9d12242 100644 --- a/.github/workflows/skill-release.yml +++ b/.github/workflows/skill-release.yml @@ -1403,15 +1403,55 @@ jobs: echo "INSTALL_EOF" } >> "$GITHUB_OUTPUT" - - name: Prepare SkillSpector report for release notes - id: skillspector_report + - name: Prepare GitHub release body + env: + SKILL_NAME: ${{ steps.parse.outputs.skill_name }} + VERSION: ${{ steps.parse.outputs.version }} + CHANGELOG: ${{ steps.changelog.outputs.changelog }} + QUICK_INSTALL: ${{ steps.install.outputs.quick_install }} + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} run: | set -euo pipefail - { - echo "body<> "$GITHUB_OUTPUT" + node -e ' + const { readFileSync, writeFileSync } = require("node:fs"); + const bodyPath = `${process.env.RUNNER_TEMP}/skill-release-body.md`; + const report = readFileSync("release-assets/skillspector-report.md", "utf8").trimEnd(); + const body = [ + `## ${process.env.SKILL_NAME} ${process.env.VERSION}`, + "", + process.env.CHANGELOG || "", + "", + process.env.QUICK_INSTALL || "", + "", + "### SkillSpector Security Report", + "", + report, + "", + `Download the generated release-payload scan: [skillspector-report.md](https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/skillspector-report.md)`, + "", + "### Verification", + "", + "`checksums.json` is cryptographically signed (`checksums.sig`) using the ClawSec CI signing key.", + "Verify the signature first, then trust hashes from `checksums.json`:", + "```bash", + `curl -sLO https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/checksums.json`, + `curl -sLO https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/checksums.sig`, + `curl -sLO https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/signing-public.pem`, + "openssl base64 -d -A -in checksums.sig -out checksums.sig.bin", + "openssl pkeyutl -verify -rawin -pubin -inkey signing-public.pem -sigfile checksums.sig.bin -in checksums.json", + "```", + "", + "### Files", + "", + "See `checksums.json` for the complete file manifest with SHA256 hashes.", + "The zip archive preserves the full directory structure of the skill.", + "", + "---", + "*Released by ClawSec skill distribution pipeline*", + ].join("\n"); + writeFileSync(bodyPath, `${body}\n`); + ' - name: Create GitHub Release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 @@ -1419,38 +1459,7 @@ jobs: name: "${{ steps.parse.outputs.skill_name }} ${{ steps.parse.outputs.version }}" tag_name: ${{ github.ref_name }} files: release-assets/* - body: | - ## ${{ steps.parse.outputs.skill_name }} ${{ steps.parse.outputs.version }} - - ${{ steps.changelog.outputs.changelog }} - - ${{ steps.install.outputs.quick_install }} - - ### SkillSpector Security Report - - ${{ steps.skillspector_report.outputs.body }} - - Download the generated release-payload scan: [skillspector-report.md](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/skillspector-report.md) - - ### Verification - - `checksums.json` is cryptographically signed (`checksums.sig`) using the ClawSec CI signing key. - Verify the signature first, then trust hashes from `checksums.json`: - ```bash - curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.json - curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.sig - curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/signing-public.pem - openssl base64 -d -A -in checksums.sig -out checksums.sig.bin - openssl pkeyutl -verify -rawin -pubin -inkey signing-public.pem -sigfile checksums.sig.bin -in checksums.json - ``` - - ### Files - - See `checksums.json` for the complete file manifest with SHA256 hashes. - The zip archive preserves the full directory structure of the skill. - - --- - *Released by ClawSec skill distribution pipeline* + body_path: ${{ runner.temp }}/skill-release-body.md draft: false prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} env: diff --git a/scripts/test-skill-release-workflow.mjs b/scripts/test-skill-release-workflow.mjs index 7f917796..87dfd2ba 100644 --- a/scripts/test-skill-release-workflow.mjs +++ b/scripts/test-skill-release-workflow.mjs @@ -90,20 +90,26 @@ assert.match( assert.match( workflow, - /### SkillSpector Security Report[\s\S]*\[skillspector-report\.md\]\(https:\/\/github\.com\/\$\{\{ github\.repository \}\}\/releases\/download\/\$\{\{ github\.ref_name \}\}\/skillspector-report\.md\)/, + /### 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, - /id: skillspector_report[\s\S]*cat release-assets\/skillspector-report\.md[\s\S]*>> "\$GITHUB_OUTPUT"/, - 'GitHub release notes must load the generated SkillSpector report content', + /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, - /### SkillSpector Security Report[\s\S]*\$\{\{ steps\.skillspector_report\.outputs\.body \}\}/, - 'GitHub release notes must display the generated SkillSpector report content inline', + /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(