Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions .github/workflows/skill-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,7 @@ jobs:
publishable: ${{ steps.publishable.outputs.publishable }}
openclaw_skill: ${{ steps.publishable.outputs.openclaw_skill }}
publish_clawhub: ${{ steps.publishable.outputs.publish_clawhub }}
clawhub_slug: ${{ steps.publishable.outputs.clawhub_slug }}
steps:
- name: Parse tag
id: parse
Expand Down Expand Up @@ -942,10 +943,13 @@ jobs:
PUBLISH_CLAWHUB=true
fi

CLAWHUB_SLUG=$(node scripts/ci/resolve_clawhub_slug.mjs "$SKILL_PATH")

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

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
Expand Down Expand Up @@ -1318,6 +1322,7 @@ jobs:
run: |
set -euo pipefail
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
CLAWHUB_SLUG="${{ steps.publishable.outputs.clawhub_slug }}"
VERSION="${{ steps.parse.outputs.version }}"
REPO="${{ github.repository }}"
TAG="${{ github.ref_name }}"
Expand Down Expand Up @@ -1351,7 +1356,7 @@ jobs:

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

**If you already have \`clawsec-suite\` installed:**
Expand Down Expand Up @@ -1567,23 +1572,24 @@ jobs:
SITE=${CLAWHUB_SITE:-https://clawhub.ai}
REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
SKILL_NAME="${{ needs.release-tag.outputs.skill_name }}"
CLAWHUB_SLUG="${{ needs.release-tag.outputs.clawhub_slug }}"
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 \
clawhub inspect "$CLAWHUB_SLUG" --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."
echo "::error::ClawHub already contains ${CLAWHUB_SLUG}@${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."
echo "No existing ${CLAWHUB_SLUG}@${VERSION} detected in ClawHub. Proceeding."
else
echo "::error::Failed to verify ClawHub version precondition."
cat /tmp/clawhub-existing-version.err
Expand All @@ -1598,6 +1604,7 @@ jobs:
REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
SKILL_PATH="${{ needs.release-tag.outputs.skill_path }}"
SKILL_NAME="${{ needs.release-tag.outputs.skill_name }}"
CLAWHUB_SLUG="${{ needs.release-tag.outputs.clawhub_slug }}"
VERSION="${{ needs.release-tag.outputs.version }}"
NAME=$(jq -r '.name' "$SKILL_PATH/skill.json")
CHANGELOG="Release ${VERSION} via CI"
Expand All @@ -1606,7 +1613,7 @@ jobs:

if ! CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
clawhub publish "$SKILL_PATH" \
--slug "$SKILL_NAME" \
--slug "$CLAWHUB_SLUG" \
--name "$NAME" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
Expand All @@ -1616,7 +1623,7 @@ jobs:
exit 1
fi

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

republish-clawhub:
# Manual workflow to republish a specific tag to ClawHub
Expand All @@ -1643,6 +1650,12 @@ jobs:

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

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

- name: Prepare ClawHub slug helper
run: cp scripts/ci/resolve_clawhub_slug.mjs "$RUNNER_TEMP/resolve_clawhub_slug.mjs"

- name: Checkout tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down Expand Up @@ -1672,6 +1685,8 @@ jobs:
exit 1
fi

CLAWHUB_SLUG=$(node "$RUNNER_TEMP/resolve_clawhub_slug.mjs" "$SKILL_PATH")
echo "clawhub_slug=${CLAWHUB_SLUG}" >> $GITHUB_OUTPUT
echo "Skill is publishable to ClawHub"

- name: Setup Node
Expand Down Expand Up @@ -1750,18 +1765,19 @@ jobs:
REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
CLAWHUB_SLUG="${{ steps.publishable.outputs.clawhub_slug }}"
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..."
echo "Publishing $SKILL_NAME@$VERSION to ClawHub as $CLAWHUB_SLUG..."

# Publish with idempotent retry handling
if ! CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
clawhub publish "$SKILL_PATH" \
--slug "$SKILL_NAME" \
--slug "$CLAWHUB_SLUG" \
--name "$NAME" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
Expand Down
49 changes: 2 additions & 47 deletions scripts/ci/generate_skill_release_trust_packet.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
#!/usr/bin/env node
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { installAgentForSkill, PLATFORM_KEYS } from "./skill_platforms.mjs";

const PLATFORM_KEYS = ["openclaw", "nanoclaw", "hermes", "picoclaw"];
const KNOWN_AGENT_TYPES = new Set(["codex", "hermes-agent", "openclaw", "universal"]);
const PLATFORM_AGENT_ALIASES = new Map([["hermes", "hermes-agent"]]);

function usage() {
return [
Expand Down Expand Up @@ -98,50 +97,6 @@ function detectPlatform(skill) {
return skill.platform || "agent-skills";
}

function collectDeclaredPlatforms(skill) {
const platforms = new Set();
if (typeof skill.platform === "string" && skill.platform.trim()) {
platforms.add(skill.platform.trim());
}
if (Array.isArray(skill.platforms)) {
for (const platform of skill.platforms) {
if (typeof platform === "string" && platform.trim()) {
platforms.add(platform.trim());
}
}
}
for (const key of PLATFORM_KEYS) {
if (skill[key] && typeof skill[key] === "object") {
platforms.add(key);
}
}
return [...platforms];
}

function installAgentForSkill(skill) {
const platforms = collectDeclaredPlatforms(skill);
if (platforms.length === 0) {
return "openclaw";
}

const matchedAgents = new Set();
let allPlatformsMatched = true;
for (const platform of platforms) {
const candidate = PLATFORM_AGENT_ALIASES.get(platform) || platform;
if (KNOWN_AGENT_TYPES.has(candidate)) {
matchedAgents.add(candidate);
} else {
allPlatformsMatched = false;
}
}

if (allPlatformsMatched && matchedAgents.size === 1) {
return [...matchedAgents][0];
}

return "openclaw";
}

function platformMetadata(skill, platform) {
const direct = skill[platform];
return direct && typeof direct === "object" ? direct : {};
Expand Down Expand Up @@ -309,7 +264,7 @@ function buildInstallDoc({ skill, repository, tag, sourceRef }) {
const refSuffix = sourceRef && sourceRef !== "main" ? `#${sourceRef}` : "";
const source = `${repository}${refSuffix}`;
const releaseUrl = tag ? `https://github.com/${repository}/releases/tag/${tag}` : `https://github.com/${repository}`;
const agent = installAgentForSkill(skill);
const agent = installAgentForSkill(skill, KNOWN_AGENT_TYPES);

