Repository navigation
NodeCollection #310
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: NodeCollection | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '0 */4 * * *' | |
| watch: | |
| types: started | |
| permissions: | |
| contents: write | |
| # P7 (v2.1.0) 优化1: 并发控制, 避免定时任务与手动触发同时运行导致 git push 冲突 | |
| concurrency: | |
| group: nodecollection-fetch | |
| cancel-in-progress: false # 不取消正在运行的, 排队等待完成 | |
| jobs: | |
| fetch: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: true | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.x' | |
| - name: Cache pip | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip- | |
| - name: Set timezone | |
| run: sudo timedatectl set-timezone 'Asia/Shanghai' | |
| - name: Install dependencies | |
| # P7 (v2.1.0) 优化3: 超时设置 | |
| timeout-minutes: 5 | |
| run: | | |
| pip install -r ./requirements.txt | |
| - name: Get subconverter latest version | |
| # P8 (v2.2.0) T8.1a: 获取 subconverter 最新版本号, 作为缓存 key | |
| id: sc-version | |
| run: | | |
| SC_VERSION=$(curl -sL https://api.github.com/repos/tindy2013/subconverter/releases/latest | grep -oP '"tag_name": "\K[^"]+' || echo "unknown") | |
| echo "version=$SC_VERSION" >> $GITHUB_OUTPUT | |
| echo "subconverter latest version: $SC_VERSION" | |
| - name: Cache subconverter | |
| # P8 (v2.2.0) T8.1a: 缓存 subconverter 二进制, 避免每次重复下载 | |
| uses: actions/cache@v4 | |
| with: | |
| path: subconverter_bin | |
| key: ${{ runner.os }}-subconverter-${{ steps.sc-version.outputs.version }} | |
| - name: Download and start subconverter | |
| # P7 (v2.1.0) 优化3: 超时设置 | |
| timeout-minutes: 5 | |
| run: | | |
| # P8 (v2.2.0) T8.1a: 缓存命中则跳过下载 | |
| if [ -d "subconverter_bin/subconverter" ] && [ -x "subconverter_bin/subconverter/subconverter" ]; then | |
| echo "✅ subconverter cache hit, skipping download" | |
| else | |
| echo "⬇️ subconverter cache miss, downloading..." | |
| # P8 (v2.2.0) T8.1d: 下载重试, 网络不稳定时自动重试 3 次 | |
| wget -q --tries=3 --retry-connrefused --timeout=30 \ | |
| https://github.com/tindy2013/subconverter/releases/latest/download/subconverter_linux64.tar.gz | |
| mkdir -p subconverter_bin | |
| tar xzf subconverter_linux64.tar.gz -C subconverter_bin | |
| fi | |
| echo "=== subconverter directory contents ===" | |
| ls -la subconverter_bin/subconverter/ | |
| echo "=== base directory (if exists) ===" | |
| ls -la subconverter_bin/subconverter/base/ 2>/dev/null || echo "no base dir" | |
| # P9.3 (v2.3.3): 不再通过 pref.ini 加载自定义配置, 改为通过 API config 参数传递 | |
| # pref.ini 保留 subconverter 默认配置 (从 pref.example.ini 复制), | |
| # 自定义配置 (custom_proxy_group / emoji / ruleset 等) 通过 main.py 的 | |
| # call_subconverter() 函数的 API config 参数直接传递 external_config.ini | |
| SC_DIR=subconverter_bin/subconverter | |
| if [ -f "$SC_DIR/pref.example.ini" ]; then | |
| cp "$SC_DIR/pref.example.ini" "$SC_DIR/pref.ini" | |
| echo "pref.ini copied from pref.example.ini" | |
| else | |
| echo "WARNING: pref.example.ini not found, using default pref.ini" | |
| fi | |
| # P14 (v2.6.2): 复制外部配置文件到 subconverter 运行目录 | |
| # main.py call_subconverter() 通过 config=<相对文件名> 传递, | |
| # subconverter 从自身 CWD 读取 (不能用 file:// 前缀或绝对路径, | |
| # fetchFile 受 isInScope 限制, 否则外部配置从未加载, 地区分组失效) | |
| if [ -f "./subconverter/external_config.ini" ]; then | |
| cp ./subconverter/external_config.ini "$SC_DIR/external_config.ini" | |
| echo "✅ external_config.ini copied to subconverter dir" | |
| else | |
| echo "WARNING: subconverter/external_config.ini not found" | |
| fi | |
| if [ -f "./subconverter/merged_config.ini" ]; then | |
| cp ./subconverter/merged_config.ini "$SC_DIR/merged_config.ini" | |
| echo "✅ merged_config.ini copied to subconverter dir" | |
| else | |
| echo "WARNING: subconverter/merged_config.ini not found" | |
| fi | |
| echo "=== pref.ini sections ===" | |
| grep "^\[" "$SC_DIR/pref.ini" | |
| cd "$SC_DIR" && nohup ./subconverter > /dev/null 2>&1 & | |
| # P7 (v2.1.0) 优化2: 轮询等待 subconverter 启动, 替代不可靠的 sleep 3 | |
| echo "Waiting for subconverter to start..." | |
| for i in $(seq 1 15); do | |
| if curl -s http://127.0.0.1:25500/version > /dev/null 2>&1; then | |
| echo "subconverter started successfully (after ${i}s)" | |
| curl -s http://127.0.0.1:25500/version | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if ! curl -s http://127.0.0.1:25500/version > /dev/null 2>&1; then | |
| echo "ERROR: subconverter failed to start within 15s" | |
| exit 1 | |
| fi | |
| - name: Run collector | |
| # P7 (v2.1.0) 优化3: 超时设置, 防止采集卡住浪费 Actions 时间 | |
| # P21 (v2.13.0): 注入告警 Webhook (GitHub Secrets 配置 ALERT_WEBHOOK_URL) | |
| timeout-minutes: 15 | |
| env: | |
| ALERT_WEBHOOK_URL: ${{ secrets.ALERT_WEBHOOK_URL }} | |
| run: | | |
| python ./main.py | |
| - name: Stop subconverter | |
| if: always() | |
| run: | | |
| pkill -f subconverter || true | |
| - name: Verify output files | |
| # P7 (v2.1.0) 优化4: commit 前验证关键输出文件存在且非空, 避免提交失败结果 | |
| run: | | |
| echo "=== Verifying output files ===" | |
| ERROR=0 | |
| for f in \ | |
| "output/merged/latest.clash.yaml" \ | |
| "output/index.json" \ | |
| "output/status.html" \ | |
| "README.md"; do | |
| if [ ! -s "$f" ]; then | |
| echo "❌ MISSING or EMPTY: $f" | |
| ERROR=1 | |
| else | |
| SIZE=$(stat -c%s "$f" 2>/dev/null || echo "?") | |
| echo "✅ OK: $f (${SIZE} bytes)" | |
| fi | |
| done | |
| if [ $ERROR -eq 1 ]; then | |
| echo "ERROR: Critical output files missing or empty, aborting commit" | |
| exit 1 | |
| fi | |
| - name: Commit and Push | |
| # P7 (v2.1.0) 优化3: 超时设置 | |
| timeout-minutes: 5 | |
| run: | | |
| git config --global user.name "GitHub Actions" | |
| git config --global user.email "actions@github.com" | |
| git add ./sub ./output ./README.md | |
| # P8 (v2.2.0) T8.1b: Commit 信息增强, 从 index.json 读取节点数和平均延迟 | |
| if [ -f output/index.json ]; then | |
| NODE_COUNT=$(python3 -c "import json; d=json.load(open('output/index.json')); print(d.get('merged',{}).get('quality',{}).get('output_count','?'))" 2>/dev/null || echo "?") | |
| AVG_LATENCY=$(python3 -c "import json; d=json.load(open('output/index.json')); print(d.get('merged',{}).get('quality',{}).get('avg_latency_ms','?'))" 2>/dev/null || echo "?") | |
| COMMIT_MSG="Update $(date '+%Y-%m-%d %H:%M:%S') | ${NODE_COUNT} nodes | ${AVG_LATENCY}ms avg" | |
| else | |
| COMMIT_MSG="Update $(date '+%Y-%m-%d %H:%M:%S')" | |
| fi | |
| echo "Commit message: $COMMIT_MSG" | |
| git diff --staged --quiet || git commit -m "$COMMIT_MSG" | |
| # P8 (v2.2.0) 热修复: git push 并发冲突处理 — 先 pull --rebase 再 push, 失败重试 3 次 | |
| # 场景: 定时任务与手动触发同时运行时, 后 push 的会因 non-fast-forward 失败 | |
| PUSH_SUCCESS=0 | |
| for i in 1 2 3; do | |
| echo "Push attempt $i/3..." | |
| git pull --rebase origin main || { echo "git pull --rebase failed, aborting and retrying..."; git rebase --abort 2>/dev/null || true; sleep 2; continue; } | |
| if git push origin main; then | |
| PUSH_SUCCESS=1 | |
| echo "✅ Push successful on attempt $i" | |
| break | |
| fi | |
| echo "Push failed on attempt $i, retrying..." | |
| sleep 2 | |
| done | |
| if [ $PUSH_SUCCESS -eq 0 ]; then | |
| echo "❌ ERROR: git push failed after 3 attempts" | |
| exit 1 | |
| fi | |
| - name: Deploy to GitHub Pages (T3.5) | |
| # P7 (v2.1.0) 优化3: 超时设置 | |
| # P8 (v2.2.0) T8.1c: 升级 peaceiris/actions-gh-pages v3 → v4 | |
| timeout-minutes: 5 | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./output | |
| force_orphan: true | |
| user_name: 'GitHub Actions' | |
| user_email: 'actions@github.com' |