Update Nodes #212
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: Update Nodes | |
| on: | |
| schedule: | |
| - cron: '0 */6 * * *' | |
| push: | |
| paths: | |
| - 'core/**' | |
| - 'main.py' | |
| - 'config/**' | |
| - 'requirements.txt' | |
| - 'tools/**' | |
| - 'tests/**' | |
| - '.github/workflows/update.yml' | |
| workflow_dispatch: | |
| inputs: | |
| top_n: | |
| description: 'verified.* 输出节点数(默认 300)' | |
| required: false | |
| default: '300' | |
| test_limit: | |
| description: '送入真测的最大节点数(默认 1500)' | |
| required: false | |
| default: '1500' | |
| min_retain_ratio: | |
| description: '缩水守门比例,0=关闭并强制写入本轮真测结果(默认 0)' | |
| required: false | |
| default: '0' | |
| audit: | |
| description: '同时跑订阅源审计' | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| concurrency: | |
| group: update-nodes | |
| cancel-in-progress: false | |
| jobs: | |
| update: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| cache: pip | |
| - run: pip install -r requirements.txt | |
| - name: Get sing-box version | |
| id: sb-version | |
| run: | | |
| set -euo pipefail | |
| VERSION=$(curl -fsSL -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ | |
| https://api.github.com/repos/SagerNet/sing-box/releases/latest | jq -r '.tag_name') | |
| if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then | |
| echo "::error::Failed to fetch sing-box version" | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Latest sing-box: $VERSION" | |
| - name: Cache sing-box binary | |
| id: cache-sb | |
| uses: actions/cache@v4 | |
| with: | |
| path: ./sing-box | |
| key: sing-box-${{ runner.os }}-${{ steps.sb-version.outputs.version }} | |
| - name: Download sing-box | |
| if: steps.cache-sb.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ steps.sb-version.outputs.version }}" | |
| VER_NUM="${VERSION#v}" | |
| curl -fsSL -L -o sb.tar.gz "https://github.com/SagerNet/sing-box/releases/download/${VERSION}/sing-box-${VER_NUM}-linux-amd64.tar.gz" | |
| tar -xzf sb.tar.gz | |
| mv "sing-box-${VER_NUM}-linux-amd64/sing-box" ./sing-box | |
| chmod +x ./sing-box | |
| - name: Audit sources | |
| run: | | |
| python tools/audit_sources.py --sources config/sources.yaml --output output/source_audit.json | |
| - name: Validate code | |
| run: | | |
| python -m compileall -q main.py core tools tests | |
| python tools/validate_config.py --sources config/sources.yaml --filter-rules config/filter_rules.yaml --scoring-rules 'config/scoring*.yaml' | |
| python -m unittest -v tests.test_regressions | |
| - name: Run pipeline | |
| env: | |
| AUTONODES_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| set -euo pipefail | |
| TOP_N="${{ github.event.inputs.top_n || '300' }}" | |
| TEST_LIMIT="${{ github.event.inputs.test_limit || '1500' }}" | |
| MIN_RETAIN_RATIO="${{ github.event.inputs.min_retain_ratio || '0' }}" | |
| [[ "$TOP_N" =~ ^[0-9]+$ ]] || { echo "::error::top_n must be a non-negative integer"; exit 1; } | |
| [[ "$TEST_LIMIT" =~ ^[0-9]+$ ]] || { echo "::error::test_limit must be a non-negative integer"; exit 1; } | |
| [[ "$MIN_RETAIN_RATIO" =~ ^[0-9]+([.][0-9]+)?$ ]] || { echo "::error::min_retain_ratio must be a non-negative number"; exit 1; } | |
| [ "$TOP_N" -le 1000 ] || { echo "::error::top_n must be <= 1000"; exit 1; } | |
| [ "$TEST_LIMIT" -le 3000 ] || { echo "::error::test_limit must be <= 3000"; exit 1; } | |
| python main.py \ | |
| --top-n "$TOP_N" \ | |
| --test-limit "$TEST_LIMIT" \ | |
| --min-retain-ratio "$MIN_RETAIN_RATIO" \ | |
| --repo "${{ github.repository }}" \ | |
| --branch "${{ github.event.repository.default_branch }}" | |
| - name: Build health report | |
| run: | | |
| python tools/source_scores_report.py --output-dir output --output output/source_scores.md | |
| python tools/scoring_profiles_report.py --profiles 'config/scoring*.yaml' --output output/scoring_profiles.md | |
| python tools/suggest_source_cleanup.py --output-dir output --sources config/sources.yaml --output output/source_cleanup_suggestions.md --json-output output/source_cleanup_suggestions.json | |
| python tools/health_report.py --output-dir output --verified-prefix verified --output output/health_report.json | |
| python tools/daily_report.py --output-dir output --output output/daily_report.md | |
| - name: Write job summary | |
| if: always() | |
| run: | | |
| if [ -f output/stats.json ]; then | |
| python tools/actions_summary.py \ | |
| --stats output/stats.json \ | |
| --repo "${{ github.repository }}" \ | |
| --branch "${{ github.event.repository.default_branch }}" | |
| else | |
| echo "# AutoNodes Run Summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "stats.json was not generated. Check previous steps." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Commit & push | |
| run: | | |
| git config user.email "actions@github.com" | |
| git config user.name "GitHub Actions" | |
| BRANCH="${{ github.event.repository.default_branch }}" | |
| # 显式 fetch 一次确保 origin/$BRANCH 是最新的, 避免 stale shallow ref | |
| git fetch --unshallow origin "$BRANCH" 2>/dev/null || git fetch origin "$BRANCH" | |
| git rm -f output/top.* 2>/dev/null || true | |
| git add output/ README.md 2>/dev/null || true | |
| if git diff --staged --quiet; then | |
| echo "no changes" | |
| else | |
| git commit -m "chore: update nodes $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M CST')" | |
| for i in 1 2 3 4 5; do | |
| if git pull --rebase --autostash --strategy-option=theirs origin "$BRANCH"; then | |
| git push origin HEAD:"$BRANCH" && break | |
| else | |
| git rebase --abort 2>/dev/null || true | |
| echo "::error::rebase failed; refusing to force push automatically" | |
| exit 1 | |
| fi | |
| if [ "$i" = "5" ]; then | |
| echo "::error::git push failed after 5 attempts" | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| fi | |
| - name: Upload debug artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: autonodes-debug | |
| path: | | |
| output/stats.json | |
| output/source_audit.json | |
| output/health_report.json | |
| output/health_report.md | |
| output/daily_report.md | |
| output/source_scores.md | |
| output/scoring_profiles.md | |
| output/source_cleanup_suggestions.md | |
| output/source_cleanup_suggestions.json | |
| output/trend_history.json | |
| output/*.converter.md | |
| - name: Purge jsDelivr cache | |
| run: | | |
| set -euo pipefail | |
| REPO="${{ github.repository }}" | |
| BRANCH="${{ github.event.repository.default_branch }}" | |
| failed=0 | |
| for f in verified.txt verified.yaml verified.json verified.urls verified.converter.md global.txt global.yaml global.json global.urls global.converter.md all.txt all.yaml all.json all.urls all.converter.md stats.json source_audit.json health_report.json health_report.md daily_report.md source_scores.md scoring_profiles.md source_cleanup_suggestions.md source_cleanup_suggestions.json trend_history.json geo_cache.json; do | |
| code=$(curl -s -o /tmp/jsdelivr-purge.json -w '%{http_code}' "https://purge.jsdelivr.net/gh/${REPO}@${BRANCH}/output/${f}" || true) | |
| if [ "$code" -ge 500 ] || [ "$code" = "000" ]; then | |
| echo "::warning::jsDelivr purge failed for output/${f} HTTP ${code}" | |
| failed=$((failed + 1)) | |
| else | |
| echo "purged output/${f} HTTP ${code}" | |
| fi | |
| done | |
| echo "jsDelivr purge warnings: ${failed}" |