Files
jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml

185 lines
7.7 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:
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: ubuntu-latest
env:
ARTIFACT_PLATFORM: ${{ inputs.artifact_platform }}
PYARMOR_PLATFORM: ${{ inputs.pyarmor_platform }}
PIP_BREAK_SYSTEM_PACKAGES: "1"
# 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: http://120.25.191.12:3000/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 一致。
- name: Setup Tools
run: 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
python -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: |
python -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: |
python -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: |
python -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: |
python -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)
"