Skip to content

NodeCollection

NodeCollection #310

Workflow file for this run

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'