feat(release): sync skill detail tabs and publish 1.2.1
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 26s

Package optional TUTORIAL/DEMO/CHANGELOG into skill ZIPs and sync tutorial_md, demo_md, demo_video_url, and versioned changelog to the marketplace API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 11:48:02 +08:00
parent cf5e74d80a
commit 54b75c641e
3 changed files with 130 additions and 40 deletions

View File

@@ -115,6 +115,12 @@ jobs:
" print('Copied README.md')" \ " print('Copied README.md')" \
'if os.path.isfile(readme_src) and not os.path.isfile(readme_dst):' \ 'if os.path.isfile(readme_src) and not os.path.isfile(readme_dst):' \
" raise RuntimeError('README.md exists in skill root but was not packaged')" \ " raise RuntimeError('README.md exists in skill root but was not packaged')" \
'for market_doc in ("TUTORIAL.md", "DEMO.md", "CHANGELOG.md"):' \
' src = os.path.abspath(market_doc)' \
' dst = os.path.join(PACKAGE, market_doc)' \
' if os.path.isfile(src):' \
' shutil.copy2(src, dst)' \
" print(f'Copied {market_doc}')" \
"copy_dir('references', os.path.join(PACKAGE, 'references'), references_ignore)" \ "copy_dir('references', os.path.join(PACKAGE, 'references'), references_ignore)" \
"copy_dir('assets', os.path.join(PACKAGE, 'assets'), common_ignore)" \ "copy_dir('assets', os.path.join(PACKAGE, 'assets'), common_ignore)" \
"copy_dir('tests', os.path.join(PACKAGE, 'tests'), common_ignore)" \ "copy_dir('tests', os.path.join(PACKAGE, 'tests'), common_ignore)" \
@@ -144,49 +150,119 @@ jobs:
- name: Parse Metadata and Pack - name: Parse Metadata and Pack
id: build_task id: build_task
run: | run: |
python3.12 -c " cat > /tmp/parse_skill_release_metadata.py <<'PY'
import frontmatter, os, json, shutil import frontmatter
post = frontmatter.load('SKILL.md') import json
import os
import re
import shutil
def load_md(path: str):
post = frontmatter.load(path)
meta = post.metadata or {}
body = (post.content or "").strip()
return meta if isinstance(meta, dict) else {}, body
def extract_changelog_for_version(text: str, version: str) -> str | None:
"""Extract Keep-a-Changelog / plain ## version section; None if not found."""
if not text or not version:
return None
ver = re.escape(version.lstrip("vV"))
pattern = re.compile(
rf"^##\s*\[?v?{ver}\]?([^\n]*)$",
re.MULTILINE | re.IGNORECASE,
)
match = pattern.search(text)
if not match:
return None
start = match.end()
next_h2 = re.search(r"^##\s+", text[start:], re.MULTILINE)
end = start + next_h2.start() if next_h2 else len(text)
return text[start:end].strip()
post = frontmatter.load("SKILL.md")
metadata = dict(post.metadata or {}) metadata = dict(post.metadata or {})
skill_readme_md = (post.content or '').strip() skill_readme_md = (post.content or "").strip()
skill_description = metadata.get('description') skill_description = metadata.get("description")
metadata['readme_md'] = skill_readme_md metadata["readme_md"] = skill_readme_md
readme_path = 'README.md'
if os.path.isfile(readme_path): if os.path.isfile("README.md"):
try: try:
readme_post = frontmatter.load(readme_path) rm_meta, body = load_md("README.md")
body = (readme_post.content or '').strip() desc = rm_meta.get("description")
rm_meta = readme_post.metadata or {}
desc = rm_meta.get('description')
if desc is not None and str(desc).strip(): if desc is not None and str(desc).strip():
metadata['description'] = str(desc).strip() metadata["description"] = str(desc).strip()
if body: if body:
metadata['readme_md'] = body metadata["readme_md"] = body
except Exception: except Exception:
pass pass
if not (metadata.get('readme_md') or '').strip():
metadata['readme_md'] = skill_readme_md if not (metadata.get("readme_md") or "").strip():
if skill_description is not None and not str(metadata.get('description', '') or '').strip(): metadata["readme_md"] = skill_readme_md
metadata['description'] = skill_description if skill_description is not None and not str(metadata.get("description", "") or "").strip():
openclaw_meta = metadata.get('metadata', {}).get('openclaw', {}) metadata["description"] = skill_description
slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip()
# Market tabs: only set keys when files exist (backend updates only present keys).
if os.path.isfile("TUTORIAL.md"):
try:
_tm_meta, tutorial_body = load_md("TUTORIAL.md")
metadata["tutorial_md"] = tutorial_body
except Exception as exc:
raise RuntimeError(f"Failed to parse TUTORIAL.md: {exc}") from exc
if os.path.isfile("DEMO.md"):
try:
demo_meta, demo_body = load_md("DEMO.md")
metadata["demo_md"] = demo_body
url = demo_meta.get("demo_video_url")
metadata["demo_video_url"] = (
str(url).strip() if url is not None and str(url).strip() else ""
)
except Exception as exc:
raise RuntimeError(f"Failed to parse DEMO.md: {exc}") from exc
openclaw_meta = metadata.get("metadata", {}).get("openclaw", {})
if not isinstance(openclaw_meta, dict):
openclaw_meta = {}
slug = (
openclaw_meta.get("slug")
or metadata.get("slug")
or metadata.get("name")
or ""
).strip()
if not slug: if not slug:
raise Exception('SKILL.md 缺少 slug/name') raise Exception("SKILL.md 缺少 slug/name")
ref_name = (os.environ.get('GITHUB_REF_NAME') or '').strip()
if not ref_name.startswith('v'): ref_name = (os.environ.get("GITHUB_REF_NAME") or "").strip()
raise Exception(f'非法标签: {ref_name}') if not ref_name.startswith("v"):
version = ref_name.lstrip('v') raise Exception(f"非法标签: {ref_name}")
metadata['version'] = version version = ref_name.lstrip("v")
artifact_platform = (os.environ.get('ARTIFACT_PLATFORM') or 'windows').strip() metadata["version"] = version
zip_base = f'{slug}-{artifact_platform}'
with open(os.environ['GITHUB_OUTPUT'], 'a') as f: if os.path.isfile("CHANGELOG.md"):
f.write(f'slug={slug}\n') try:
f.write(f'version={version}\n') with open("CHANGELOG.md", encoding="utf-8") as fh:
f.write(f'zip_base={zip_base}\n') changelog_text = fh.read()
f.write(f'artifact_platform={artifact_platform}\n') section = extract_changelog_for_version(changelog_text, version)
f.write(f'metadata={json.dumps(metadata, ensure_ascii=False)}\n') if section is not None:
shutil.make_archive(zip_base, 'zip', 'dist/package') metadata["changelog"] = section
" except Exception as exc:
raise RuntimeError(f"Failed to parse CHANGELOG.md: {exc}") from exc
artifact_platform = (os.environ.get("ARTIFACT_PLATFORM") or "windows").strip()
zip_base = f"{slug}-{artifact_platform}"
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
f.write(f"slug={slug}\n")
f.write(f"version={version}\n")
f.write(f"zip_base={zip_base}\n")
f.write(f"artifact_platform={artifact_platform}\n")
f.write(f"metadata={json.dumps(metadata, ensure_ascii=False)}\n")
shutil.make_archive(zip_base, "zip", "dist/package")
PY
python3.12 /tmp/parse_skill_release_metadata.py
- name: Sync Database - name: Sync Database
env: env:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jiangchang-platform-kit" name = "jiangchang-platform-kit"
version = "1.2.0" version = "1.2.1"
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK" description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View File

