Files
jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml
2026-04-19 15:17:57 +08:00

197 lines
8.4 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Reusable Skill Release
on:
workflow_call:
inputs:
runs_on:
description: "Runner label; must match a registered runner (use host runner for pip/python on same machine as Node frontend)"
required: false
type: string
default: ubuntu-latest
artifact_platform:
required: false
type: string
default: windows
pyarmor_platform:
required: false
type: string
default: windows.x86_64
upload_url:
required: false
type: string
default: https://jc2009.com/api/upload
sync_url:
required: false
type: string
default: https://jc2009.com/api/skill/update
prune_url:
required: false
type: string
default: https://jc2009.com/api/artifacts/prune-old-versions
secrets:
# Base64 of official pyarmor-regfile-*.zip — optional; without it, PyArmor trial applies (e.g. large per-file code limits).
PYARMOR_REG_B64:
required: false
jobs:
build-and-deploy:
runs-on: ${{ inputs.runs_on }}
defaults:
run:
shell: bash
env:
ARTIFACT_PLATFORM: ${{ inputs.artifact_platform }}
PYARMOR_PLATFORM: ${{ inputs.pyarmor_platform }}
PIP_BREAK_SYSTEM_PACKAGES: "1"
# Prefer self-built Python 3.12 under /usr/local (Alibaba Cloud Linux host); keep system paths as fallback.
# 显式前缀,避免部分 Runner 未注入 env.PATH 时丢失系统路径
PATH: /usr/local/bin:/usr/local/python3.12/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
# PyArmor 交叉平台加密时会内部执行 pip 安装 pyarmor.cli.core.* 等包;不设则默认走 files.pythonhosted.org国内 CI 易超时。
PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple
PIP_EXTRA_INDEX_URL: https://mirrors.aliyun.com/pypi/simple https://mirrors.cloud.tencent.com/pypi/simple https://mirrors.huaweicloud.com/repository/pypi/simple
PIP_DEFAULT_TIMEOUT: "180"
PIP_TRUSTED_HOST: pypi.tuna.tsinghua.edu.cn mirrors.aliyun.com mirrors.cloud.tencent.com mirrors.huaweicloud.com files.pythonhosted.org pypi.org
steps:
- uses: https://git.jc2009.com/admin/actions-checkout@v4
# Pin PyArmor 8.5.3 — matches desktop bundles; 9.x trial is stricter in CI。
# 镜像由 job envPIP_INDEX_URL / PIP_EXTRA_INDEX_URL统一指定与 Encrypt 步骤中 PyArmor 内部 pip 一致。
# 使用 python3.12 -m pip避免仅存在 python3(3.6) 或裸 pip 不在 PATH 的宿主机/容器。
- name: Setup Tools
run: python3.12 -m pip install "pyarmor==8.5.3" requests python-frontmatter --break-system-packages
- name: Register PyArmor (optional)
env:
PYARMOR_REG_B64: ${{ secrets.PYARMOR_REG_B64 }}
run: |
if [ -z "${PYARMOR_REG_B64}" ]; then
echo "PyArmor: no PYARMOR_REG_B64 secret — trial mode (very large single .py modules may fail to obfuscate)."
else
python3.12 -c "import os,base64,pathlib,subprocess; p=pathlib.Path('/tmp/pyarmor-reg.zip'); p.write_bytes(base64.standard_b64decode(os.environ['PYARMOR_REG_B64'])); subprocess.run(['pyarmor','reg',str(p)],check=True); p.unlink(missing_ok=True)"
fi
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);产物保留与源码一致的 scripts/ 层级,入口为 scripts/main.py。
- name: Encrypt Source Code
run: |
mkdir -p dist/package/scripts
set -euo pipefail
test -d scripts
( cd scripts && pyarmor gen --platform "${PYARMOR_PLATFORM}" -r -O ../dist/package/scripts . )
cp SKILL.md dist/package/
if [ -d references ]; then cp -r references dist/package/; fi
if [ -d assets ]; then cp -r assets dist/package/; fi
- name: Parse Metadata and Pack
id: build_task
run: |
python3.12 -c "
import frontmatter, os, json, shutil
post = frontmatter.load('SKILL.md')
metadata = dict(post.metadata or {})
skill_readme_md = (post.content or '').strip()
skill_description = metadata.get('description')
metadata['readme_md'] = skill_readme_md
readme_path = os.path.join('references', 'README.md')
if os.path.isfile(readme_path):
try:
readme_post = frontmatter.load(readme_path)
body = (readme_post.content or '').strip()
rm_meta = readme_post.metadata or {}
desc = rm_meta.get('description')
if desc is not None and str(desc).strip():
metadata['description'] = str(desc).strip()
if body:
metadata['readme_md'] = body
except Exception:
pass
if not (metadata.get('readme_md') or '').strip():
metadata['readme_md'] = skill_readme_md
if skill_description is not None and not str(metadata.get('description', '') or '').strip():
metadata['description'] = skill_description
openclaw_meta = metadata.get('metadata', {}).get('openclaw', {})
slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip()
if not slug:
raise Exception('SKILL.md 缺少 slug/name')
ref_name = (os.environ.get('GITHUB_REF_NAME') or '').strip()
if not ref_name.startswith('v'):
raise Exception(f'非法标签: {ref_name}')
version = ref_name.lstrip('v')
metadata['version'] = version
artifact_platform = (os.environ.get('ARTIFACT_PLATFORM') or 'windows').strip()
zip_base = f'{slug}-{artifact_platform}'
with open(os.environ['GITHUB_OUTPUT'], 'a') 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')
"
- name: Sync Database
env:
METADATA_JSON: ${{ steps.build_task.outputs.metadata }}
SYNC_URL: ${{ inputs.sync_url }}
run: |
python3.12 -c "
import requests, json, os
metadata = json.loads(os.environ['METADATA_JSON'])
res = requests.post(os.environ['SYNC_URL'], json=metadata)
if res.status_code != 200:
exit(1)
body = res.json()
if body.get('code') != 200:
exit(1)
"
- name: Upload Encrypted ZIP
env:
SLUG: ${{ steps.build_task.outputs.slug }}
VERSION: ${{ steps.build_task.outputs.version }}
ZIP_BASE: ${{ steps.build_task.outputs.zip_base }}
ARTIFACT_PLATFORM: ${{ steps.build_task.outputs.artifact_platform }}
UPLOAD_URL: ${{ inputs.upload_url }}
run: |
python3.12 -c "
import requests, os
slug = os.environ['SLUG']
version = os.environ['VERSION']
zip_path = f\"{os.environ['ZIP_BASE']}.zip\"
payload = {
'plugin_name': slug,
'version': version,
'artifact_type': 'skill',
'artifact_platform': os.environ.get('ARTIFACT_PLATFORM', 'windows'),
}
filename = os.path.basename(zip_path)
with open(zip_path, 'rb') as f:
res = requests.post(os.environ['UPLOAD_URL'], data=payload, files={'file': (filename, f)})
if res.status_code != 200:
exit(1)
body = res.json()
if body.get('code') != 200:
exit(1)
"
- name: Prune Old Versions
env:
SLUG: ${{ steps.build_task.outputs.slug }}
VERSION: ${{ steps.build_task.outputs.version }}
PRUNE_URL: ${{ inputs.prune_url }}
run: |
python3.12 -c "
import requests, os
payload = {
'name': os.environ['SLUG'],
'artifact_type': 'skill',
'keep_count': 1,
'protect_version': os.environ['VERSION']
}
res = requests.post(os.environ['PRUNE_URL'], json=payload)
if res.status_code != 200:
exit(1)
body = res.json()
if body.get('code') != 200:
exit(1)
"