Skip to content

Update Nodes

Update Nodes #221

Workflow file for this run

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}"
# Try download, fallback to cache if fails
if curl -fsSL -L -o sb.tar.gz "https://github.com/SagerNet/sing-box/releases/download/${VERSION}/sing-box-${VER_NUM}-linux-amd64.tar.gz"; then
tar -xzf sb.tar.gz
mv "sing-box-${VER_NUM}-linux-amd64/sing-box" ./sing-box
chmod +x ./sing-box
else
echo "::warning::sing-box download failed, trying fallback versions"
for FALLBACK_VER in "v1.11.15" "v1.10.7" "v1.9.7"; do
FV="${FALLBACK_VER#v}"
if curl -fsSL -L -o sb.tar.gz "https://github.com/SagerNet/sing-box/releases/download/${FALLBACK_VER}/sing-box-${FV}-linux-amd64.tar.gz"; then
tar -xzf sb.tar.gz
mv "sing-box-${FV}-linux-amd64/sing-box" ./sing-box
chmod +x ./sing-box
echo "Using fallback sing-box ${FALLBACK_VER}"
break
fi
done
if [ ! -f ./sing-box ]; then
echo "::error::All sing-box download attempts failed"
exit 1
fi
fi
- 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: Source discovery (scan other projects)
if: always()
continue-on-error: true
run: |
python tools/source_discovery.py --sources config/sources.yaml --output output/source_discovery.json --timeout 15 || true
- 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/source_discovery.json
output/node_stability.json
output/trend_history.json
output/*.converter.md
- name: Notify on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `🔴 CI Failed: ${{ github.run_id }}`,
body: `The node update workflow failed.\n\n[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\nTriggered by: ${{ github.event_name }}\nBranch: ${{ github.ref_name }}`,
labels: ['ci-failure']
})
.catch(err => core.warning(`Failed to create issue: ${err.message}`))
- name: Close ci-failure issues on success
if: success()
uses: actions/github-script@v7
with:
script: |
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'ci-failure'
});
for (const issue of issues.data) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed',
labels: [...issue.labels.map(l => l.name), 'resolved']
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `✅ CI recovered. This issue is now resolved.\n\n[Latest successful run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`
});
}
core.info(`Closed ${issues.data.length} ci-failure issues.`);
- 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 source_discovery.json node_stability.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
# Purge chunk files and protocol files
for f in chunks/verified_1.txt chunks/verified_2.txt chunks/verified_3.txt chunks/global_1.txt chunks/global_2.txt chunks/global_3.txt by_protocol/verified_vmess.txt by_protocol/verified_vless.txt by_protocol/verified_trojan.txt by_protocol/verified_shadowsocks.txt; do
code=$(curl -s -o /dev/null -w '%{http_code}' "https://purge.jsdelivr.net/gh/${REPO}@${BRANCH}/output/${f}" || true) && echo "purged output/${f} HTTP ${code}" || true
done
echo "jsDelivr purge warnings: ${failed}"