@@ -39,9 +39,23 @@ def test_workflow_packages_readme_at_root(workflow_text: str) -> None:
def test_workflow_metadata_reads_root_readme(workflow_text: str) -> None: def test_workflow_metadata_reads_root_readme(workflow_text: str) -> None:
assert "readme_path = 'README.md'" in workflow_text assert 'load_md("README.md")' in workflow_text
assert "frontmatter.load(readme_path)" in workflow_text assert 'metadata["readme_md"] = body' in workflow_text
assert "metadata['readme_md'] = body" in workflow_text
def test_workflow_packages_market_docs_when_present(workflow_text: str) -> None:
assert 'for market_doc in ("TUTORIAL.md", "DEMO.md", "CHANGELOG.md"):' in workflow_text
assert "Copied {market_doc}" in workflow_text
def test_workflow_metadata_syncs_tutorial_demo_changelog(workflow_text: str) -> None:
assert 'load_md("TUTORIAL.md")' in workflow_text
assert 'metadata["tutorial_md"] = tutorial_body' in workflow_text
assert 'load_md("DEMO.md")' in workflow_text
assert 'metadata["demo_md"] = demo_body' in workflow_text
assert 'metadata["demo_video_url"]' in workflow_text
assert "extract_changelog_for_version" in workflow_text
assert 'metadata["changelog"] = section' in workflow_text
def test_workflow_does_not_use_references_readme_for_marketplace( def test_workflow_does_not_use_references_readme_for_marketplace(