Enhance/skill release (#8)

* Refactor skill packaging and checksum generation process

- Removed .skill package creation from the skill-release workflow and scripts, focusing on checksum generation only.
- Updated README and SKILL.md files to reflect new installation methods using clawhub.
- Simplified the skill checksums generator script to only generate checksums without packaging.
- Adjusted installation instructions across various skills to promote clawhub for easier installation.
- Enhanced error handling and verification steps in the installation scripts for individual files.

* Add ext-docs to .gitignore to exclude documentation files from version control
This commit is contained in:
davida-ps
2026-02-08 18:18:21 +01:00
committed by GitHub
parent 85966ff569
commit 4542b7b96b
9 changed files with 462 additions and 254 deletions
+362 -109
View File
@@ -4,6 +4,10 @@ on:
push:
tags:
- '*-v[0-9]*.[0-9]*.[0-9]*'
pull_request:
paths:
- 'skills/*/skill.json'
- 'skills/*/SKILL.md'
permissions:
contents: write
@@ -15,7 +19,349 @@ concurrency:
cancel-in-progress: false
jobs:
validate-pr-version-sync:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate version parity for bumped skills
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
get_md_version() {
local md_file="$1"
awk '
NR == 1 && $0 == "---" { in_frontmatter = 1; next }
in_frontmatter && $0 == "---" { exit }
in_frontmatter && $0 ~ /^version:[[:space:]]*/ {
sub(/^version:[[:space:]]*/, "", $0)
gsub(/[[:space:]]+$/, "", $0)
print $0
exit
}
' "$md_file"
}
get_md_version_from_git() {
local sha="$1"
local path="$2"
local tmp_file
tmp_file="$(mktemp)"
if git cat-file -e "${sha}:${path}" 2>/dev/null; then
git show "${sha}:${path}" > "$tmp_file"
get_md_version "$tmp_file"
fi
rm -f "$tmp_file"
}
touched_skills_file="$(mktemp)"
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- 'skills/*/skill.json' 'skills/*/SKILL.md' \
| 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."
rm -f "${touched_skills_file}"
exit 0
fi
checked_skills=0
failures=0
while IFS= read -r skill_dir; do
json_path="${skill_dir}/skill.json"
md_path="${skill_dir}/SKILL.md"
head_json_version=""
if [ -f "${json_path}" ]; then
head_json_version="$(jq -r '.version // empty' "${json_path}" 2>/dev/null || true)"
fi
head_md_version=""
if [ -f "${md_path}" ]; then
head_md_version="$(get_md_version "${md_path}")"
fi
base_json_version=""
if git cat-file -e "${BASE_SHA}:${json_path}" 2>/dev/null; then
base_json_version="$(git show "${BASE_SHA}:${json_path}" | jq -r '.version // empty' 2>/dev/null || true)"
fi
base_md_version="$(get_md_version_from_git "${BASE_SHA}" "${md_path}")"
json_version_changed=false
md_version_changed=false
if [ "${head_json_version}" != "${base_json_version}" ]; then
json_version_changed=true
fi
if [ "${head_md_version}" != "${base_md_version}" ]; then
md_version_changed=true
fi
if [ "${json_version_changed}" != "true" ] && [ "${md_version_changed}" != "true" ]; then
echo "No version bump detected for ${skill_dir}; skipping."
continue
fi
checked_skills=$((checked_skills + 1))
echo "Version bump detected for ${skill_dir} (skill.json changed: ${json_version_changed}, SKILL.md changed: ${md_version_changed})"
if [ ! -f "${json_path}" ]; then
echo "::error file=${json_path}::Missing skill.json after version bump."
failures=$((failures + 1))
continue
fi
if [ ! -f "${md_path}" ]; then
echo "::error file=${md_path}::Missing SKILL.md after version bump."
failures=$((failures + 1))
continue
fi
if [ -z "${head_json_version}" ]; then
echo "::error file=${json_path}::Missing .version in skill.json."
failures=$((failures + 1))
continue
fi
if [ -z "${head_md_version}" ]; then
echo "::error file=${md_path}::Missing version in SKILL.md frontmatter."
failures=$((failures + 1))
continue
fi
if [ "${head_json_version}" != "${head_md_version}" ]; then
echo "::error file=${json_path}::Version mismatch. skill.json=${head_json_version}, SKILL.md=${head_md_version}"
failures=$((failures + 1))
continue
fi
echo "Version parity OK for ${skill_dir}: ${head_json_version}"
done < "${touched_skills_file}"
rm -f "${touched_skills_file}"
if [ "${checked_skills}" -eq 0 ]; then
echo "No version bumps detected in changed skill metadata files."
exit 0
fi
if [ "${failures}" -gt 0 ]; then
echo "::error::Found ${failures} version parity issue(s) across ${checked_skills} bumped skill(s)."
exit 1
fi
echo "Validated ${checked_skills} bumped skill(s): skill.json and SKILL.md versions are present and equal."
release:
if: github.event_name == 'pull_request'
needs: validate-pr-version-sync
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run release dry-run for changed skills
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
get_md_version() {
local md_file="$1"
awk '
NR == 1 && $0 == "---" { in_frontmatter = 1; next }
in_frontmatter && $0 == "---" { exit }
in_frontmatter && $0 ~ /^version:[[:space:]]*/ {
sub(/^version:[[:space:]]*/, "", $0)
gsub(/[[:space:]]+$/, "", $0)
print $0
exit
}
' "$md_file"
}
touched_skills_file="$(mktemp)"
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- 'skills/*/skill.json' 'skills/*/SKILL.md' \
| 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."
rm -f "${touched_skills_file}"
exit 0
fi
dry_run_count=0
failures=0
mkdir -p dist/dry-run
while IFS= read -r skill_dir; do
json_path="${skill_dir}/skill.json"
md_path="${skill_dir}/SKILL.md"
head_json_version=""
if [ -f "${json_path}" ]; then
head_json_version="$(jq -r '.version // empty' "${json_path}" 2>/dev/null || true)"
fi
head_md_version=""
if [ -f "${md_path}" ]; then
head_md_version="$(get_md_version "${md_path}")"
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))
continue
fi
if [ ! -f "${json_path}" ]; then
echo "::error file=${json_path}::Missing skill.json."
failures=$((failures + 1))
continue
fi
if [ ! -f "${md_path}" ]; then
echo "::error file=${md_path}::Missing SKILL.md."
failures=$((failures + 1))
continue
fi
if ! jq -e '.name and .version and .sbom and .sbom.files and (.sbom.files | type == "array")' "${json_path}" >/dev/null 2>&1; then
echo "::error file=${json_path}::skill.json missing required release fields (name/version/sbom.files)."
failures=$((failures + 1))
continue
fi
skill_name="$(basename "${skill_dir}")"
version="${head_json_version}"
tag="${skill_name}-v${version}"
dry_run_count=$((dry_run_count + 1))
echo "::group::Dry-run release ${tag}"
out_root="dist/dry-run/${tag}"
out_dist="${out_root}/dist"
out_assets="${out_root}/release-assets"
checksums_file="${out_dist}/checksums.json"
mkdir -p "${out_dist}" "${out_assets}"
cat > "${checksums_file}" << EOF
{
"skill": "${skill_name}",
"version": "${version}",
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"repository": "${{ github.repository }}",
"tag": "${tag}",
"files": {
EOF
first=true
temp_sbom_file="$(mktemp)"
jq -r '.sbom.files[].path' "${json_path}" > "${temp_sbom_file}"
while IFS= read -r file; do
[ -z "${file}" ] && continue
full_path="${skill_dir}/${file}"
if [ -f "${full_path}" ]; then
sha256="$(sha256sum "${full_path}" | awk '{print $1}')"
size="$(stat -c%s "${full_path}" 2>/dev/null || stat -f%z "${full_path}")"
filename="$(basename "${file}")"
if [ "${first}" = true ]; then
first=false
else
echo " ," >> "${checksums_file}"
fi
cat >> "${checksums_file}" << FILEENTRY
"${filename}": {
"sha256": "${sha256}",
"size": ${size},
"path": "${file}",
"url": "https://github.com/${{ github.repository }}/releases/download/${tag}/${filename}"
}
FILEENTRY
cp "${full_path}" "${out_assets}/${filename}"
else
echo "::error file=${json_path}::SBOM references missing file: ${file}"
failures=$((failures + 1))
fi
done < "${temp_sbom_file}"
rm -f "${temp_sbom_file}"
skill_json_sha="$(sha256sum "${json_path}" | awk '{print $1}')"
skill_json_size="$(stat -c%s "${json_path}" 2>/dev/null || stat -f%z "${json_path}")"
cat >> "${checksums_file}" << SKILLJSON
,
"skill.json": {
"sha256": "${skill_json_sha}",
"size": ${skill_json_size},
"url": "https://github.com/${{ github.repository }}/releases/download/${tag}/skill.json"
}
SKILLJSON
cat >> "${checksums_file}" << EOF
}
}
EOF
if ! jq -e . "${checksums_file}" >/dev/null 2>&1; then
echo "::error file=${checksums_file}::Generated checksums.json is invalid JSON."
failures=$((failures + 1))
echo "::endgroup::"
continue
fi
cp "${json_path}" "${out_assets}/skill.json"
if [ -f "${skill_dir}/README.md" ]; then
cp "${skill_dir}/README.md" "${out_assets}/README.md"
fi
cp "${checksums_file}" "${out_assets}/checksums.json"
echo "Prepared dry-run assets for ${tag}:"
ls -la "${out_assets}"
echo "::endgroup::"
done < "${touched_skills_file}"
rm -f "${touched_skills_file}"
if [ "${failures}" -gt 0 ]; then
echo "::error::Release dry-run failed with ${failures} issue(s) across ${dry_run_count} skill(s)."
exit 1
fi
if [ "${dry_run_count}" -eq 0 ]; then
echo "No changed skills found for dry-run."
exit 0
fi
echo "Release dry-run completed successfully for ${dry_run_count} changed skill(s)."
release-tag:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
@@ -191,96 +537,7 @@ jobs:
}
SKILLJSON
# Note: checksums.json is NOT closed here - will be finalized after .skill package is created
echo "=== Intermediate checksums.json (before .skill) ==="
cat "dist/checksums.json"
- name: Bundle security skills into suite
if: steps.parse.outputs.skill_name == 'clawsec-suite'
run: |
SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
echo "=== Bundling security skills into suite ==="
# Create bundled directory
mkdir -p "$SKILL_PATH/bundled"
# List of skills to bundle (exclude clawtributor - opt-in only)
BUNDLE_SKILLS=("clawsec-feed" "openclaw-audit-watchdog" "soul-guardian")
for skill in "${BUNDLE_SKILLS[@]}"; do
if [ -d "skills/$skill" ]; then
echo "Bundling $skill..."
mkdir -p "$SKILL_PATH/bundled/$skill"
cp -r "skills/$skill"/* "$SKILL_PATH/bundled/$skill/"
# Verify skill.json exists
if [ -f "$SKILL_PATH/bundled/$skill/skill.json" ]; then
SKILL_VERSION=$(jq -r '.version' "$SKILL_PATH/bundled/$skill/skill.json")
echo "✓ Bundled $skill v${SKILL_VERSION}"
else
echo "ERROR: $skill/skill.json not found"
exit 1
fi
else
echo "WARNING: skills/$skill not found, skipping..."
fi
done
echo "Bundling complete"
- name: Create .skill package
run: |
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
cd "$SKILL_PATH"
# Create zip starting with skill.json
zip -q "../../dist/${SKILL_NAME}.skill" skill.json
# Add each SBOM file individually to preserve directory structure
while IFS= read -r file; do
if [ -f "$file" ]; then
zip -qu "../../dist/${SKILL_NAME}.skill" "$file"
echo "Added: $file"
else
echo "Warning: SBOM file not found: $file"
fi
done < <(jq -r '.sbom.files[].path' skill.json)
# Add README if it exists
if [ -f README.md ]; then
zip -qu "../../dist/${SKILL_NAME}.skill" README.md
echo "Added: README.md"
fi
cd ../..
echo "=== Created ${SKILL_NAME}.skill ==="
unzip -l "dist/${SKILL_NAME}.skill"
- name: Add .skill checksum and finalize checksums.json
run: |
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
SKILL_PACKAGE="dist/${SKILL_NAME}.skill"
# Calculate .skill package checksum
SKILL_PACKAGE_SHA=$(sha256sum "$SKILL_PACKAGE" | awk '{print $1}')
SKILL_PACKAGE_SIZE=$(stat -c%s "$SKILL_PACKAGE" 2>/dev/null || stat -f%z "$SKILL_PACKAGE")
# Add .skill package entry to checksums
cat >> "dist/checksums.json" << SKILLPACKAGE
,
"${SKILL_NAME}.skill": {
"sha256": "${SKILL_PACKAGE_SHA}",
"size": ${SKILL_PACKAGE_SIZE},
"url": "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${SKILL_NAME}.skill"
}
SKILLPACKAGE
# Close JSON
# Close checksums JSON
cat >> "dist/checksums.json" << EOF
}
}
@@ -301,12 +558,6 @@ jobs:
jq -r '.sbom.files[].path' "$SKILL_PATH/skill.json" > "$TEMPFILE"
while IFS= read -r file; do
# Skip bundled files - they're only for the .skill package
if [[ "$file" == bundled/* ]]; then
echo "Skipping bundled file: $file"
continue
fi
if [ -f "$SKILL_PATH/$file" ]; then
# Flatten directory structure for release assets
cp "$SKILL_PATH/$file" "release-assets/$(basename "$file")"
@@ -322,8 +573,7 @@ jobs:
cp "$SKILL_PATH/README.md" release-assets/
fi
# Copy package and checksums
cp "dist/${SKILL_NAME}.skill" release-assets/
# Copy checksums
cp "dist/checksums.json" release-assets/
echo "=== Release assets ==="
@@ -362,29 +612,32 @@ jobs:
### Quick Install
Download the complete skill package:
**Via clawhub (recommended):**
```bash
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${{ steps.parse.outputs.skill_name }}.skill
npx clawhub@latest install ${{ steps.parse.outputs.skill_name }}
```
Or fetch the main skill file directly:
**Manual download with verification:**
```bash
curl -sL https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/SKILL.md
# 1. Download checksums
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.json
# 2. Download individual files
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/SKILL.md
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/skill.json
# 3. Verify checksums
sha256sum SKILL.md
# Compare with value in checksums.json
```
### Verification
All files include SHA256 checksums. Download `checksums.json` and verify:
All files include SHA256 checksums in `checksums.json`:
```bash
curl -sL https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.json | jq .
```
Verify a file:
```bash
sha256sum SKILL.md
# Compare with value in checksums.json
```
### Files
See `checksums.json` for the complete file manifest with SHA256 hashes.
+1
View File
@@ -2,6 +2,7 @@
.codex
_bmad
_bmad-output
ext-docs
# Logs
logs
*.log
+59 -30
View File
@@ -10,6 +10,7 @@ Thank you for your interest in contributing security skills to the ClawSec ecosy
- [skill.json Reference](#skilljson-reference)
- [Testing Your Skill](#testing-your-skill)
- [Submission Process](#submission-process)
- [Version Bump and Release Flow](#version-bump-and-release-flow)
- [Review Criteria](#review-criteria)
- [After Acceptance](#after-acceptance)
- [Submitting Security Advisories](#submitting-security-advisories)
@@ -49,7 +50,7 @@ git checkout -b skill/my-new-skill
All skills distributed through ClawSec undergo security review and are hashed for agent verification. Trust is implicit:
- **Backend Verification**: Every skill is validated against checksums, SBOM manifests, and security policies
- **Transparent Security**: SHA256 checksums, signature verification, and advisory feeds operate automatically
- **Transparent Security**: SHA256 checksums, and advisory feeds operate automatically
- **Contribution Flow**: Submit skills via PR → maintainer review → approval → release
@@ -145,14 +146,22 @@ Create `skill.json` with the following structure:
```
**Important Notes:**
- Start with version `0.0.1`
- Start with version `0.0.1` in both `skill.json` and `SKILL.md` frontmatter
- List ALL files your skill needs in the SBOM
### Step 3: Create SKILL.md
This is the main documentation for your skill. Use this template:
This is the main documentation for your skill. Include YAML frontmatter with a `version` that matches `skill.json`:
````markdown
```markdown
---
name: my-skill-name
version: 0.0.1
description: Brief description of what your skill does
metadata: {"openclaw":{"emoji":"🔒","category":"security"}}
---
# My Skill Name
## Overview
@@ -161,11 +170,7 @@ Brief description of what this skill does and why it's useful for AI agent secur
## Usage
How to use the skill:
```bash
# Example commands or usage patterns
```
How to use the skill.
## Features
@@ -182,25 +187,8 @@ How to use the skill:
## Security Considerations
Important security notes about this skill.
## Examples
### Example 1: Basic Usage
Description and example output.
### Example 2: Advanced Usage
Description and example output.
## Troubleshooting
Common issues and solutions.
## Contributing
How others can improve this skill.
```
````
### Step 4: Add Supporting Files
@@ -314,7 +302,8 @@ If your skill includes executable scripts or requires testing:
- [ ] All SBOM files exist
- [ ] skill.json is valid JSON
- [ ] Version is 1.0.0 for new skills
- [ ] Version is `0.0.1` for new skills
- [ ] `skill.json` version matches `SKILL.md` frontmatter version
- [ ] No hardcoded credentials or secrets
- [ ] Trigger phrases are descriptive
- [ ] Required binaries are documented
@@ -380,6 +369,39 @@ Any special considerations for reviewers.
---
## Version Bump and Release Flow
This repository uses a branch-first workflow for skill versions:
1. Make skill changes on a branch (`skill/<name>-...`).
2. Keep versions in sync:
- `skills/<skill>/skill.json` -> `.version`
- `skills/<skill>/SKILL.md` -> frontmatter `version`
3. For existing skills, you can bump versions on your branch with:
```bash
./scripts/release-skill.sh <skill-name> <new-version>
```
4. Push your branch and open a PR. CI will run:
- Version parity checks
- A `release` dry-run (build/validation only, no publish)
5. Do **not** push release tags from PR branches.
- `scripts/release-skill.sh` creates a local tag. Keep it local during PR review.
- If you need to remove that local tag: `git tag -d <skill-name>-v<version>`
6. After merge, a maintainer creates and pushes the release tag from `main`:
```bash
git checkout main
git pull --ff-only origin main
git tag -a <skill-name>-v<version> -m "<skill-name> version <version>"
git push origin <skill-name>-v<version>
```
7. Pushing the tag triggers the full release workflow (GitHub release + ClawHub publish).
---
## Review Criteria
Maintainers will review your skill based on:
@@ -419,8 +441,8 @@ Once your skill is accepted:
1. **Maintainers will:**
- Review your PR (Prompt Security staff or designated maintainers)
- Merge your PR after security review
- Create the first release using `scripts/release-skill.sh`
- Generate checksums and publish to GitHub Releases
- Create and push a release tag from merged `main` (`<skill>-v<version>`)
- Generate checksums and publish to GitHub Releases + ClawHub
- Update the skills catalog website
2. **You'll be credited:**
@@ -463,7 +485,7 @@ mkdir -p skills/simple-scanner
cat > skills/simple-scanner/skill.json << 'EOF'
{
"name": "simple-scanner",
"version": "0.0.1,
"version": "0.0.1",
"description": "Basic security scanner for AI agents",
"author": "contributor-name",
"license": "MIT",
@@ -484,6 +506,13 @@ cat > skills/simple-scanner/skill.json << 'EOF'
EOF
cat > skills/simple-scanner/SKILL.md << 'EOF'
---
name: simple-scanner
version: 0.0.1
description: Basic security scanner for AI agents
metadata: {"openclaw":{"emoji":"🔍","category":"security"}}
---
# Simple Scanner
A basic security scanner for AI agents.
+6 -9
View File
@@ -41,7 +41,7 @@ ClawSec is a **complete security skill suite for the OpenClaw family of agents (
- **🛡️ File Integrity Protection** - Drift detection and auto-restore for critical agent files (SOUL.md, IDENTITY.md, etc.)
- **📡 Live Security Advisories** - Automated NVD CVE polling and community threat intelligence
- **🔍 Security Audits** - Self-check scripts to detect prompt injection markers and vulnerabilities
- **🔐 Checksum Verification** - SHA256 checksums for all skill artifacts via `.skill` packages
- **🔐 Checksum Verification** - SHA256 checksums for all skill artifacts
- **Health Checks** - Automated updates and integrity verification for all installed skills
---
@@ -170,10 +170,9 @@ When a skill is tagged (e.g., `soul-guardian-v1.0.0`), the pipeline:
1. **Validates** - Checks `skill.json` version matches tag
2. **Generates Checksums** - Creates `checksums.json` with SHA256 hashes for all SBOM files
3. **Packages** - Creates `.skill` zip file with all required files
4. **Releases** - Publishes to GitHub Releases with all artifacts
5. **Supersedes Old Releases** - Marks older versions (same major) as pre-releases
6. **Triggers Pages Update** - Refreshes the skills catalog on the website
3. **Releases** - Publishes to GitHub Releases with all artifacts
4. **Supersedes Old Releases** - Marks older versions (same major) as pre-releases
5. **Triggers Pages Update** - Refreshes the skills catalog on the website
### Release Versioning & Superseding
@@ -194,7 +193,6 @@ When you release `skill-v0.0.2`, the previous `skill-v0.0.1` release is automati
### Release Artifacts
Each skill release includes:
- `<skill>.skill` - Packaged skill (zip format)
- `checksums.json` - SHA256 hashes for integrity verification
- `skill.json` - Skill metadata
- `SKILL.md` - Main skill documentation
@@ -220,16 +218,15 @@ Checks:
- SBOM files exist and are readable
- OpenClaw metadata is properly structured
### Skill Packager
### Skill Checksums Generator
Creates a distributable `.skill` file with checksums:
Generates `checksums.json` with SHA256 hashes for a skill:
```bash
python utils/package_skill.py skills/clawsec-feed ./dist
```
Outputs:
- `clawsec-feed.skill` - Zip package with all SBOM files
- `checksums.json` - SHA256 hashes for verification
---
+1 -1
View File
@@ -106,7 +106,7 @@ export const SkillDetail: React.FC = () => {
};
const installCommand = skillData
? `curl -sLO https://clawsec.prompt.security/releases/download/${skillData.name}-v${skillData.version}/${skillData.name}.skill`
? `npx clawhub@latest install ${skillData.name}`
: '';
const releasePageUrl = useMemo(() => {
+1 -45
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# populate-local-skills.sh
# Builds local skills index from skills/ directory for development preview.
# This mirrors the skill-release.yml pipeline exactly - generates real checksums and .skill packages.
# This mirrors the skill-release.yml pipeline exactly - generates real checksums.
#
# Usage: ./scripts/populate-local-skills.sh
@@ -159,50 +159,6 @@ FILEENTRY
}
SKILLJSON
# === Create .skill package BEFORE closing checksums JSON ===
SKILL_PACKAGE="$PUBLIC_SKILLS_DIR/$SKILL_NAME/${SKILL_NAME}.skill"
# Get files from SBOM and create zip
pushd "$SKILL_DIR" > /dev/null
FILES=$(jq -r '.sbom.files[].path' skill.json 2>/dev/null | tr '\n' ' ')
if [ -n "$FILES" ]; then
# Create zip with SBOM files + skill.json
zip -r "$SKILL_PACKAGE" $FILES skill.json 2>/dev/null || true
# Add README if exists
if [ -f README.md ]; then
zip -u "$SKILL_PACKAGE" README.md 2>/dev/null || true
fi
if [ -f "$SKILL_PACKAGE" ]; then
PACKAGE_SIZE=$(stat -f%z "$SKILL_PACKAGE" 2>/dev/null || stat -c%s "$SKILL_PACKAGE")
echo " ✓ Created: ${SKILL_NAME}.skill ($(( PACKAGE_SIZE / 1024 ))KB)"
# Add .skill package checksum
if command -v sha256sum &> /dev/null; then
SKILL_PACKAGE_SHA=$(sha256sum "$SKILL_PACKAGE" | awk '{print $1}')
else
SKILL_PACKAGE_SHA=$(shasum -a 256 "$SKILL_PACKAGE" | awk '{print $1}')
fi
echo "," >> "$CHECKSUMS_FILE"
cat >> "$CHECKSUMS_FILE" << SKILLPACKAGE
"${SKILL_NAME}.skill": {
"sha256": "$SKILL_PACKAGE_SHA",
"size": $PACKAGE_SIZE,
"url": "https://clawsec.prompt.security/releases/download/$TAG/${SKILL_NAME}.skill"
}
SKILLPACKAGE
echo " ✓ Checksum: ${SKILL_NAME}.skill ($SKILL_PACKAGE_SHA)"
fi
else
echo " ⚠️ No SBOM files, skipping .skill package"
fi
popd > /dev/null
# Close checksums JSON
cat >> "$CHECKSUMS_FILE" << EOF
}
+16 -6
View File
@@ -8,18 +8,26 @@
# 3. Committing the changes
# 4. Creating the git tag
#
# After running, push with: git push && git push origin <tag>
# After running, push your current branch and tag:
# git push origin <branch>
# git push origin <tag>
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <skill-name> <version>"
echo "Example: $0 clawsec-feed 1.1.0"
exit 1
fi
SKILL_NAME="$1"
VERSION="$2"
SKILL_PATH="skills/$SKILL_NAME"
# Validation
if [ -z "$SKILL_NAME" ] || [ -z "$VERSION" ]; then
echo "Usage: $0 <skill-name> <version>"
echo "Example: $0 clawsec-feed 1.1.0"
# Ensure we're on a branch (not detached HEAD) so release flow works from feature branches
CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD || true)"
if [ -z "$CURRENT_BRANCH" ]; then
echo "Error: Detached HEAD detected. Checkout a branch before running release." >&2
exit 1
fi
@@ -57,6 +65,7 @@ if ! git diff --quiet "$SKILL_PATH/" 2>/dev/null; then
fi
echo "Releasing $SKILL_NAME version $VERSION"
echo "Branch: $CURRENT_BRANCH"
echo "======================================="
# Create a temporary directory for atomic operations
@@ -215,7 +224,8 @@ fi
echo ""
echo "Done! To release, push the commit and tag:"
echo " git push && git push origin $TAG"
echo " git push origin $CURRENT_BRANCH"
echo " git push origin $TAG"
echo ""
echo "Or to undo:"
echo " git reset --hard HEAD~1 && git tag -d $TAG"
+2 -9
View File
@@ -45,8 +45,7 @@ get_release_assets() {
# Always included
assets+=("skill.json")
assets+=("checksums.json")
assets+=("${skill_name}.skill")
# README if exists
if [ -f "$skill_path/README.md" ]; then
assets+=("README.md")
@@ -151,12 +150,6 @@ validate_skill() {
fi
done < <(extract_all_referenced_files "$skill_path/SKILL.md")
# Check for common patterns that reference this skill
if grep -qE "/${skill_name}\.skill" "$skill_path/SKILL.md"; then
if printf '%s\n' "${RELEASE_ASSETS[@]}" | grep -q "^${skill_name}.skill$"; then
echo -e " ${GREEN}${NC} ${skill_name}.skill reference found and will be created"
fi
fi
fi
echo ""
@@ -199,7 +192,7 @@ validate_skill() {
for doc in "$other_skill_dir"/*.md; do
[ -f "$doc" ] || continue
if grep -qE "/${skill_name}\.skill|/${skill_name}-v" "$doc" 2>/dev/null; then
if grep -qE "/${skill_name}-v" "$doc" 2>/dev/null; then
echo -e " → Referenced by ${other_skill}/$(basename "$doc")"
cross_refs_found=true
fi
+14 -45
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Skill Packager - Creates a distributable .skill file and checksums
Skill Checksums Generator - Generates checksums.json for a skill
Usage:
python utils/package_skill.py <path/to/skill-folder> [output-directory]
@@ -13,7 +13,6 @@ Example:
import hashlib
import json
import sys
import zipfile
from datetime import datetime, timezone
from pathlib import Path
@@ -31,14 +30,14 @@ def calculate_sha256(file_path: Path) -> str:
def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None, Path | None]:
"""
Package a skill folder into a .skill file and generate checksums.
Generate checksums for a skill folder.
Args:
skill_path: Path to the skill folder
output_dir: Optional output directory (defaults to current directory)
Returns:
Tuple of (skill_file_path, checksums_file_path) or (None, None) on error
Tuple of (None, checksums_file_path) or (None, None) on error
"""
skill_path = Path(skill_path).resolve()
@@ -66,26 +65,25 @@ def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None,
else:
output_path = Path.cwd()
skill_filename = output_path / f"{skill_name}.skill"
checksums_filename = output_path / "checksums.json"
# Collect files from SBOM
files_to_package = []
files_to_checksum = []
sbom_files = skill_data.get("sbom", {}).get("files", [])
for file_entry in sbom_files:
file_rel_path = file_entry["path"]
full_path = skill_path / file_rel_path
if full_path.exists():
files_to_package.append((file_rel_path, full_path))
files_to_checksum.append((file_rel_path, full_path))
# Always include skill.json
files_to_package.append(("skill.json", skill_json_path))
files_to_checksum.append(("skill.json", skill_json_path))
# Include README.md if it exists
readme_path = skill_path / "README.md"
if readme_path.exists():
files_to_package.append(("README.md", readme_path))
files_to_checksum.append(("README.md", readme_path))
# Generate checksums
print("Generating checksums...")
@@ -98,7 +96,7 @@ def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None,
"files": {},
}
for rel_path, full_path in files_to_package:
for rel_path, full_path in files_to_checksum:
filename = Path(rel_path).name
sha256 = calculate_sha256(full_path)
size = full_path.stat().st_size
@@ -111,40 +109,12 @@ def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None,
}
print(f" {filename}: {sha256[:16]}...")
# Create .skill package (zip file) first so we can include its checksum
print("\nCreating .skill package...")
try:
with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
for rel_path, full_path in files_to_package:
# Use skill folder as root in archive
arcname = f"{skill_name}/{rel_path}"
zipf.write(full_path, arcname)
print(f" Added: {arcname}")
print(f"\n[OK] Package created: {skill_filename}")
except Exception as e:
print(f"[ERROR] Failed to create package: {e}")
return None, None
# Add .skill file to checksums now that it exists
skill_file_name = f"{skill_name}.skill"
skill_sha256 = calculate_sha256(skill_filename)
skill_size = skill_filename.stat().st_size
checksums_data["files"][skill_file_name] = {
"sha256": skill_sha256,
"size": skill_size,
"url": f"https://clawsec.prompt.security/releases/download/{skill_name}-v{version}/{skill_file_name}",
}
print(f" {skill_file_name}: {skill_sha256[:16]}...")
# Write checksums.json
with open(checksums_filename, "w") as f:
json.dump(checksums_data, f, indent=2)
print(f"\n[OK] Checksums written to: {checksums_filename}")
return skill_filename, checksums_filename
return None, checksums_filename
def main():
@@ -158,18 +128,17 @@ def main():
skill_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
print(f"Packaging skill: {skill_path}")
print(f"Generating checksums for: {skill_path}")
if output_dir:
print(f" Output directory: {output_dir}")
print()
skill_file, checksums_file = package_skill(skill_path, output_dir)
_, checksums_file = package_skill(skill_path, output_dir)
if skill_file and checksums_file:
if checksums_file:
print("\n" + "=" * 50)
print("Packaging complete!")
print(f" Skill package: {skill_file}")
print(f" Checksums: {checksums_file}")
print("Checksums generation complete!")
print(f" Checksums: {checksums_file}")
print("=" * 50)
sys.exit(0)
else: