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 env(PIP_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/*.py 会漏模块导致运行时 ModuleNotFoundError。 - name: Encrypt Source Code run: | mkdir -p dist/package set -euo pipefail test -d scripts ( cd scripts && pyarmor gen --platform "${PYARMOR_PLATFORM}" -r -O ../dist/package . ) 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 {}) metadata['readme_md'] = (post.content or '').strip() 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) "