Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71a9ab1700 | |||
| ca117cb5ac | |||
| 892cf837a6 | |||
| a6bbd89350 | |||
| 8778d641ab | |||
| 5037d83b3d | |||
| 8154de452b | |||
| 0bb6707e68 | |||
| 4b15c6d99c | |||
| 4856167682 | |||
| c67487ba16 | |||
| 69702f8ea2 |
115
.github/workflows/reusable-release-frontend.yaml
vendored
Normal file
115
.github/workflows/reusable-release-frontend.yaml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
# Standalone reusable workflow for Node/Vite static sites.
|
||||
# Does not reference reusable-release-skill.yaml; skill workflows remain unchanged.
|
||||
#
|
||||
# Node:请在 Runner 上预装 Node 20+(勿用 actions/setup-node,避免每次从 GitHub 拉包)。
|
||||
# npm:通过 npm_registry 输入使用国内镜像(默认 npmmirror)。
|
||||
# Checkout:不用 actions/checkout(host 模式下 JS Action 仍会进无 Node 的容器);改用 git + github.token。
|
||||
# 重要:act_runner 会把 run: 脚本写到 $GITHUB_WORKSPACE/workflow/*.sh,
|
||||
# 所以 checkout 绝对不能在 workspace 根上 rm -rf ./*,否则会把自己这个脚本一起删了。
|
||||
# 解决办法:克隆到 $GITHUB_WORKSPACE/_src 子目录,后续 step 都在该子目录操作。
|
||||
# 浅拉不要用「裸 SHA」作 fetch 参数(Gitea 上易失败);按分支 clone 再对齐 SHA。
|
||||
|
||||
name: Reusable Frontend Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
deploy_path:
|
||||
description: "Target directory for static files (trailing slash optional)"
|
||||
required: false
|
||||
type: string
|
||||
default: "/www/wwwroot/sandbox/web"
|
||||
build_command:
|
||||
required: false
|
||||
type: string
|
||||
default: "npm run build:jc2009"
|
||||
npm_registry:
|
||||
description: "npm registry (e.g. npmmirror for CN)"
|
||||
required: false
|
||||
type: string
|
||||
default: "https://registry.npmmirror.com"
|
||||
runs_on:
|
||||
description: "Runner label; must match a registered runner"
|
||||
required: false
|
||||
type: string
|
||||
default: "ubuntu-latest"
|
||||
chown_www:
|
||||
description: "Run chown -R www:www after sync (requires permission on runner)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ${{ inputs.runs_on }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}/_src
|
||||
env:
|
||||
NPM_CONFIG_REGISTRY: ${{ inputs.npm_registry }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
# 这一步在 workspace 根执行,不能用默认的 _src(此时还不存在)
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GITEA_HOST: git.jc2009.com
|
||||
GITEA_TOKEN: ${{ github.token }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory '*'
|
||||
REPO="${{ github.repository }}"
|
||||
SHA="${{ github.sha }}"
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
TOKEN="${GITEA_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||
test -n "$TOKEN" || { echo "Checkout: empty token (github.token not available to this job)"; exit 1; }
|
||||
URL="https://x-access-token:${TOKEN}@${GITEA_HOST}/${REPO}.git"
|
||||
|
||||
SRC_DIR="${GITHUB_WORKSPACE}/_src"
|
||||
# 只清理 _src,不动 workspace 根(保护 runner 写入的 workflow/*.sh)
|
||||
rm -rf "$SRC_DIR"
|
||||
git clone --depth 1 --branch "$BRANCH" "$URL" "$SRC_DIR"
|
||||
cd "$SRC_DIR"
|
||||
if [ "$(git rev-parse HEAD)" != "$SHA" ]; then
|
||||
git fetch --depth 200 origin "refs/heads/${BRANCH}"
|
||||
git checkout --force "$SHA"
|
||||
fi
|
||||
echo "Checked out $(git rev-parse HEAD) to $SRC_DIR"
|
||||
|
||||
- name: Check Node.js (pre-installed on runner)
|
||||
run: |
|
||||
set -e
|
||||
command -v node >/dev/null || { echo "Runner 上未找到 node,请先安装 Node 20+"; exit 1; }
|
||||
command -v npm >/dev/null || { echo "Runner 上未找到 npm"; exit 1; }
|
||||
node -v
|
||||
npm -v
|
||||
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
|
||||
test "$NODE_MAJOR" -ge 20 || { echo "需要 Node 20 及以上,当前: $(node -v)"; exit 1; }
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: ${{ inputs.build_command }}
|
||||
|
||||
- name: Deploy (sync dist to web root)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -d dist
|
||||
DEST="${{ inputs.deploy_path }}"
|
||||
DEST="${DEST%/}"
|
||||
mkdir -p "$DEST"
|
||||
# 宝塔面板会自动生成 .user.ini(并加 immutable 属性)和 .htaccess,跳过它们
|
||||
find "$DEST" -mindepth 1 -maxdepth 1 \
|
||||
! -name ".user.ini" \
|
||||
! -name ".htaccess" \
|
||||
-exec rm -rf {} +
|
||||
cp -a dist/. "$DEST/"
|
||||
|
||||
- name: Set ownership for Nginx (Baota www)
|
||||
run: |
|
||||
if [ "${{ inputs.chown_www }}" != "true" ]; then exit 0; fi
|
||||
DEST="${{ inputs.deploy_path }}"
|
||||
DEST="${DEST%/}"
|
||||
chown -R www:www "$DEST" || chown -R nginx:nginx "$DEST" || true
|
||||
50
.github/workflows/reusable-release-skill.yaml
vendored
50
.github/workflows/reusable-release-skill.yaml
vendored
@@ -3,6 +3,11 @@ 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
|
||||
@@ -30,23 +35,30 @@ on:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
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: http://120.25.191.12:3000/admin/actions-checkout@v4
|
||||
- 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 env(PIP_INDEX_URL / PIP_EXTRA_INDEX_URL)统一指定,与 Encrypt 步骤中 PyArmor 内部 pip 一致。
|
||||
# 使用 python3.12 -m pip,避免仅存在 python3(3.6) 或裸 pip 不在 PATH 的宿主机/容器。
|
||||
- name: Setup Tools
|
||||
run: pip install "pyarmor==8.5.3" requests python-frontmatter --break-system-packages
|
||||
run: python3.12 -m pip install "pyarmor==8.5.3" requests python-frontmatter --break-system-packages
|
||||
|
||||
- name: Register PyArmor (optional)
|
||||
env:
|
||||
@@ -55,12 +67,13 @@ jobs:
|
||||
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)"
|
||||
python3.12 -c "import os,base64,pathlib,subprocess; os.environ['PATH']='/usr/local/bin:/usr/local/python3.12/bin:'+os.environ.get('PATH',''); 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: |
|
||||
export PATH="/usr/local/bin:/usr/local/python3.12/bin:${PATH:-}"
|
||||
mkdir -p dist/package/scripts
|
||||
set -euo pipefail
|
||||
test -d scripts
|
||||
@@ -72,11 +85,30 @@ jobs:
|
||||
- name: Parse Metadata and Pack
|
||||
id: build_task
|
||||
run: |
|
||||
python -c "
|
||||
python3.12 -c "
|
||||
import frontmatter, os, json, shutil
|
||||
post = frontmatter.load('SKILL.md')
|
||||
metadata = dict(post.metadata or {})
|
||||
metadata['readme_md'] = (post.content or '').strip()
|
||||
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:
|
||||
@@ -102,7 +134,7 @@ jobs:
|
||||
METADATA_JSON: ${{ steps.build_task.outputs.metadata }}
|
||||
SYNC_URL: ${{ inputs.sync_url }}
|
||||
run: |
|
||||
python -c "
|
||||
python3.12 -c "
|
||||
import requests, json, os
|
||||
metadata = json.loads(os.environ['METADATA_JSON'])
|
||||
res = requests.post(os.environ['SYNC_URL'], json=metadata)
|
||||
@@ -121,7 +153,7 @@ jobs:
|
||||
ARTIFACT_PLATFORM: ${{ steps.build_task.outputs.artifact_platform }}
|
||||
UPLOAD_URL: ${{ inputs.upload_url }}
|
||||
run: |
|
||||
python -c "
|
||||
python3.12 -c "
|
||||
import requests, os
|
||||
slug = os.environ['SLUG']
|
||||
version = os.environ['VERSION']
|
||||
@@ -148,7 +180,7 @@ jobs:
|
||||
VERSION: ${{ steps.build_task.outputs.version }}
|
||||
PRUNE_URL: ${{ inputs.prune_url }}
|
||||
run: |
|
||||
python -c "
|
||||
python3.12 -c "
|
||||
import requests, os
|
||||
payload = {
|
||||
'name': os.environ['SLUG'],
|
||||
|
||||
Reference in New Issue
Block a user