Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71a9ab1700 | |||
| ca117cb5ac | |||
| 892cf837a6 | |||
| a6bbd89350 | |||
| 8778d641ab | |||
| 5037d83b3d | |||
| 8154de452b | |||
| 0bb6707e68 | |||
| 4b15c6d99c | |||
| 4856167682 | |||
| c67487ba16 | |||
| 69702f8ea2 | |||
| 60b4f7a77f |
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:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
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:
|
artifact_platform:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
@@ -30,23 +35,30 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ inputs.runs_on }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
env:
|
env:
|
||||||
ARTIFACT_PLATFORM: ${{ inputs.artifact_platform }}
|
ARTIFACT_PLATFORM: ${{ inputs.artifact_platform }}
|
||||||
PYARMOR_PLATFORM: ${{ inputs.pyarmor_platform }}
|
PYARMOR_PLATFORM: ${{ inputs.pyarmor_platform }}
|
||||||
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
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 易超时。
|
# PyArmor 交叉平台加密时会内部执行 pip 安装 pyarmor.cli.core.* 等包;不设则默认走 files.pythonhosted.org,国内 CI 易超时。
|
||||||
PIP_INDEX_URL: https://pypi.tuna.tsinghua.edu.cn/simple
|
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_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_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
|
PIP_TRUSTED_HOST: pypi.tuna.tsinghua.edu.cn mirrors.aliyun.com mirrors.cloud.tencent.com mirrors.huaweicloud.com files.pythonhosted.org pypi.org
|
||||||
steps:
|
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。
|
# 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 一致。
|
# 镜像由 job env(PIP_INDEX_URL / PIP_EXTRA_INDEX_URL)统一指定,与 Encrypt 步骤中 PyArmor 内部 pip 一致。
|
||||||
|
# 使用 python3.12 -m pip,避免仅存在 python3(3.6) 或裸 pip 不在 PATH 的宿主机/容器。
|
||||||
- name: Setup Tools
|
- 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)
|
- name: Register PyArmor (optional)
|
||||||
env:
|
env:
|
||||||
@@ -55,12 +67,13 @@ jobs:
|
|||||||
if [ -z "${PYARMOR_REG_B64}" ]; then
|
if [ -z "${PYARMOR_REG_B64}" ]; then
|
||||||
echo "PyArmor: no PYARMOR_REG_B64 secret — trial mode (very large single .py modules may fail to obfuscate)."
|
echo "PyArmor: no PYARMOR_REG_B64 secret — trial mode (very large single .py modules may fail to obfuscate)."
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);产物保留与源码一致的 scripts/ 层级,入口为 scripts/main.py。
|
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);产物保留与源码一致的 scripts/ 层级,入口为 scripts/main.py。
|
||||||
- name: Encrypt Source Code
|
- name: Encrypt Source Code
|
||||||
run: |
|
run: |
|
||||||
|
export PATH="/usr/local/bin:/usr/local/python3.12/bin:${PATH:-}"
|
||||||
mkdir -p dist/package/scripts
|
mkdir -p dist/package/scripts
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
test -d scripts
|
test -d scripts
|
||||||
@@ -72,11 +85,30 @@ jobs:
|
|||||||
- name: Parse Metadata and Pack
|
- name: Parse Metadata and Pack
|
||||||
id: build_task
|
id: build_task
|
||||||
run: |
|
run: |
|
||||||
python -c "
|
python3.12 -c "
|
||||||
import frontmatter, os, json, shutil
|
import frontmatter, os, json, shutil
|
||||||
post = frontmatter.load('SKILL.md')
|
post = frontmatter.load('SKILL.md')
|
||||||
metadata = dict(post.metadata or {})
|
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', {})
|
openclaw_meta = metadata.get('metadata', {}).get('openclaw', {})
|
||||||
slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip()
|
slug = (openclaw_meta.get('slug') or metadata.get('slug') or metadata.get('name') or '').strip()
|
||||||
if not slug:
|
if not slug:
|
||||||
@@ -102,7 +134,7 @@ jobs:
|
|||||||
METADATA_JSON: ${{ steps.build_task.outputs.metadata }}
|
METADATA_JSON: ${{ steps.build_task.outputs.metadata }}
|
||||||
SYNC_URL: ${{ inputs.sync_url }}
|
SYNC_URL: ${{ inputs.sync_url }}
|
||||||
run: |
|
run: |
|
||||||
python -c "
|
python3.12 -c "
|
||||||
import requests, json, os
|
import requests, json, os
|
||||||
metadata = json.loads(os.environ['METADATA_JSON'])
|
metadata = json.loads(os.environ['METADATA_JSON'])
|
||||||
res = requests.post(os.environ['SYNC_URL'], json=metadata)
|
res = requests.post(os.environ['SYNC_URL'], json=metadata)
|
||||||
@@ -121,7 +153,7 @@ jobs:
|
|||||||
ARTIFACT_PLATFORM: ${{ steps.build_task.outputs.artifact_platform }}
|
ARTIFACT_PLATFORM: ${{ steps.build_task.outputs.artifact_platform }}
|
||||||
UPLOAD_URL: ${{ inputs.upload_url }}
|
UPLOAD_URL: ${{ inputs.upload_url }}
|
||||||
run: |
|
run: |
|
||||||
python -c "
|
python3.12 -c "
|
||||||
import requests, os
|
import requests, os
|
||||||
slug = os.environ['SLUG']
|
slug = os.environ['SLUG']
|
||||||
version = os.environ['VERSION']
|
version = os.environ['VERSION']
|
||||||
@@ -148,7 +180,7 @@ jobs:
|
|||||||
VERSION: ${{ steps.build_task.outputs.version }}
|
VERSION: ${{ steps.build_task.outputs.version }}
|
||||||
PRUNE_URL: ${{ inputs.prune_url }}
|
PRUNE_URL: ${{ inputs.prune_url }}
|
||||||
run: |
|
run: |
|
||||||
python -c "
|
python3.12 -c "
|
||||||
import requests, os
|
import requests, os
|
||||||
payload = {
|
payload = {
|
||||||
'name': os.environ['SLUG'],
|
'name': os.environ['SLUG'],
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
`{JIANGCHANG_DATA_ROOT}/python-runtime/`
|
`{JIANGCHANG_DATA_ROOT}/python-runtime/`
|
||||||
|
|
||||||
并在该目录执行 `uv sync` 生成 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/`。
|
并在该目录执行 `uv sync` 生成 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/`。
|
||||||
Playwright 浏览器缓存目录:`{JIANGCHANG_DATA_ROOT}/playwright-browsers/`(由宿主设置 `PLAYWRIGHT_BROWSERS_PATH`)。
|
|
||||||
|
**Playwright**:仅安装 Python 包 `playwright` 即可;技能通过 **`channel=chrome` / `msedge`** 使用用户本机已安装的 Chrome 或 Edge。**不要**在宿主流程里执行 `playwright install chromium`(避免下载约 170MB 的自带 Chromium)。
|
||||||
|
|
||||||
## 维护流程
|
## 维护流程
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ JIANGCHANG_* 数据根与用户目录:解析规则 + 可选本地 CLI 默认
|
|||||||
|
|
||||||
宿主在 skills.entries 与 Gateway 中注入:
|
宿主在 skills.entries 与 Gateway 中注入:
|
||||||
- PATH 前缀:{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/(Scripts|bin)
|
- PATH 前缀:{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/(Scripts|bin)
|
||||||
- PLAYWRIGHT_BROWSERS_PATH、VIRTUAL_ENV、JIANGCHANG_PYTHON_EXE
|
- VIRTUAL_ENV、JIANGCHANG_PYTHON_EXE
|
||||||
|
Playwright 使用本机 Chrome/Edge(launch 时 channel=chrome|msedge),不依赖宿主下载的 Chromium 包。
|
||||||
技能勿在仓库内维护独立 .venv;依赖以 jiangchang-platform-kit/python-runtime 锁文件为准。
|
技能勿在仓库内维护独立 .venv;依赖以 jiangchang-platform-kit/python-runtime 锁文件为准。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user