name: Skill Release

on:
  push:
    tags:
      - '*-v[0-9]*.[0-9]*.[0-9]*'
  pull_request:
    paths:
      - 'skills/**'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to re-publish to ClawHub (e.g., clawsec-suite-v0.0.10)'
        required: true
        type: string

permissions: read-all

env:
  CLAWHUB_CLI_VERSION: 0.7.0

concurrency:
  group: skill-release-${{ github.ref }}
  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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Verify signing key consistency (repo + docs)
        run: ./scripts/ci/verify_signing_key_consistency.sh

      - name: Validate version parity 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"
          }

          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"
          }

          escape_regex() {
            printf '%s' "$1" | sed -e 's/[][(){}.^$*+?|\\]/\\&/g'
          }

          touched_skills_file="$(mktemp)"
          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 release-relevant skill package 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=""
            head_has_json=false
            if [ -f "${json_path}" ]; then
              head_has_json=true
              head_json_version="$(jq -r '.version // empty' "${json_path}" 2>/dev/null || true)"
            fi

            head_md_version=""
            head_has_md=false
            if [ -f "${md_path}" ]; then
              head_has_md=true
              head_md_version="$(get_md_version "${md_path}")"
            fi

            base_json_version=""
            base_has_json=false
            if git cat-file -e "${BASE_SHA}:${json_path}" 2>/dev/null; then
              base_has_json=true
              base_json_version="$(git show "${BASE_SHA}:${json_path}" | jq -r '.version // empty' 2>/dev/null || true)"
            fi

            base_md_version=""
            base_has_md=false
            if git cat-file -e "${BASE_SHA}:${md_path}" 2>/dev/null; then
              base_has_md=true
              base_md_version="$(get_md_version_from_git "${BASE_SHA}" "${md_path}")"
            fi

            if [ "${base_has_json}" = "true" ] && [ "${base_has_md}" = "true" ] && [ "${head_has_json}" != "true" ] && [ "${head_has_md}" != "true" ]; then
              echo "Skill ${skill_dir} was removed in this PR; skipping version parity check."
              continue
            fi

            checked_skills=$((checked_skills + 1))

            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 "::error file=${skill_dir}::Changed skill package has no version bump. Update skill.json and SKILL.md versions and add CHANGELOG.md release notes."
              failures=$((failures + 1))
              continue
            fi

            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}"

            changelog_path="${skill_dir}/CHANGELOG.md"
            if [ ! -f "${changelog_path}" ]; then
              echo "::error file=${changelog_path}::Missing CHANGELOG.md for bumped skill version ${head_json_version}."
              failures=$((failures + 1))
              continue
            fi

            escaped_version="$(escape_regex "${head_json_version}")"
            if ! grep -Eq "^## \\[${escaped_version}\\] - [0-9]{4}-[0-9]{2}-[0-9]{2}$" "${changelog_path}"; then
              echo "::error file=${changelog_path}::Missing required release-notes heading: ## [${head_json_version}] - YYYY-MM-DD"
              failures=$((failures + 1))
              continue
            fi

            changelog_entry="$(awk -v version="${head_json_version}" '
              BEGIN { in_section = 0; found = 0 }
              $0 ~ ("^## \\[" version "\\] - [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$") { in_section = 1; found = 1; next }
              in_section && found && /^---/ { exit }
              in_section && found && /^## / { exit }
              in_section { print }
            ' "${changelog_path}" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}')"

            if [ -z "${changelog_entry}" ]; then
              echo "::error file=${changelog_path}::Changelog entry for ${head_json_version} is empty. Add release notes under the version heading."
              failures=$((failures + 1))
              continue
            fi

            echo "Release notes check 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} skill metadata/release-notes issue(s) across ${checked_skills} bumped skill(s)."
            exit 1
          fi

          echo "Validated ${checked_skills} bumped skill(s): version parity and changelog release notes are present."

  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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          fetch-depth: 0

      - name: Generate test signing key for dry-run
        run: |
          set -euo pipefail
          echo "Generating temporary Ed25519 test key for dry-run validation"
          umask 077
          mkdir -p /tmp/test-signing
          # Use Ed25519 to match production signing (not RSA)
          openssl genpkey -algorithm ED25519 -out /tmp/test-signing/private.pem
          openssl pkey -in /tmp/test-signing/private.pem -pubout -out /tmp/test-signing/public.pem
          echo "TEST_SIGNING_KEY_DIR=/tmp/test-signing" >> $GITHUB_ENV

      - 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

          # Helper function to sign advisory artifacts with test key (dry-run only)
          sign_advisory_artifacts() {
            local skill_dir="$1"
            local advisory_dir="${skill_dir}/advisories"

            if [ ! -d "$advisory_dir" ] || [ ! -f "$advisory_dir/feed.json" ]; then
              return 0
            fi

            echo "  [Dry-run] Signing advisory artifacts with test key"

            local key_file="$TEST_SIGNING_KEY_DIR/private.pem"
            local pub_file="$TEST_SIGNING_KEY_DIR/public.pem"
            local tmp_sig_bin

            # Sign feed.json with Ed25519 (requires -rawin flag)
            openssl pkeyutl -sign -rawin -inkey "$key_file" -in "$advisory_dir/feed.json" | \
              openssl base64 -A > "$advisory_dir/feed.json.sig"

            # Verify Ed25519 feed.json signature (requires -rawin flag)
            tmp_sig_bin=$(mktemp)
            openssl base64 -d -A -in "$advisory_dir/feed.json.sig" -out "$tmp_sig_bin"
            if ! openssl pkeyutl -verify -rawin -pubin -inkey "$pub_file" -sigfile "$tmp_sig_bin" -in "$advisory_dir/feed.json" >/dev/null 2>&1; then
              echo "::error file=${skill_dir}/advisories/feed.json.sig::Feed signature verification failed after signing"
              rm -f "$tmp_sig_bin"
              return 1
            fi
            rm -f "$tmp_sig_bin"
            echo "  [Dry-run] Verified feed.json signature"

            # Generate checksums.json
            local feed_sha=$(sha256sum "$advisory_dir/feed.json" | awk '{print $1}')
            local feed_size=$(stat -c%s "$advisory_dir/feed.json" 2>/dev/null || stat -f%z "$advisory_dir/feed.json")
            local feed_sig_sha=$(sha256sum "$advisory_dir/feed.json.sig" | awk '{print $1}')
            local feed_sig_size=$(stat -c%s "$advisory_dir/feed.json.sig" 2>/dev/null || stat -f%z "$advisory_dir/feed.json.sig")

            jq -n \
              --arg schema_version "1" \
              --arg algorithm "sha256" \
              --arg version "test" \
              --arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
              --arg feed_sha "$feed_sha" \
              --argjson feed_size "$feed_size" \
              --arg feed_sig_sha "$feed_sig_sha" \
              --argjson feed_sig_size "$feed_sig_size" \
              '{
                schema_version: $schema_version,
                algorithm: $algorithm,
                version: $version,
                generated_at: $generated,
                files: {
                  "advisories/feed.json": {sha256: $feed_sha, size: $feed_size},
                  "advisories/feed.json.sig": {sha256: $feed_sig_sha, size: $feed_sig_size}
                }
              }' > "$advisory_dir/checksums.json"

            # Sign checksums.json with Ed25519 (requires -rawin flag)
            openssl pkeyutl -sign -rawin -inkey "$key_file" -in "$advisory_dir/checksums.json" | \
              openssl base64 -A > "$advisory_dir/checksums.json.sig"

            # Verify Ed25519 checksums.json signature (requires -rawin flag)
            tmp_sig_bin=$(mktemp)
            openssl base64 -d -A -in "$advisory_dir/checksums.json.sig" -out "$tmp_sig_bin"
            if ! openssl pkeyutl -verify -rawin -pubin -inkey "$pub_file" -sigfile "$tmp_sig_bin" -in "$advisory_dir/checksums.json" >/dev/null 2>&1; then
              echo "::error file=${skill_dir}/advisories/checksums.json.sig::Checksums signature verification failed after signing"
              rm -f "$tmp_sig_bin"
              return 1
            fi
            rm -f "$tmp_sig_bin"
            echo "  [Dry-run] Verified checksums.json signature"

            # Copy public key
            cp "$pub_file" "$advisory_dir/feed-signing-public.pem"

            echo "  [Dry-run] Advisory artifacts signed and verified with test key"
          }

          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

          dry_run_count=0
          failures=0
          mkdir -p dist/dry-run

          normalize_release_path() {
            local path="$1"
            path="${path//\\//}"
            while [[ "$path" == ./* ]]; do
              path="${path#./}"
            done
            while [[ "$path" == *//* ]]; do
              path="${path//\/\//\/}"
            done
            if [[ -z "$path" || "$path" == /* || "$path" == [A-Za-z]:* || "$path" == ".." || "$path" == ../* || "$path" == */.. || "$path" == */../* ]]; then
              return 1
            fi
            printf '%s\n' "$path"
          }

          is_test_release_path() {
            local lower="${1,,}"
            [[ "$lower" == test/* || "$lower" == tests/* || "$lower" == */test/* || "$lower" == */tests/* ]]
          }

          while IFS= read -r skill_dir; do
            json_path="${skill_dir}/skill.json"
            md_path="${skill_dir}/SKILL.md"

            head_json_version=""
            head_has_json=false
            if [ -f "${json_path}" ]; then
              head_has_json=true
              head_json_version="$(jq -r '.version // empty' "${json_path}" 2>/dev/null || true)"
            fi

            head_md_version=""
            head_has_md=false
            if [ -f "${md_path}" ]; then
              head_has_md=true
              head_md_version="$(get_md_version "${md_path}")"
            fi

            base_json_version=""
            base_has_json=false
            if git cat-file -e "${BASE_SHA}:${json_path}" 2>/dev/null; then
              base_has_json=true
              base_json_version="$(git show "${BASE_SHA}:${json_path}" | jq -r '.version // empty' 2>/dev/null || true)"
            fi

            base_md_version=""
            base_has_md=false
            if git cat-file -e "${BASE_SHA}:${md_path}" 2>/dev/null; then
              base_has_md=true
              base_md_version="$(get_md_version_from_git "${BASE_SHA}" "${md_path}")"
            fi

            if [ "${base_has_json}" = "true" ] && [ "${base_has_md}" = "true" ] && [ "${head_has_json}" != "true" ] && [ "${head_has_md}" != "true" ]; then
              echo "Skill ${skill_dir} was removed in this PR; skipping dry-run."
              continue
            fi

            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 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))
              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_assets="${out_root}/release-assets"
            mkdir -p "${out_assets}"

            # --- Sign advisory artifacts if present (dry-run with test key) ---
            if ! sign_advisory_artifacts "${skill_dir}"; then
              failures=$((failures + 1))
              echo "::endgroup::"
              continue
            fi

            # --- Stage SBOM files preserving directory structure ---
            staging_dir="$(mktemp -d)"
            inner_dir="${staging_dir}/${skill_name}"
            mkdir -p "${inner_dir}"
            temp_sbom_file="$(mktemp)"
            jq -r '.sbom.files[].path' "${json_path}" > "${temp_sbom_file}"

            while IFS= read -r raw_file; do
              [ -z "${raw_file}" ] && continue
              if ! file="$(normalize_release_path "${raw_file}")"; then
                echo "::error file=${json_path}::SBOM references unsafe file path: ${raw_file}"
                failures=$((failures + 1))
                continue
              fi
              if is_test_release_path "${file}"; then
                echo "  [Dry-run] Skipping test-only release file: ${file}"
                continue
              fi
              full_path="${skill_dir}/${file}"
              if [ -f "${full_path}" ]; then
                mkdir -p "${inner_dir}/$(dirname "${file}")"
                cp "${full_path}" "${inner_dir}/${file}"
              else
                echo "::error file=${json_path}::SBOM references missing file: ${file}"
                failures=$((failures + 1))
              fi
            done < "${temp_sbom_file}"

            cp "${json_path}" "${inner_dir}/skill.json"

            # --- Remove test-only artifacts from staging (don't include in release zip) ---
            # The test signatures/keys were needed for SBOM validation but shouldn't ship
            if [ -d "${inner_dir}/advisories" ]; then
              rm -f "${inner_dir}/advisories/feed.json.sig"
              rm -f "${inner_dir}/advisories/checksums.json"
              rm -f "${inner_dir}/advisories/checksums.json.sig"
              rm -f "${inner_dir}/advisories/feed-signing-public.pem"
              echo "  [Dry-run] Removed test signatures from release staging"
            fi

            # --- Verify staged runtime import closure before archiving ---
            python3 scripts/ci/verify_skill_release_import_closure.py "${inner_dir}"

            # --- 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
              echo "::error::Dry-run release archive contains test-only files: ${zip_name}"
              unzip -Z1 "${out_assets}/${zip_name}" | grep -Ei '(^|/)(test|tests)/' || true
              failures=$((failures + 1))
            fi

            # --- Clean up test artifacts from source directory ---
            if [ -d "${skill_dir}/advisories" ]; then
              rm -f "${skill_dir}/advisories/feed.json.sig"
              rm -f "${skill_dir}/advisories/checksums.json"
              rm -f "${skill_dir}/advisories/checksums.json.sig"
              rm -f "${skill_dir}/advisories/feed-signing-public.pem"
            fi

            # --- Generate checksums.json via jq ---
            files_json="{}"
            while IFS= read -r raw_file; do
              [ -z "${raw_file}" ] && continue
              if ! file="$(normalize_release_path "${raw_file}")"; then
                continue
              fi
              if is_test_release_path "${file}"; then
                continue
              fi
              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}")"
                files_json="$(echo "${files_json}" | jq \
                  --arg key "${file}" \
                  --arg sha "${sha256}" \
                  --argjson sz "${size}" \
                  '. + {($key): {sha256: $sha, size: $sz, path: $key}}')"
              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}")"
            files_json="$(echo "${files_json}" | jq \
              --arg sha "${skill_json_sha}" \
              --argjson sz "${skill_json_size}" \
              '. + {"skill.json": {sha256: $sha, size: $sz}}')"

            zip_sha="$(sha256sum "${out_assets}/${zip_name}" | awk '{print $1}')"
            zip_size="$(stat -c%s "${out_assets}/${zip_name}" 2>/dev/null || stat -f%z "${out_assets}/${zip_name}")"

            jq -n \
              --arg skill "${skill_name}" \
              --arg version "${version}" \
              --arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
              --arg repo "${{ github.repository }}" \
              --arg tag "${tag}" \
              --arg zip_file "${zip_name}" \
              --arg zip_sha "${zip_sha}" \
              --argjson zip_size "${zip_size}" \
              --arg zip_url "https://github.com/${{ github.repository }}/releases/download/${tag}/${zip_name}" \
              --argjson files "${files_json}" \
              '{
                skill: $skill,
                version: $version,
                generated_at: $generated,
                repository: $repo,
                tag: $tag,
                archive: {
                  filename: $zip_file,
                  sha256: $zip_sha,
                  size: $zip_size,
                  url: $zip_url
                },
                files: $files
              }' > "${out_assets}/checksums.json"

            if ! jq -e . "${out_assets}/checksums.json" >/dev/null 2>&1; then
              echo "::error::Generated checksums.json is invalid JSON."
              failures=$((failures + 1))
              rm -rf "${staging_dir}"
              echo "::endgroup::"
              continue
            fi

            # --- Copy skill.json and root-level docs alongside the zip ---
            cp "${json_path}" "${out_assets}/skill.json"
            if [ -f "${skill_dir}/SKILL.md" ]; then
              cp "${skill_dir}/SKILL.md" "${out_assets}/SKILL.md"
            fi
            if [ -f "${skill_dir}/README.md" ]; then
              cp "${skill_dir}/README.md" "${out_assets}/README.md"
            fi

            rm -rf "${staging_dir}"

            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 version bumps detected in changed skill metadata files."
            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
    permissions:
      contents: write
    outputs:
      skill_name: ${{ steps.parse.outputs.skill_name }}
      version: ${{ steps.parse.outputs.version }}
      skill_path: ${{ steps.parse.outputs.skill_path }}
      publishable: ${{ steps.publishable.outputs.publishable }}
      openclaw_skill: ${{ steps.publishable.outputs.openclaw_skill }}
      publish_clawhub: ${{ steps.publishable.outputs.publish_clawhub }}
    steps:
      - name: Parse tag
        id: parse
        run: |
          TAG="${{ github.ref_name }}"
          # Extract skill name (everything before -v)
          SKILL_NAME="${TAG%-v*}"
          # Extract version (everything after -v)
          VERSION="${TAG#*-v}"

          echo "skill_name=${SKILL_NAME}" >> $GITHUB_OUTPUT
          echo "version=${VERSION}" >> $GITHUB_OUTPUT
          echo "skill_path=skills/${SKILL_NAME}" >> $GITHUB_OUTPUT

          echo "Parsed tag: skill=${SKILL_NAME}, version=${VERSION}"

      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Verify signing key consistency (repo + docs)
        run: ./scripts/ci/verify_signing_key_consistency.sh

      - name: Validate skill exists
        run: |
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          if [ ! -d "$SKILL_PATH" ]; then
            echo "Error: Skill directory not found: $SKILL_PATH"
            exit 1
          fi
          if [ ! -f "$SKILL_PATH/skill.json" ]; then
            echo "Error: skill.json not found in $SKILL_PATH"
            exit 1
          fi
          echo "Skill validated: $SKILL_PATH"

      - name: Validate version match
        run: |
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          TAG_VERSION="${{ steps.parse.outputs.version }}"

          # Extract version from skill.json
          JSON_VERSION=$(jq -r '.version' "$SKILL_PATH/skill.json")

          if [ "$TAG_VERSION" != "$JSON_VERSION" ]; then
            echo "::error::Version mismatch! Tag version ($TAG_VERSION) != skill.json version ($JSON_VERSION)"
            echo "Please ensure the version in $SKILL_PATH/skill.json matches your tag."
            exit 1
          fi

          echo "Version validated: $TAG_VERSION"

      - name: Validate SKILL.md frontmatter version
        run: |
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          TAG_VERSION="${{ steps.parse.outputs.version }}"

          # Check if SKILL.md exists
          if [ -f "$SKILL_PATH/SKILL.md" ]; then
            # Extract version from YAML frontmatter
            MD_VERSION=$(grep -m 1 "^version:" "$SKILL_PATH/SKILL.md" | sed 's/version: *//' | tr -d '\r')

            if [ -z "$MD_VERSION" ]; then
              echo "::warning::No version found in $SKILL_PATH/SKILL.md frontmatter"
            elif [ "$TAG_VERSION" != "$MD_VERSION" ]; then
              echo "::error::Version mismatch! Tag version ($TAG_VERSION) != SKILL.md version ($MD_VERSION)"
              echo "Please ensure the version in $SKILL_PATH/SKILL.md frontmatter matches your tag."
              exit 1
            else
              echo "SKILL.md version validated: $MD_VERSION"
            fi
          else
            echo "::error::Missing required SKILL.md: $SKILL_PATH/SKILL.md"
            exit 1
          fi

      - name: Detect publishability and install defaults
        id: publishable
        run: |
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          INTERNAL=$(jq -r 'if (.openclaw | type) == "object" then (.openclaw.internal // false) else false end' "$SKILL_PATH/skill.json")

          OPENCLAW_SKILL=false
          if jq -e '(.openclaw | type == "object") and ((.openclaw | length) > 0)' "$SKILL_PATH/skill.json" >/dev/null; then
            OPENCLAW_SKILL=true
          fi

          PUBLISHABLE=true
          if [ "$INTERNAL" = "true" ]; then
            PUBLISHABLE=false
            echo "Skill marked internal=true; will skip ClawHub publishing."
          fi

          PUBLISH_CLAWHUB=false
          if [ "$PUBLISHABLE" = "true" ]; then
            PUBLISH_CLAWHUB=true
          fi

          echo "internal=${INTERNAL}" >> $GITHUB_OUTPUT
          echo "openclaw_skill=${OPENCLAW_SKILL}" >> $GITHUB_OUTPUT
          echo "publish_clawhub=${PUBLISH_CLAWHUB}" >> $GITHUB_OUTPUT
          echo "publishable=${PUBLISHABLE}" >> $GITHUB_OUTPUT

      - name: Setup Node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: 20

      - name: Sign embedded advisory feed and verify
        if: hashFiles(format('skills/{0}/advisories/feed.json', steps.parse.outputs.skill_name)) != ''
        uses: ./.github/actions/sign-and-verify
        with:
          private_key: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY }}
          private_key_passphrase: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE }}
          input_file: skills/${{ steps.parse.outputs.skill_name }}/advisories/feed.json
          signature_file: skills/${{ steps.parse.outputs.skill_name }}/advisories/feed.json.sig
          public_key_output: skills/${{ steps.parse.outputs.skill_name }}/advisories/feed-signing-public.pem

      - name: Generate embedded advisory checksums manifest
        if: hashFiles(format('skills/{0}/advisories/feed.json', steps.parse.outputs.skill_name)) != ''
        run: |
          set -euo pipefail
          ADVISORY_DIR="${{ steps.parse.outputs.skill_path }}/advisories"
          FEED_SHA=$(sha256sum "$ADVISORY_DIR/feed.json" | awk '{print $1}')
          FEED_SIZE=$(stat -c%s "$ADVISORY_DIR/feed.json" 2>/dev/null || stat -f%z "$ADVISORY_DIR/feed.json")
          FEED_SIG_SHA=$(sha256sum "$ADVISORY_DIR/feed.json.sig" | awk '{print $1}')
          FEED_SIG_SIZE=$(stat -c%s "$ADVISORY_DIR/feed.json.sig" 2>/dev/null || stat -f%z "$ADVISORY_DIR/feed.json.sig")

          jq -n \
            --arg schema_version "1" \
            --arg algorithm "sha256" \
            --arg version "${{ steps.parse.outputs.version }}" \
            --arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            --arg repo "${{ github.repository }}" \
            --arg feed_sha "$FEED_SHA" \
            --argjson feed_size "$FEED_SIZE" \
            --arg feed_sig_sha "$FEED_SIG_SHA" \
            --argjson feed_sig_size "$FEED_SIG_SIZE" \
            '{
              schema_version: $schema_version,
              algorithm: $algorithm,
              version: $version,
              generated_at: $generated,
              repository: $repo,
              files: {
                "advisories/feed.json": {
                  sha256: $feed_sha,
                  size: $feed_size,
                  path: "advisories/feed.json"
                },
                "advisories/feed.json.sig": {
                  sha256: $feed_sig_sha,
                  size: $feed_sig_size,
                  path: "advisories/feed.json.sig"
                }
              }
            }' > "$ADVISORY_DIR/checksums.json"

          echo "Generated $ADVISORY_DIR/checksums.json"
          jq . "$ADVISORY_DIR/checksums.json"

      - name: Sign embedded advisory checksums and verify
        if: hashFiles(format('skills/{0}/advisories/feed.json', steps.parse.outputs.skill_name)) != ''
        uses: ./.github/actions/sign-and-verify
        with:
          private_key: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY }}
          private_key_passphrase: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE }}
          input_file: skills/${{ steps.parse.outputs.skill_name }}/advisories/checksums.json
          signature_file: skills/${{ steps.parse.outputs.skill_name }}/advisories/checksums.json.sig

      - name: Show embedded advisory signing outputs
        if: hashFiles(format('skills/{0}/advisories/feed.json', steps.parse.outputs.skill_name)) != ''
        run: |
          ADVISORY_DIR="${{ steps.parse.outputs.skill_path }}/advisories"
          echo "Successfully signed embedded advisory artifacts:"
          ls -la "$ADVISORY_DIR"

      - name: Package release assets
        run: |
          set -euo pipefail
          SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          VERSION="${{ steps.parse.outputs.version }}"
          TAG="${{ github.ref_name }}"

          mkdir -p release-assets

          normalize_release_path() {
            local path="$1"
            path="${path//\\//}"
            while [[ "$path" == ./* ]]; do
              path="${path#./}"
            done
            while [[ "$path" == *//* ]]; do
              path="${path//\/\//\/}"
            done
            if [[ -z "$path" || "$path" == /* || "$path" == [A-Za-z]:* || "$path" == ".." || "$path" == ../* || "$path" == */.. || "$path" == */../* ]]; then
              return 1
            fi
            printf '%s\n' "$path"
          }

          is_test_release_path() {
            local lower="${1,,}"
            [[ "$lower" == test/* || "$lower" == tests/* || "$lower" == */test/* || "$lower" == */tests/* ]]
          }

          # --- Stage SBOM files preserving directory structure ---
          STAGING_DIR="$(mktemp -d)"
          INNER_DIR="$STAGING_DIR/$SKILL_NAME"
          mkdir -p "$INNER_DIR"
          TEMPFILE="$(mktemp)"
          jq -r '.sbom.files[].path' "$SKILL_PATH/skill.json" > "$TEMPFILE"

          while IFS= read -r raw_file; do
            [ -z "$raw_file" ] && continue
            if ! file="$(normalize_release_path "$raw_file")"; then
              echo "::error file=$SKILL_PATH/skill.json::SBOM references unsafe file path: $raw_file"
              exit 1
            fi
            if is_test_release_path "$file"; then
              echo "Skipping test-only release file: $file"
              continue
            fi
            FULL_PATH="$SKILL_PATH/$file"
            if [ -f "$FULL_PATH" ]; then
              mkdir -p "$INNER_DIR/$(dirname "$file")"
              cp "$FULL_PATH" "$INNER_DIR/$file"
            else
              echo "::error file=$SKILL_PATH/skill.json::SBOM references missing file: $file"
              exit 1
            fi
          done < "$TEMPFILE"

          cp "$SKILL_PATH/skill.json" "$INNER_DIR/skill.json"

          # --- Verify staged runtime import closure before archiving ---
          python3 scripts/ci/verify_skill_release_import_closure.py "$INNER_DIR"

          # --- 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
            echo "::error::Release archive contains test-only files: $ZIP_NAME"
            unzip -Z1 "release-assets/$ZIP_NAME" | grep -Ei '(^|/)(test|tests)/' || true
            exit 1
          fi

          # --- Generate checksums.json via jq ---
          FILES_JSON="{}"
          while IFS= read -r raw_file; do
            [ -z "$raw_file" ] && continue
            if ! file="$(normalize_release_path "$raw_file")"; then
              continue
            fi
            if is_test_release_path "$file"; then
              continue
            fi
            FULL_PATH="$SKILL_PATH/$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")
              FILES_JSON="$(echo "$FILES_JSON" | jq \
                --arg key "$file" \
                --arg sha "$SHA256" \
                --argjson sz "$SIZE" \
                '. + {($key): {sha256: $sha, size: $sz, path: $key}}')"
            fi
          done < "$TEMPFILE"

          rm -f "$TEMPFILE"

          SKILL_JSON_SHA=$(sha256sum "$SKILL_PATH/skill.json" | awk '{print $1}')
          SKILL_JSON_SIZE=$(stat -c%s "$SKILL_PATH/skill.json" 2>/dev/null || stat -f%z "$SKILL_PATH/skill.json")
          FILES_JSON="$(echo "$FILES_JSON" | jq \
            --arg sha "$SKILL_JSON_SHA" \
            --argjson sz "$SKILL_JSON_SIZE" \
            '. + {"skill.json": {sha256: $sha, size: $sz}}')"

          ZIP_SHA=$(sha256sum "release-assets/$ZIP_NAME" | awk '{print $1}')
          ZIP_SIZE=$(stat -c%s "release-assets/$ZIP_NAME" 2>/dev/null || stat -f%z "release-assets/$ZIP_NAME")

          jq -n \
            --arg skill "$SKILL_NAME" \
            --arg version "$VERSION" \
            --arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            --arg repo "${{ github.repository }}" \
            --arg tag "$TAG" \
            --arg zip_file "$ZIP_NAME" \
            --arg zip_sha "$ZIP_SHA" \
            --argjson zip_size "$ZIP_SIZE" \
            --arg zip_url "https://github.com/${{ github.repository }}/releases/download/$TAG/$ZIP_NAME" \
            --argjson files "$FILES_JSON" \
            '{
              skill: $skill,
              version: $version,
              generated_at: $generated,
              repository: $repo,
              tag: $tag,
              archive: {
                filename: $zip_file,
                sha256: $zip_sha,
                size: $zip_size,
                url: $zip_url
              },
              files: $files
            }' > "release-assets/checksums.json"

          # --- Copy skill.json and root-level docs alongside the zip ---
          cp "$SKILL_PATH/skill.json" release-assets/skill.json
          if [ -f "$SKILL_PATH/SKILL.md" ]; then
            cp "$SKILL_PATH/SKILL.md" release-assets/
          fi
          if [ -f "$SKILL_PATH/README.md" ]; then
            cp "$SKILL_PATH/README.md" release-assets/
          fi

          rm -rf "$STAGING_DIR"

          echo "=== checksums.json ==="
          jq . "release-assets/checksums.json"
          echo ""
          echo "=== Release assets ==="
          ls -la release-assets/

      - name: Sign checksums and verify
        uses: ./.github/actions/sign-and-verify
        with:
          private_key: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY }}
          private_key_passphrase: ${{ secrets.CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE }}
          input_file: release-assets/checksums.json
          signature_file: release-assets/checksums.sig
          public_key_output: release-assets/signing-public.pem

      - name: Verify generated release signing key matches canonical key
        run: |
          set -euo pipefail
          CANONICAL_FPR=$(openssl pkey -pubin -in clawsec-signing-public.pem -outform DER | sha256sum | awk '{print $1}')
          GENERATED_FPR=$(openssl pkey -pubin -in release-assets/signing-public.pem -outform DER | sha256sum | awk '{print $1}')
          echo "Canonical key fingerprint: $CANONICAL_FPR"
          echo "Generated key fingerprint: $GENERATED_FPR"
          if [ "$CANONICAL_FPR" != "$GENERATED_FPR" ]; then
            echo "::error::release-assets/signing-public.pem fingerprint mismatch vs clawsec-signing-public.pem"
            exit 1
          fi

      - name: Show signed release assets
        run: |
          echo "Signed and verified release-assets/checksums.json"
          ls -la release-assets/

      - name: Extract changelog entry
        id: changelog
        run: |
          set -euo pipefail
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          VERSION="${{ steps.parse.outputs.version }}"

          if [ ! -f "$SKILL_PATH/CHANGELOG.md" ]; then
            echo "::error::Missing required changelog file: $SKILL_PATH/CHANGELOG.md"
            exit 1
          fi

          # Extract the changelog section for this version
          # Pattern: ## [VERSION] - DATE ... until next ## [, separator (---), or any other ## heading
          CHANGELOG_ENTRY=$(awk -v version="$VERSION" '
            BEGIN { in_section = 0; found = 0 }
            $0 ~ ("^## \\[" version "\\]") { in_section = 1; found = 1; next }
            in_section && found && /^---/ { exit }
            in_section && found && /^## / { exit }
            in_section { print }
          ' "$SKILL_PATH/CHANGELOG.md" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}')

          if [ -z "$CHANGELOG_ENTRY" ]; then
            echo "::error::No changelog entry found for version $VERSION in $SKILL_PATH/CHANGELOG.md"
            echo "::error::Expected heading format: ## [$VERSION] - YYYY-MM-DD"
            exit 1
          fi

          echo "Found changelog entry for version $VERSION"
          # Use multiline output format for GitHub Actions
          {
            echo "changelog<<EOF"
            echo "$CHANGELOG_ENTRY"
            echo "EOF"
          } >> $GITHUB_OUTPUT

      - name: Build quick install instructions
        id: install
        run: |
          set -euo pipefail
          SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
          VERSION="${{ steps.parse.outputs.version }}"
          REPO="${{ github.repository }}"
          TAG="${{ github.ref_name }}"

          {
            echo "quick_install<<INSTALL_EOF"

            if [ "${{ steps.publishable.outputs.publish_clawhub }}" = "true" ] && [ "${{ steps.publishable.outputs.openclaw_skill }}" = "true" ]; then
              cat <<EOF
          ### Quick Install

          **Via ClawHub (recommended):**
          \`\`\`bash
          npx clawhub@latest install ${SKILL_NAME}
          \`\`\`

          **If you already have \`clawsec-suite\` installed:**
          Ask your agent to pull \`${SKILL_NAME}\` from the ClawSec catalog and it will handle setup and verification automatically.
          EOF
            else
              cat <<EOF
          ### Quick Install

          **GitHub release artifact (recommended):**
          Ask your agent to read the published skill instructions from this GitHub release and follow them:

          https://github.com/${REPO}/releases/download/${TAG}/SKILL.md

          Or download them locally:
          \`\`\`bash
          curl -sLO https://github.com/${REPO}/releases/download/${TAG}/SKILL.md
          \`\`\`
          EOF
            fi

            cat <<EOF

          **Manual download with verification:**
          \`\`\`bash
          # 1. Download the release archive, checksums, and signing material
          curl -sLO https://github.com/${REPO}/releases/download/${TAG}/${SKILL_NAME}-v${VERSION}.zip
          curl -sLO https://github.com/${REPO}/releases/download/${TAG}/checksums.json
          curl -sLO https://github.com/${REPO}/releases/download/${TAG}/checksums.sig
          curl -sLO https://github.com/${REPO}/releases/download/${TAG}/signing-public.pem

          # 2. Verify the checksums manifest signature (Ed25519)
          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

          # 3. Verify archive checksum from the signed manifest
          echo "\$(jq -r '.archive.sha256' checksums.json)  ${SKILL_NAME}-v${VERSION}.zip" | sha256sum -c

          # 4. Extract (creates ${SKILL_NAME}/ directory)
          unzip ${SKILL_NAME}-v${VERSION}.zip
          \`\`\`
          EOF

            echo "INSTALL_EOF"
          } >> "$GITHUB_OUTPUT"

      - 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*
          draft: false
          prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Delete superseded releases
        run: |
          SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
          CURRENT_VERSION="${{ steps.parse.outputs.version }}"

          # Extract major version from current release
          CURRENT_MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)

          echo "Current release: $SKILL_NAME v$CURRENT_VERSION (major: $CURRENT_MAJOR)"

          # List all releases for this skill
          gh release list --limit 100 | grep "^${SKILL_NAME} " | while read -r line; do
            # Extract tag from release list (3rd tab-delimited column)
            TAG=$(echo "$line" | awk -F'\t' '{print $3}')
            VERSION="${TAG#${SKILL_NAME}-v}"

            # Skip current version
            if [ "$VERSION" = "$CURRENT_VERSION" ]; then
              continue
            fi

            # Extract major version
            RELEASE_MAJOR=$(echo "$VERSION" | cut -d. -f1)

            # Only delete if same major version (preserve old majors for backwards compat)
            if [ "$RELEASE_MAJOR" = "$CURRENT_MAJOR" ]; then
              echo "Deleting $TAG (superseded by v$CURRENT_VERSION)"
              gh release delete "$TAG" --yes || echo "Warning: Could not delete $TAG"
            else
              echo "Keeping $TAG as latest for major version $RELEASE_MAJOR"
            fi
          done

          echo "Superseded release cleanup complete"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  publish-clawhub:
    # Separate job for ClawHub publishing - runs after GitHub release
    # Non-blocking: if this fails, the release is still successful
    # Retriggerable: can be manually triggered for failed publishes
    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
    needs: release-tag
    runs-on: ubuntu-latest
    continue-on-error: true
    permissions:
      contents: read
    env:
      CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
    steps:
      - name: Check if publishable
        if: needs.release-tag.outputs.publish_clawhub != 'true'
        run: |
          echo "Skill is not eligible for ClawHub publishing; skipping"
          exit 0

      - name: Checkout
        if: needs.release-tag.outputs.publish_clawhub == 'true'
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Setup Node
        if: needs.release-tag.outputs.publish_clawhub == 'true'
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: 20

      - name: Install clawhub CLI
        if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
        run: npm install -g clawhub@${CLAWHUB_CLI_VERSION}

      - name: Patch clawhub publish payload workaround
        # Temporary: clawhub@0.7.0 publish payload is missing acceptLicenseTerms.
        if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
        run: |
          node <<'NODE'
          const { execSync } = require("node:child_process");
          const fs = require("node:fs");
          const path = require("node:path");

          const npmRoot = execSync("npm root -g", { encoding: "utf8" }).trim();
          const publishScriptPath = path.join(
            npmRoot,
            "clawhub",
            "dist",
            "cli",
            "commands",
            "publish.js"
          );

          if (!fs.existsSync(publishScriptPath)) {
            throw new Error(`clawhub publish script not found: ${publishScriptPath}`);
          }

          const original = fs.readFileSync(publishScriptPath, "utf8");
          if (original.includes("acceptLicenseTerms: true")) {
            console.log(`[patch-clawhub] Already patched: ${publishScriptPath}`);
            process.exit(0);
          }

          const payloadPattern = /changelog,\r?\n(\s*)tags,/;
          if (!payloadPattern.test(original)) {
            throw new Error(
              `[patch-clawhub] Could not find expected publish payload pattern in ${publishScriptPath}`
            );
          }

          const patched = original.replace(
            payloadPattern,
            (_, indent) => `changelog,\n${indent}acceptLicenseTerms: true,\n${indent}tags,`
          );
          fs.writeFileSync(publishScriptPath, patched, "utf8");
          console.log(`[patch-clawhub] Patched: ${publishScriptPath}`);
          NODE

      - name: Login to ClawHub
        if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
        run: |
          set -euo pipefail
          SITE=${CLAWHUB_SITE:-https://clawhub.ai}
          REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
          export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json"
          mkdir -p "$(dirname "$CLAWHUB_CONFIG_PATH")"
          CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
            clawhub login --token "$CLAWHUB_TOKEN" --site "$SITE" --no-input

      - name: Guard duplicate ClawHub version
        if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
        run: |
          set -euo pipefail
          SITE=${CLAWHUB_SITE:-https://clawhub.ai}
          REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
          SKILL_NAME="${{ needs.release-tag.outputs.skill_name }}"
          VERSION="${{ needs.release-tag.outputs.version }}"
          export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json"

          set +e
          CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
            clawhub inspect "$SKILL_NAME" --version "$VERSION" --json \
            > /tmp/clawhub-existing-version.json 2> /tmp/clawhub-existing-version.err
          STATUS=$?
          set -e

          if [ "$STATUS" -eq 0 ]; then
            echo "::error::ClawHub already contains ${SKILL_NAME}@${VERSION}. Bump the version before tagging."
            exit 1
          fi

          if grep -Eqi "Version not found|Skill not found" /tmp/clawhub-existing-version.err; then
            echo "No existing ${SKILL_NAME}@${VERSION} detected in ClawHub. Proceeding."
          else
            echo "::error::Failed to verify ClawHub version precondition."
            cat /tmp/clawhub-existing-version.err
            exit 1
          fi

      - name: Publish to ClawHub
        if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
        run: |
          set -euo pipefail
          SITE=${CLAWHUB_SITE:-https://clawhub.ai}
          REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
          SKILL_PATH="${{ needs.release-tag.outputs.skill_path }}"
          SKILL_NAME="${{ needs.release-tag.outputs.skill_name }}"
          VERSION="${{ needs.release-tag.outputs.version }}"
          NAME=$(jq -r '.name' "$SKILL_PATH/skill.json")
          CHANGELOG="Release ${VERSION} via CI"

          export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json"

          if ! CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
            clawhub publish "$SKILL_PATH" \
              --slug "$SKILL_NAME" \
              --name "$NAME" \
              --version "$VERSION" \
              --changelog "$CHANGELOG" \
              --tags "latest" \
              --no-input 2>&1 | tee /tmp/clawhub-publish.log; then
            echo "::error::ClawHub publish failed. Check logs above for details."
            exit 1
          fi

          echo "✓ Successfully published $SKILL_NAME@$VERSION to ClawHub"

  republish-clawhub:
    # Manual workflow to republish a specific tag to ClawHub
    # Usage: Go to Actions → Skill Release → Run workflow → Enter tag name
    if: github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    env:
      CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
    steps:
      - name: Parse tag
        id: parse
        run: |
          TAG="${{ github.event.inputs.tag }}"
          # Extract skill name (everything before -v)
          SKILL_NAME="${TAG%-v*}"
          # Extract version (everything after -v)
          VERSION="${TAG#*-v}"

          echo "skill_name=${SKILL_NAME}" >> $GITHUB_OUTPUT
          echo "version=${VERSION}" >> $GITHUB_OUTPUT
          echo "skill_path=skills/${SKILL_NAME}" >> $GITHUB_OUTPUT

          echo "Parsed tag: skill=${SKILL_NAME}, version=${VERSION}"

      - name: Checkout tag
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ github.event.inputs.tag }}

      - name: Validate skill exists
        run: |
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          if [ ! -d "$SKILL_PATH" ]; then
            echo "Error: Skill directory not found: $SKILL_PATH"
            exit 1
          fi
          if [ ! -f "$SKILL_PATH/skill.json" ]; then
            echo "Error: skill.json not found in $SKILL_PATH"
            exit 1
          fi
          echo "Skill validated: $SKILL_PATH"

      - name: Check if publishable
        id: publishable
        run: |
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          INTERNAL=$(jq -r 'if (.openclaw | type) == "object" then (.openclaw.internal // false) else false end' "$SKILL_PATH/skill.json")

          if [ "$INTERNAL" = "true" ]; then
            echo "::error::Skill is marked internal and cannot be published to ClawHub"
            exit 1
          fi

          echo "Skill is publishable to ClawHub"

      - name: Setup Node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: 20

      - name: Install clawhub CLI
        run: npm install -g clawhub@${CLAWHUB_CLI_VERSION}

      - name: Patch clawhub publish payload workaround
        # Temporary: clawhub@0.7.0 publish payload is missing acceptLicenseTerms.
        run: |
          node <<'NODE'
          const { execSync } = require("node:child_process");
          const fs = require("node:fs");
          const path = require("node:path");

          const npmRoot = execSync("npm root -g", { encoding: "utf8" }).trim();
          const publishScriptPath = path.join(
            npmRoot,
            "clawhub",
            "dist",
            "cli",
            "commands",
            "publish.js"
          );

          if (!fs.existsSync(publishScriptPath)) {
            throw new Error(`clawhub publish script not found: ${publishScriptPath}`);
          }

          const original = fs.readFileSync(publishScriptPath, "utf8");
          if (original.includes("acceptLicenseTerms: true")) {
            console.log(`[patch-clawhub] Already patched: ${publishScriptPath}`);
            process.exit(0);
          }

          const payloadPattern = /changelog,\r?\n(\s*)tags,/;
          if (!payloadPattern.test(original)) {
            throw new Error(
              `[patch-clawhub] Could not find expected publish payload pattern in ${publishScriptPath}`
            );
          }

          const patched = original.replace(
            payloadPattern,
            (_, indent) => `changelog,\n${indent}acceptLicenseTerms: true,\n${indent}tags,`
          );
          fs.writeFileSync(publishScriptPath, patched, "utf8");
          console.log(`[patch-clawhub] Patched: ${publishScriptPath}`);
          NODE

      - name: Login to ClawHub
        run: |
          set -euo pipefail
          if [ -z "$CLAWHUB_TOKEN" ]; then
            echo "::error::CLAWHUB_TOKEN secret is not set"
            exit 1
          fi

          SITE=${CLAWHUB_SITE:-https://clawhub.ai}
          REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
          export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json"
          mkdir -p "$(dirname "$CLAWHUB_CONFIG_PATH")"
          CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
            clawhub login --token "$CLAWHUB_TOKEN" --site "$SITE" --no-input

      - name: Publish to ClawHub
        run: |
          set -euo pipefail
          SITE=${CLAWHUB_SITE:-https://clawhub.ai}
          REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
          SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
          SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
          VERSION="${{ steps.parse.outputs.version }}"
          NAME=$(jq -r '.name' "$SKILL_PATH/skill.json")
          CHANGELOG="Manual republish of ${VERSION} via workflow_dispatch"

          export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json"

          echo "Publishing $SKILL_NAME@$VERSION to ClawHub..."

          # Publish with idempotent retry handling
          if ! CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
            clawhub publish "$SKILL_PATH" \
              --slug "$SKILL_NAME" \
              --name "$NAME" \
              --version "$VERSION" \
              --changelog "$CHANGELOG" \
              --tags "latest" \
              --no-input 2>&1 | tee /tmp/clawhub-publish.log; then

            # Check if it's a "version already exists" error (which is OK on retry)
            if grep -qi "version already exists" /tmp/clawhub-publish.log; then
              echo "::warning::Version $VERSION already published to ClawHub"
              echo "This is expected if you're retrying a failed publish."
              echo "✓ Skill is available on ClawHub"
              exit 0
            else
              echo "::error::ClawHub publish failed. Check logs above for details."
              cat /tmp/clawhub-publish.log
              exit 1
            fi
          fi

          echo "✓ Successfully published $SKILL_NAME@$VERSION to ClawHub"