return `# Install and Update ${skill.name}

Expand Down
79 changes: 79 additions & 0 deletions scripts/ci/resolve_clawhub_slug.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env node
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { collectDeclaredPlatforms, PLATFORM_KEYS } from "./skill_platforms.mjs";

const EXPLICIT_SLUGS = new Map([
["openclaw-traffic-guardian", "clawsec-openclaw-traffic-guardian"],
["openclaw-audit-watchdog", "clawsec-openclaw-audit-watchdog"],
["soul-guardian", "clawsec-openclaw-soul-guardian"],
["hermes-attestation-guardian", "clawsec-hermes-attestation-guardian"],
["hermes-traffic-guardian", "clawsec-hermes-traffic-guardian"],
["nanoclaw-traffic-guardian", "clawsec-nanoclaw-traffic-guardian"],
["picoclaw-security-guardian", "clawsec-picoclaw-security-guardian"],
["picoclaw-self-pen-testing", "clawsec-picoclaw-self-pen-testing"],
["picoclaw-traffic-guardian", "clawsec-picoclaw-traffic-guardian"],
["clawtributor", "clawsec-clawtributor"],
]);

function usage() {
return [
"Usage: node scripts/ci/resolve_clawhub_slug.mjs <skill-dir-or-name>",
"",
"Prints the ClawHub slug for a skill without changing the GitHub release tag or skill package name.",
].join("\n");
}

function loadSkill(input) {
const skillJsonPath = existsSync(path.join(input, "skill.json")) ? path.join(input, "skill.json") : null;
if (!skillJsonPath) {
return { name: input, platforms: [] };
}

const skill = JSON.parse(readFileSync(skillJsonPath, "utf8"));
if (!skill.name || typeof skill.name !== "string") {
throw new Error(`${skillJsonPath} missing string field: name`);
}

return { name: skill.name, platforms: collectDeclaredPlatforms(skill) };
}

export function resolveClawHubSlug({ name, platforms = [] }) {
if (!/^[a-z0-9-]+$/.test(name)) {
throw new Error(`Invalid skill name for ClawHub slug mapping: ${name}`);
}

if (name.startsWith("clawsec-")) {
return name;
}

if (EXPLICIT_SLUGS.has(name)) {
return EXPLICIT_SLUGS.get(name);
}

if (PLATFORM_KEYS.some((platform) => name.startsWith(`${platform}-`))) {
return `clawsec-${name}`;
}

const declaredPlatforms = collectDeclaredPlatforms({ platforms });
if (declaredPlatforms.length === 1 && PLATFORM_KEYS.includes(declaredPlatforms[0])) {
return `clawsec-${declaredPlatforms[0]}-${name}`;
}

return `clawsec-${name}`;
Comment thread
davida-ps marked this conversation as resolved.
}

if (import.meta.url === `file://${process.argv[1]}`) {
const input = process.argv[2];
if (!input || input === "--help" || input === "-h") {
console.log(usage());
process.exit(input ? 0 : 1);
}

try {
console.log(resolveClawHubSlug(loadSkill(input)));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
52 changes: 52 additions & 0 deletions scripts/ci/skill_platforms.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export const PLATFORM_KEYS = Object.freeze(["openclaw", "nanoclaw", "hermes", "picoclaw"]);

const PLATFORM_AGENT_ALIASES = new Map([["hermes", "hermes-agent"]]);

function asStringArray(value) {
if (Array.isArray(value)) {
return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
}
if (typeof value === "string" && value.trim()) {
return [value.trim()];
}
return [];
}

export function collectDeclaredPlatforms(skill) {
const platforms = new Set([
...asStringArray(skill.platform),
...asStringArray(skill.platforms),
]);

for (const key of PLATFORM_KEYS) {
if (skill[key] && typeof skill[key] === "object") {
platforms.add(key);
}
}

return [...platforms];
}

export function installAgentForSkill(skill, agentTypes, fallback = "openclaw") {
const platforms = collectDeclaredPlatforms(skill);
if (platforms.length === 0) {
return fallback;
}

const matchedAgents = new Set();
let allPlatformsMatched = true;
for (const platform of platforms) {
const candidate = PLATFORM_AGENT_ALIASES.get(platform) || platform;
if (agentTypes.has(candidate)) {
matchedAgents.add(candidate);
} else {
allPlatformsMatched = false;
}
}

if (allPlatformsMatched && matchedAgents.size === 1) {
return [...matchedAgents][0];
}

return fallback;
}
Loading
Loading