From 4542b7b96b6cacf85b253438e2e9368338945ff3 Mon Sep 17 00:00:00 2001 From: davida-ps Date: Sun, 8 Feb 2026 18:18:21 +0100 Subject: [PATCH] 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 --- .github/workflows/skill-release.yml | 471 +++++++++++++++++++++------- .gitignore | 1 + CONTRIBUTING.md | 89 ++++-- README.md | 15 +- pages/SkillDetail.tsx | 2 +- scripts/populate-local-skills.sh | 46 +-- scripts/release-skill.sh | 22 +- scripts/validate-release-links.sh | 11 +- utils/package_skill.py | 59 +--- 9 files changed, 462 insertions(+), 254 deletions(-) diff --git a/.github/workflows/skill-release.yml b/.github/workflows/skill-release.yml index 14b1d32..ad6e144 100644 --- a/.github/workflows/skill-release.yml +++ b/.github/workflows/skill-release.yml @@ -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. diff --git a/.gitignore b/.gitignore index 86b9f61..cebeee4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .codex _bmad _bmad-output +ext-docs # Logs logs *.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d2696e..94b9497 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/-...`). +2. Keep versions in sync: + - `skills//skill.json` -> `.version` + - `skills//SKILL.md` -> frontmatter `version` +3. For existing skills, you can bump versions on your branch with: + +```bash +./scripts/release-skill.sh +``` + +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 -v` +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 -v -m " version " +git push origin -v +``` + +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` (`-v`) + - 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. diff --git a/README.md b/README.md index 03728bf..663574c 100644 --- a/README.md +++ b/README.md @@ -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` - 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 --- diff --git a/pages/SkillDetail.tsx b/pages/SkillDetail.tsx index 0b07145..9ab27e2 100644 --- a/pages/SkillDetail.tsx +++ b/pages/SkillDetail.tsx @@ -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(() => { diff --git a/scripts/populate-local-skills.sh b/scripts/populate-local-skills.sh index 754492f..2c080fc 100755 --- a/scripts/populate-local-skills.sh +++ b/scripts/populate-local-skills.sh @@ -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 } diff --git a/scripts/release-skill.sh b/scripts/release-skill.sh index 16a3c48..e478711 100755 --- a/scripts/release-skill.sh +++ b/scripts/release-skill.sh @@ -8,18 +8,26 @@ # 3. Committing the changes # 4. Creating the git tag # -# After running, push with: git push && git push origin +# After running, push your current branch and tag: +# git push origin +# git push origin set -euo pipefail +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + 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 " - 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" diff --git a/scripts/validate-release-links.sh b/scripts/validate-release-links.sh index 1db292f..a6c0c0a 100755 --- a/scripts/validate-release-links.sh +++ b/scripts/validate-release-links.sh @@ -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 diff --git a/utils/package_skill.py b/utils/package_skill.py index 8128969..0ffce34 100644 --- a/utils/package_skill.py +++ b/utils/package_skill.py @@ -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 [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: