mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-15 06:21:21 +03:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f51e53cdd | |||
| d8dec965a8 | |||
| 9fd3059271 | |||
| 1b676fd42c |
@@ -53,9 +53,21 @@ jobs:
|
||||
|
||||
- name: Collect traffic
|
||||
env:
|
||||
GH_TRAFFIC_TOKEN: ${{ secrets.TRAFFIC_ARCHIVE_TOKEN || github.token }}
|
||||
# Traffic endpoints reject the Actions GITHUB_TOKEN ("Resource not
|
||||
# accessible by integration") — a PAT from a user with push access
|
||||
# is required: classic with repo scope, or fine-grained with read
|
||||
# access to Administration on this repository.
|
||||
GH_TRAFFIC_TOKEN: ${{ secrets.TRAFFIC_ARCHIVE_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: node scripts/archive-github-traffic.mjs --archive-dir "${TRAFFIC_ARCHIVE_DIR}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${GH_TRAFFIC_TOKEN}" ]; then
|
||||
echo "::error::No traffic-capable token configured. Set the TRAFFIC_ARCHIVE_TOKEN secret to a PAT with push access (classic: repo scope; fine-grained: Administration read)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node scripts/archive-github-traffic.mjs --archive-dir "${TRAFFIC_ARCHIVE_DIR}"
|
||||
|
||||
- name: Commit archive
|
||||
run: |
|
||||
|
||||
@@ -90,7 +90,15 @@ jobs:
|
||||
'skills/*/**' \
|
||||
':(exclude)skills/*/test/**' \
|
||||
':(exclude)skills/*/tests/**' \
|
||||
| awk -F/ 'NF >= 3 {print $1 "/" $2}' \
|
||||
| awk -F/ '
|
||||
NF >= 3 {
|
||||
path = tolower($0)
|
||||
name = tolower($NF)
|
||||
if (path ~ /(^|\/)(__tests__|test|tests)\//) next
|
||||
if (name ~ /^(test|spec)[_-]/ || name ~ /\.(test|spec)\./) next
|
||||
print $1 "/" $2
|
||||
}
|
||||
' \
|
||||
| sort -u > "${touched_skills_file}"
|
||||
|
||||
if [ ! -s "${touched_skills_file}" ]; then
|
||||
@@ -400,12 +408,15 @@ jobs:
|
||||
}
|
||||
|
||||
touched_skills_file="$(mktemp)"
|
||||
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- 'skills/*/skill.json' 'skills/*/SKILL.md' \
|
||||
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- \
|
||||
'skills/*/**' \
|
||||
':(exclude)skills/*/test/**' \
|
||||
':(exclude)skills/*/tests/**' \
|
||||
| awk -F/ 'NF >= 3 {print $1 "/" $2}' \
|
||||
| sort -u > "${touched_skills_file}"
|
||||
|
||||
if [ ! -s "${touched_skills_file}" ]; then
|
||||
echo "No skill metadata files changed in this PR."
|
||||
echo "No release-relevant skill package files changed in this PR."
|
||||
rm -f "${touched_skills_file}"
|
||||
exit 0
|
||||
fi
|
||||
@@ -431,7 +442,8 @@ jobs:
|
||||
|
||||
is_test_release_path() {
|
||||
local lower="${1,,}"
|
||||
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == */test/* || "$lower" == */tests/* ]]
|
||||
local name="${lower##*/}"
|
||||
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == __tests__/* || "$lower" == */test/* || "$lower" == */tests/* || "$lower" == */__tests__/* || "$name" == test_* || "$name" == test-* || "$name" == spec_* || "$name" == spec-* || "$name" == *.test.* || "$name" == *.spec.* ]]
|
||||
}
|
||||
|
||||
generate_skillspector_report() {
|
||||
@@ -526,11 +538,6 @@ jobs:
|
||||
md_version_changed=true
|
||||
fi
|
||||
|
||||
if [ "${json_version_changed}" != "true" ] && [ "${md_version_changed}" != "true" ]; then
|
||||
echo "No version bump detected for ${skill_dir}; skipping dry-run."
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${head_json_version}" ] || [ -z "${head_md_version}" ] || [ "${head_json_version}" != "${head_md_version}" ]; then
|
||||
echo "::error file=${skill_dir}::Version metadata is invalid for dry-run. Ensure validate-pr-version-sync passes."
|
||||
failures=$((failures + 1))
|
||||
@@ -619,9 +626,9 @@ jobs:
|
||||
# --- Create zip preserving directory structure ---
|
||||
zip_name="${skill_name}-v${version}.zip"
|
||||
(cd "${staging_dir}" && zip -qr "${OLDPWD}/${out_assets}/${zip_name}" .)
|
||||
if unzip -Z1 "${out_assets}/${zip_name}" | grep -Eiq '(^|/)(test|tests)/'; then
|
||||
if unzip -Z1 "${out_assets}/${zip_name}" | grep -Eiq '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.'; then
|
||||
echo "::error::Dry-run release archive contains test-only files: ${zip_name}"
|
||||
unzip -Z1 "${out_assets}/${zip_name}" | grep -Ei '(^|/)(test|tests)/' || true
|
||||
unzip -Z1 "${out_assets}/${zip_name}" | grep -Ei '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.' || true
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
@@ -710,7 +717,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::"
|
||||
@@ -777,12 +784,177 @@ jobs:
|
||||
fi
|
||||
|
||||
if [ "${dry_run_count}" -eq 0 ]; then
|
||||
echo "No version bumps detected in changed skill metadata files."
|
||||
echo "No changed skill directories required dry-run assets."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Release dry-run completed successfully for ${dry_run_count} changed skill(s)."
|
||||
|
||||
- name: Prepare SkillSpector PR report artifact
|
||||
if: always()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf dist/skillspector-pr-reports
|
||||
mkdir -p dist/dry-run dist/skillspector-pr-reports
|
||||
|
||||
found_reports=false
|
||||
while IFS= read -r report_path; do
|
||||
tag="${report_path#dist/dry-run/}"
|
||||
tag="${tag%%/*}"
|
||||
mkdir -p "dist/skillspector-pr-reports/${tag}"
|
||||
cp "${report_path}" "dist/skillspector-pr-reports/${tag}/skillspector-report.md"
|
||||
found_reports=true
|
||||
done < <(find dist/dry-run -path '*/release-assets/skillspector-report.md' -type f | sort)
|
||||
|
||||
if [ "${found_reports}" != "true" ]; then
|
||||
printf 'No SkillSpector reports were generated for this pull request.\n' > dist/skillspector-pr-reports/NO_SKILLSPECTOR_REPORTS.txt
|
||||
fi
|
||||
|
||||
- name: Upload SkillSpector PR reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: skillspector-pr-reports
|
||||
path: dist/skillspector-pr-reports
|
||||
retention-days: 14
|
||||
|
||||
comment-skillspector-report:
|
||||
if: always() && github.event_name == 'pull_request' && needs.release.result != 'cancelled'
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Download SkillSpector reports
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: skillspector-pr-reports
|
||||
path: skillspector-pr-reports
|
||||
|
||||
- name: Comment SkillSpector reports
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = "skillspector-pr-reports";
|
||||
const maxCommentLength = 65000;
|
||||
|
||||
async function findReports(dir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const reports = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
reports.push(...await findReports(fullPath));
|
||||
} else if (entry.isFile() && entry.name === "skillspector-report.md") {
|
||||
reports.push(fullPath);
|
||||
}
|
||||
}
|
||||
return reports;
|
||||
}
|
||||
|
||||
function tagFromReportPath(reportPath) {
|
||||
const parts = reportPath.split(path.sep);
|
||||
const releaseAssetsIndex = parts.lastIndexOf("release-assets");
|
||||
if (releaseAssetsIndex > 0) {
|
||||
return parts[releaseAssetsIndex - 1];
|
||||
}
|
||||
return path.basename(path.dirname(reportPath));
|
||||
}
|
||||
|
||||
function sanitizeReportForComment(report) {
|
||||
const omittedBlock = "_[code block omitted from PR comment; download the workflow artifact for raw details]_";
|
||||
return report
|
||||
.replace(/```[\s\S]*?```/g, omittedBlock)
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => !/^\s{4,}\S/.test(line))
|
||||
.join("\n")
|
||||
.replace(/`[^`\n]*`/g, "`[inline snippet omitted]`")
|
||||
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[redacted-email]")
|
||||
.replace(/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, "[redacted-aws-key]")
|
||||
.replace(/\b(?:ghp|gho|ghu|ghs|ghr|github_pat|glpat|xox[baprs]?|sk|pk)_[A-Za-z0-9_=-]{12,}\b/gi, "[redacted-token]")
|
||||
.replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[redacted-secret-like-value]")
|
||||
.trimEnd();
|
||||
}
|
||||
|
||||
function buildComment({ tag, report }) {
|
||||
const marker = `<!-- clawsec-skillspector-report:${tag} -->`;
|
||||
const sanitizedReport = sanitizeReportForComment(report);
|
||||
const footer = [
|
||||
"_Generated by the Skill Release dry-run for `" + tag + "`._",
|
||||
"_Raw snippets, code blocks, inline code, emails, and token-like values are omitted from this PR comment._",
|
||||
"_Download the `skillspector-pr-reports` workflow artifact for the full report._",
|
||||
].join("\n");
|
||||
let body = `${marker}\n${sanitizedReport}\n\n${footer}`;
|
||||
|
||||
if (body.length <= maxCommentLength) {
|
||||
return body;
|
||||
}
|
||||
|
||||
const truncatedFooter = [
|
||||
"_Report truncated because it exceeds GitHub's comment size limit._",
|
||||
"_Download the `skillspector-pr-reports` workflow artifact for the full report._",
|
||||
footer,
|
||||
].join("\n");
|
||||
const budget = maxCommentLength - marker.length - truncatedFooter.length - 8;
|
||||
return `${marker}\n${sanitizedReport.slice(0, Math.max(0, budget)).trimEnd()}\n\n${truncatedFooter}`;
|
||||
}
|
||||
|
||||
const reports = await findReports(root);
|
||||
if (reports.length === 0) {
|
||||
core.info("No SkillSpector reports found; nothing to comment.");
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
for (const reportPath of reports.sort()) {
|
||||
const tag = tagFromReportPath(reportPath);
|
||||
const report = await fs.readFile(reportPath, "utf8");
|
||||
const marker = `<!-- clawsec-skillspector-report:${tag} -->`;
|
||||
const body = buildComment({ tag, report });
|
||||
const existing = comments.find((comment) => comment.body?.includes(marker));
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
core.info(`Updated SkillSpector PR comment for ${tag}.`);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
core.info(`Created SkillSpector PR comment for ${tag}.`);
|
||||
}
|
||||
}
|
||||
|
||||
simulate-tag-release-build:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: validate-pr-version-sync
|
||||
@@ -1065,7 +1237,8 @@ jobs:
|
||||
|
||||
is_test_release_path() {
|
||||
local lower="${1,,}"
|
||||
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == */test/* || "$lower" == */tests/* ]]
|
||||
local name="${lower##*/}"
|
||||
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == __tests__/* || "$lower" == */test/* || "$lower" == */tests/* || "$lower" == */__tests__/* || "$name" == test_* || "$name" == test-* || "$name" == spec_* || "$name" == spec-* || "$name" == *.test.* || "$name" == *.spec.* ]]
|
||||
}
|
||||
|
||||
generate_skillspector_report() {
|
||||
@@ -1146,9 +1319,9 @@ jobs:
|
||||
# --- Create zip preserving directory structure ---
|
||||
ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip"
|
||||
(cd "$STAGING_DIR" && zip -qr "$OLDPWD/release-assets/$ZIP_NAME" .)
|
||||
if unzip -Z1 "release-assets/$ZIP_NAME" | grep -Eiq '(^|/)(test|tests)/'; then
|
||||
if unzip -Z1 "release-assets/$ZIP_NAME" | grep -Eiq '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.'; then
|
||||
echo "::error::Release archive contains test-only files: $ZIP_NAME"
|
||||
unzip -Z1 "release-assets/$ZIP_NAME" | grep -Ei '(^|/)(test|tests)/' || true
|
||||
unzip -Z1 "release-assets/$ZIP_NAME" | grep -Ei '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.' || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1221,7 +1394,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
|
||||
@@ -1403,38 +1576,61 @@ jobs:
|
||||
echo "INSTALL_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- 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
|
||||
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 || "",
|
||||
"",
|
||||
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
|
||||
with:
|
||||
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 }}
|
||||
|
||||
### 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:
|
||||
|
||||
+1172
-1515
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
agiAAFvzM1vNHxH2+bGtyeKqFScLWJHnNreBcPpTODUqD0xqFi0cnyP/ZaZX+Rsw1Y9uZ7pGdFdA93pD4lh2BQ==
|
||||
jPrlTYwicRwoQgTs5Rk3Y3g6Lz78jNRs9ZNf0R09M4jkJokZENxfvhvHphI9MH4u+7wv0sFZ+yZbQtJ42y+hCQ==
|
||||
+274
-206
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
q1EyZ75QcdG2X6FVDkUoAyBtQE3ONA+7k9cmNFmXFgOOuGRPOpSDFUtbSvy86HPqnii26DMoeFJ1hatWJ0lBCQ==
|
||||
M1Jm4YHXsm0msygmd+XCJBRWMrXIjQfv1Y5v7XS8RCachLQwEzUJ1nhhic6CXxItNLmvgmDjVCMPVdHpnOMqDA==
|
||||
Generated
+478
-948
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -31,13 +31,13 @@
|
||||
"@types/node": "^25.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
||||
"@typescript-eslint/parser": "^8.58.1",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"fast-check": "^4.7.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.2"
|
||||
"vite": "^8.0.16"
|
||||
},
|
||||
"overrides": {
|
||||
"ajv": "6.14.0",
|
||||
|
||||
@@ -321,7 +321,14 @@ const fetchJson = async ({ repo, token, pathname, fetchImpl }) => {
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
const suffix = body ? ` ${body.slice(0, 500)}` : '';
|
||||
throw new Error(`GitHub traffic API request failed for ${repo}: ${url.pathname}${url.search} returned ${response.status}.${suffix}`);
|
||||
const lacksPushAccess = response.status === 403
|
||||
&& /resource not accessible|must have push access/i.test(body);
|
||||
const hint = lacksPushAccess
|
||||
? ' Traffic endpoints require a token with push access to the repository; the Actions GITHUB_TOKEN is always rejected. Use a classic PAT with the repo scope or a fine-grained PAT with read access to Administration.'
|
||||
: response.status === 401
|
||||
? ' The token was rejected as invalid — it may be expired or revoked. Rotate the TRAFFIC_ARCHIVE_TOKEN secret.'
|
||||
: '';
|
||||
throw new Error(`GitHub traffic API request failed for ${repo}: ${url.pathname}${url.search} returned ${response.status}.${suffix}${hint}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
|
||||
@@ -469,7 +469,7 @@ async function main() {
|
||||
|
||||
await runSkillSpector({
|
||||
skillspectorBin: args.skillspectorBin,
|
||||
skillDir: tempSkillDir,
|
||||
skillDir: innerDir,
|
||||
reportPath: path.join(releaseAssetsDir, "skillspector-report.md"),
|
||||
});
|
||||
|
||||
|
||||
@@ -76,6 +76,40 @@ test('fetchGitHubTraffic requests the daily GitHub traffic endpoints with auth',
|
||||
assert.deepEqual(snapshot.clones.clones, responses[`/repos/${TEST_REPOSITORY}/traffic/clones?per=day`].clones);
|
||||
});
|
||||
|
||||
test('fetchGitHubTraffic explains traffic token requirements on 403', async () => {
|
||||
const fetchImpl = async () => new globalThis.Response(
|
||||
JSON.stringify({ message: 'Resource not accessible by integration' }),
|
||||
{ status: 403 },
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
fetchGitHubTraffic({
|
||||
repo: TEST_REPOSITORY,
|
||||
token: 'installation-token',
|
||||
capturedAt,
|
||||
fetchImpl,
|
||||
}),
|
||||
/returned 403\..*push access/,
|
||||
);
|
||||
});
|
||||
|
||||
test('fetchGitHubTraffic flags invalid tokens on 401', async () => {
|
||||
const fetchImpl = async () => new globalThis.Response(
|
||||
JSON.stringify({ message: 'Bad credentials' }),
|
||||
{ status: 401 },
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
fetchGitHubTraffic({
|
||||
repo: TEST_REPOSITORY,
|
||||
token: 'expired-token',
|
||||
capturedAt,
|
||||
fetchImpl,
|
||||
}),
|
||||
/returned 401\..*expired or revoked/,
|
||||
);
|
||||
});
|
||||
|
||||
test('mergeTrafficArchive upserts daily views and clones without double-counting overlapping windows', () => {
|
||||
const archive = mergeTrafficArchive(
|
||||
{
|
||||
@@ -232,7 +266,8 @@ test('traffic archive workflow uses a daily schedule and a dedicated archive bra
|
||||
|
||||
assert.match(workflow, /cron:\s+'17 3 \* \* \*'/);
|
||||
assert.match(workflow, /TRAFFIC_ARCHIVE_BRANCH:\s+traffic-archive/);
|
||||
assert.match(workflow, /TRAFFIC_ARCHIVE_TOKEN/);
|
||||
assert.match(workflow, /GH_TRAFFIC_TOKEN:\s*\$\{\{\s*secrets\.TRAFFIC_ARCHIVE_TOKEN\b/);
|
||||
assert.doesNotMatch(workflow, /GH_TRAFFIC_TOKEN:[^\n]*github\.token/);
|
||||
assert.match(workflow, /node scripts\/archive-github-traffic\.mjs/);
|
||||
assert.match(workflow, /git add traffic\/archive\.json traffic\/summary\.json/);
|
||||
assert.match(workflow, /git rm --ignore-unmatch traffic\/README\.md/);
|
||||
|
||||
@@ -34,6 +34,13 @@ assert.match(
|
||||
'Skill release validation must ignore test-only skill changes while inspecting release-relevant skill files',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
workflow.includes('name = tolower($NF)')
|
||||
&& workflow.includes('name ~ /^(test|spec)[_-]/')
|
||||
&& workflow.includes('name ~ /\\.(test|spec)\\./'),
|
||||
'Skill release validation must filter test-named skill files such as scripts/test_*.py before selecting dry-run skill directories',
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
workflow,
|
||||
/No version bump detected for \$\{skill_dir\}; skipping\./,
|
||||
@@ -88,6 +95,74 @@ assert.match(
|
||||
'Skill release workflow must generate a SkillSpector report for each released skill',
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
workflow,
|
||||
/"### SkillSpector Security Report"/,
|
||||
'GitHub release notes must not add a duplicate SkillSpector heading before the generated report',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/readFileSync\("release-assets\/skillspector-report\.md", "utf8"\)[\s\S]*report,[\s\S]*\[skillspector-report\.md\]\(https:\/\/github\.com\/\$\{process\.env\.REPO\}\/releases\/download\/\$\{process\.env\.TAG\}\/skillspector-report\.md\)/,
|
||||
'GitHub release notes must embed the generated SkillSpector report and include a direct 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.match(
|
||||
workflow,
|
||||
/Run release dry-run for changed skills[\s\S]*git diff --name-only "\$\{BASE_SHA\}\.\.\.\$\{HEAD_SHA\}" --[\s\S]*'skills\/\*\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/test\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/tests\/\*\*'/,
|
||||
'PR dry-run SkillSpector scan must run when any release-relevant skill package file changes',
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
workflow.includes('local name="${lower##*/}"')
|
||||
&& workflow.includes('"$name" == test_*')
|
||||
&& workflow.includes('"$name" == *.test.*')
|
||||
&& workflow.includes('(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\\.(test|spec)\\.'),
|
||||
'Skill release archives must exclude test directories and test-named files from staged release payloads',
|
||||
);
|
||||
|
||||
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/,
|
||||
@@ -139,6 +214,48 @@ assert.match(
|
||||
'SkillSpector report must be included in the signed checksums manifest',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/Upload SkillSpector PR reports[\s\S]*actions\/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7\.0\.1[\s\S]*name: skillspector-pr-reports/,
|
||||
'PR dry-run must upload generated SkillSpector reports as workflow artifacts',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/comment-skillspector-report:[\s\S]*needs: release[\s\S]*issues: write[\s\S]*actions\/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8\.0\.1/,
|
||||
'Skill release workflow must download generated SkillSpector reports in a separate PR comment job with comment permissions',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/comment-skillspector-report:[\s\S]*if: always\(\) && github\.event_name == 'pull_request' && needs\.release\.result != 'cancelled'[\s\S]*Download SkillSpector reports[\s\S]*continue-on-error: true/,
|
||||
'SkillSpector PR comments must still run when the release dry-run produced reports but the release job failed later',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/function sanitizeReportForComment\(report\)[\s\S]*code block omitted from PR comment[\s\S]*inline snippet omitted[\s\S]*redacted-email[\s\S]*redacted-token/,
|
||||
'SkillSpector PR comments must sanitize raw report content before posting to the PR',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/const sanitizedReport = sanitizeReportForComment\(report\);[\s\S]*`\$\{marker\}\\n\$\{sanitizedReport\}/,
|
||||
'SkillSpector PR comments must use the sanitized report body, not the raw artifact text',
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
workflow,
|
||||
/`\$\{marker\}\\n\$\{report\.trimEnd\(\)\}/,
|
||||
'SkillSpector PR comments must not post report.trimEnd() verbatim',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/clawsec-skillspector-report:\$\{tag\}[\s\S]*github\.rest\.issues\.updateComment[\s\S]*github\.rest\.issues\.createComment/,
|
||||
'SkillSpector PR comments must use stable per-skill markers and update existing comments before creating new ones',
|
||||
);
|
||||
|
||||
assert.match(
|
||||
workflow,
|
||||
/Simulate tag release build/,
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
+1172
-1515
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
agiAAFvzM1vNHxH2+bGtyeKqFScLWJHnNreBcPpTODUqD0xqFi0cnyP/ZaZX+Rsw1Y9uZ7pGdFdA93pD4lh2BQ==
|
||||
jPrlTYwicRwoQgTs5Rk3Y3g6Lz78jNRs9ZNf0R09M4jkJokZENxfvhvHphI9MH4u+7wv0sFZ+yZbQtJ42y+hCQ==
|
||||
@@ -35,6 +35,7 @@
|
||||
| GitHub API | Deploy/release workflows | Discover releases, download assets, publish outputs. |
|
||||
| GitHub Pages | Deploy workflow | Serve static site and mirrored artifacts. |
|
||||
| ClawHub CLI/registry | Install scripts + optional publish jobs | Install and publish skills. |
|
||||
| [NVIDIA SkillSpector](https://github.com/NVIDIA/SkillSpector) | Skill release workflow | Scan staged skill release payloads and produce Markdown release evidence. |
|
||||
| Optional local SMTP/sendmail | `openclaw-audit-watchdog` scripts | Deliver audit reports by email. |
|
||||
|
||||
## Development Tools
|
||||
@@ -46,6 +47,7 @@
|
||||
| Bandit | `bandit -r utils/ -ll` | Python security checks. |
|
||||
| Trivy | Workflow + optional local run | FS/config vulnerability scans. |
|
||||
| Gitleaks | `scripts/prepare-to-push.sh` optional local run | Secret leak detection before push. |
|
||||
| SkillSpector | `.github/workflows/skill-release.yml` | Release-payload scanner used for PR comments and signed release artifacts. |
|
||||
|
||||
## Example Snippets
|
||||
```json
|
||||
@@ -83,6 +85,7 @@ skips = ["B101"]
|
||||
- PR validation enforces version parity between `skill.json` and `SKILL.md` frontmatter for bumped skills.
|
||||
- The public skills index keeps latest discovered version per skill for UI display.
|
||||
- Signed artifact manifests (`checksums.json`) are versioned per release and include file hashes and URLs.
|
||||
- SkillSpector reports are generated per release payload and included in signed artifact manifests.
|
||||
|
||||
## Source References
|
||||
- package.json
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@
|
||||
| --- | --- |
|
||||
| Skill Tag | Git tag formatted as `<skill>-v<semver>` used by release automation. |
|
||||
| Release Assets | Files attached to GitHub release (zip, `skill.json`, checksums, signatures). |
|
||||
| SkillSpector Report | Markdown security scan evidence generated from a staged skill release payload. |
|
||||
| Catalog Index | `public/skills/index.json`, generated list consumed by web catalog. |
|
||||
| Embedded Components | Capability bundle from one skill included in another (for example feed embedded in suite). |
|
||||
|
||||
@@ -39,7 +40,7 @@
|
||||
| --- | --- |
|
||||
| Poll NVD CVEs Workflow | Scheduled workflow that fetches and transforms NVD CVEs into advisories. |
|
||||
| Community Advisory Workflow | Issue-label-triggered workflow that publishes approved community advisories. |
|
||||
| Skill Release Workflow | Tag-triggered packaging/signing/publishing pipeline for skills. |
|
||||
| Skill Release Workflow | PR and tag-triggered packaging/signing/publishing pipeline for skills. |
|
||||
| Deploy Pages Workflow | Workflow that builds site assets and mirrors release/advisory artifacts. |
|
||||
|
||||
## Source References
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Track translation coverage and freshness versus English source docs.
|
||||
|
||||
_Last updated: 2026-04-27_
|
||||
_Last updated: 2026-06-14_
|
||||
|
||||
## README Coverage
|
||||
|
||||
@@ -24,6 +24,12 @@ _Last updated: 2026-04-27_
|
||||
| `wiki/testing.md` | — | pending |
|
||||
| `wiki/workflow.md` | — | pending |
|
||||
|
||||
## English Source Freshness Notes
|
||||
|
||||
| Date | Changed pages | Translation impact |
|
||||
| --- | --- | --- |
|
||||
| 2026-06-14 | `wiki/workflow.md`, `wiki/modules/automation-release.md`, `wiki/security-signing-runbook.md`, `wiki/dependencies.md`, `wiki/glossary.md` | Added SkillSpector release-pipeline documentation, signed-report behavior, and PR comment behavior. Translation refresh pending. |
|
||||
|
||||
## Wiki Coverage (KO)
|
||||
|
||||
| Source page | Korean page | Status |
|
||||
|
||||
@@ -19,11 +19,38 @@ This module intentionally focuses on automation/release-specific workflow behavi
|
||||
When a skill is tagged (for example, `soul-guardian-v1.0.0`), the pipeline:
|
||||
1. Validates `skill.json` version/tag alignment.
|
||||
2. Enforces signing-key consistency against canonical repo key material.
|
||||
3. Generates `checksums.json` for SBOM files.
|
||||
4. Signs and verifies release checksum artifacts.
|
||||
5. Publishes GitHub Release assets.
|
||||
6. Supersedes older releases within the same major version (tags remain).
|
||||
7. Triggers website catalog refresh.
|
||||
3. Stages the release payload from SBOM-scoped files and root skill docs.
|
||||
4. Generates release trust packet files, install instructions, and a SkillSpector security report.
|
||||
5. Generates `checksums.json` for the archive and release assets.
|
||||
6. Signs and verifies release checksum artifacts.
|
||||
7. Publishes GitHub Release assets.
|
||||
8. Supersedes older releases within the same major version (tags remain).
|
||||
9. Triggers website catalog refresh.
|
||||
|
||||
### PR dry-run behavior
|
||||
PRs that touch skill packages run the release workflow in validation mode:
|
||||
- `validate-pr-version-sync` checks changed skill metadata and documentation parity.
|
||||
- `release` builds dry-run release assets for changed release-relevant skill files.
|
||||
- `comment-skillspector-report` posts a sanitized SkillSpector summary back to the PR when reports are available.
|
||||
- `simulate-tag-release-build` exercises the tag-release builder across skills without publishing.
|
||||
|
||||
The PR path exists to catch packaging, signing, and release-evidence regressions before a maintainer pushes a real release tag.
|
||||
|
||||
### SkillSpector release evidence
|
||||
The pipeline installs [NVIDIA SkillSpector](https://github.com/NVIDIA/SkillSpector) inside GitHub Actions and runs:
|
||||
|
||||
```bash
|
||||
skillspector scan <staged-release-payload> --no-llm --format markdown --output skillspector-report.md
|
||||
```
|
||||
|
||||
The scan target is the staged payload, not the raw `skills/<name>/` source directory. That matters because release evidence should describe what users install, while source-only tests and fixtures stay outside the packaged payload.
|
||||
|
||||
SkillSpector output is used in three places:
|
||||
- PR dry-run artifact: `skillspector-pr-reports`
|
||||
- GitHub release asset: `skillspector-report.md`
|
||||
- Signed checksum manifest: `checksums.json` includes the SkillSpector report hash
|
||||
|
||||
PR comments intentionally use a sanitized summary. Raw code blocks, inline snippets, emails, and token-like values are omitted from the comment body, and reviewers can download the workflow artifact when they need the full report.
|
||||
|
||||
### Signing-key consistency guardrails
|
||||
Guardrail script:
|
||||
@@ -40,9 +67,16 @@ Enforced in:
|
||||
|
||||
### Release artifacts
|
||||
Each skill release includes:
|
||||
- `<skill>-v<version>.zip`
|
||||
- `checksums.json`
|
||||
- `checksums.sig`
|
||||
- `signing-public.pem`
|
||||
- `skill.json`
|
||||
- `SKILL.md`
|
||||
- `skill-card.md`
|
||||
- `permissions.json`
|
||||
- `install.md`
|
||||
- `skillspector-report.md`
|
||||
- Additional SBOM-scoped files
|
||||
|
||||
Operational docs:
|
||||
@@ -58,6 +92,7 @@ Operational docs:
|
||||
- `.github/workflows/deploy-pages.yml`: site build + asset mirroring to GitHub Pages.
|
||||
- `.github/workflows/wiki-sync.yml`: syncs repository `wiki/` into GitHub Wiki.
|
||||
- `.github/actions/sign-and-verify/action.yml`: shared Ed25519 sign/verify composite action.
|
||||
- `https://github.com/NVIDIA/SkillSpector`: upstream SkillSpector scanner installed by the release workflow.
|
||||
- `scripts/prepare-to-push.sh`: local CI-like quality gate.
|
||||
- `scripts/release-skill.sh`: manual helper for version bump + tag workflow.
|
||||
|
||||
|
||||
@@ -141,11 +141,18 @@ Current behavior:
|
||||
Current release generator:
|
||||
- `.github/workflows/skill-release.yml`
|
||||
|
||||
Current behavior:
|
||||
Detailed packaging and SkillSpector behavior lives in [Automation and Release Pipelines](modules/automation-release.md). This runbook only records the signing controls operators must verify.
|
||||
|
||||
Signing controls:
|
||||
- creates `checksums.json`, signs it as `checksums.sig`, and verifies signature before publish
|
||||
- includes `signing-public.pem` in release assets
|
||||
- validates generated public-key fingerprint against canonical key material
|
||||
|
||||
Operator review points:
|
||||
- verify `checksums.json` includes the release-evidence files documented in `wiki/modules/automation-release.md`
|
||||
- verify `checksums.sig` validates against `signing-public.pem`
|
||||
- review the release workflow run and PR evidence links before pushing or approving follow-up release tags
|
||||
|
||||
## 8) Rotation policy and runbook
|
||||
|
||||
### Rotation cadence
|
||||
|
||||
+7
-1
@@ -32,11 +32,16 @@
|
||||
|
||||
## Release Workflow Details
|
||||
- Version bump and docs parity are enforced for PR/tag paths.
|
||||
- Skill packaging includes SBOM-declared files and integrity manifests.
|
||||
- PR runs validate changed skill packages with a dry-run build before anything is published.
|
||||
- Tag pushes matching `<skill>-v<semver>` build the real release payload, sign `checksums.json`, verify the signature, and publish GitHub Release assets.
|
||||
- Skill packaging includes SBOM-declared files, release trust packet files, install instructions, security scan evidence, and integrity manifests.
|
||||
- `checksums.json` is signed and immediately verified in workflow execution.
|
||||
- Optional publish-to-ClawHub job runs after successful GitHub release when configured.
|
||||
- Older releases within same major line can be superseded/deleted by automation.
|
||||
|
||||
## SkillSpector Release Evidence
|
||||
Detailed SkillSpector release behavior lives in [Automation and Release Pipelines](modules/automation-release.md). Keep the detailed scanner command, staged-payload rules, PR comment behavior, and release-asset list there so scanner changes have one primary documentation owner.
|
||||
|
||||
## Advisory Workflow Details
|
||||
- NVD workflow determines incremental window from previous feed `updated` timestamp.
|
||||
- Transform phase maps CVE metrics to severity/type and normalizes affected targets.
|
||||
@@ -74,6 +79,7 @@ on:
|
||||
- .github/workflows/poll-nvd-cves.yml
|
||||
- .github/workflows/community-advisory.yml
|
||||
- .github/workflows/skill-release.yml
|
||||
- https://github.com/NVIDIA/SkillSpector
|
||||
- .github/workflows/deploy-pages.yml
|
||||
- .github/workflows/pages-verify.yml
|
||||
- .github/workflows/wiki-sync.yml
|
||||
|
||||
Reference in New Issue
Block a user