From 56661793505489b57cb923e55126aff5c26f7eec Mon Sep 17 00:00:00 2001 From: Meow <197331664+Meo597@users.noreply.github.com> Date: Sat, 16 May 2026 06:34:20 +0800 Subject: [PATCH] Auto Cleanup Stale Xray-core Version Badges --- .github/workflows/cleanup-version-badges.yml | 67 +++++++++ scripts/cleanup-version-badges.mjs | 138 +++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 .github/workflows/cleanup-version-badges.yml create mode 100644 scripts/cleanup-version-badges.mjs diff --git a/.github/workflows/cleanup-version-badges.yml b/.github/workflows/cleanup-version-badges.yml new file mode 100644 index 00000000..3159204c --- /dev/null +++ b/.github/workflows/cleanup-version-badges.yml @@ -0,0 +1,67 @@ +name: Cleanup Stale Xray-core Version Badges + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: cleanup-version-badges + cancel-in-progress: false + +jobs: + cleanup: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.ref_name }} + token: ${{ secrets.ACTION_PERSONAL_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Resolve latest Xray-core release + id: latest + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.ACTION_PERSONAL_TOKEN }} + script: | + const release = await github.request( + "GET /repos/{owner}/{repo}/releases/latest", + { + "owner": "XTLS", + "repo": "Xray-core", + "headers": { + "accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2026-03-10" + } + } + ) + + core.setOutput("tag", release.data.tag_name) + core.info(`Latest Xray-core release: ${release.data.tag_name}`) + + - name: Remove stale version badges + run: node scripts/cleanup-version-badges.mjs --latest "${{ steps.latest.outputs.tag }}" + + - name: Commit cleaned docs + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if git diff --quiet; then + echo "No stale version badges to clean." + exit 0 + fi + + git add docs + git commit -m "chore: clear stale Xray-core badges for ${{ steps.latest.outputs.tag }}" + git push diff --git a/scripts/cleanup-version-badges.mjs b/scripts/cleanup-version-badges.mjs new file mode 100644 index 00000000..b3312104 --- /dev/null +++ b/scripts/cleanup-version-badges.mjs @@ -0,0 +1,138 @@ +import fs from "node:fs" +import path from "node:path" + +const ROOT = process.cwd() +const args = process.argv.slice(2) + +function readArg(name) { + const index = args.indexOf(name) + if (index === -1) return null + return args[index + 1] ?? null +} + +function hasFlag(name) { + return args.includes(name) +} + +const docsDir = path.resolve(ROOT, readArg("--docs") ?? "docs") +const latestTag = readArg("--latest") ?? process.env.XRAY_CORE_LATEST_TAG +const dryRun = hasFlag("--dry-run") + +if (!latestTag) { + console.error( + "Missing latest tag. Use --latest v26.2.26 or set XRAY_CORE_LATEST_TAG." + ) + process.exit(1) +} + +const latestVersion = parseVersion(latestTag) +if (!latestVersion) { + console.error(`Unsupported latest tag format: ${latestTag}`) + process.exit(1) +} + +if (!fs.existsSync(docsDir)) { + console.error(`Docs directory does not exist: ${docsDir}`) + process.exit(1) +} + +function parseVersion(input) { + const value = String(input).trim() + if (!/^v\d+(?:\.\d+)+$/i.test(value)) return null + return value + .replace(/^v/i, "") + .split(".") + .map((part) => Number.parseInt(part, 10)) +} + +function compareVersions(left, right) { + const maxLen = Math.max(left.length, right.length) + for (let i = 0; i < maxLen; i += 1) { + const a = left[i] ?? 0 + const b = right[i] ?? 0 + if (a > b) return 1 + if (a < b) return -1 + } + return 0 +} + +function walk(dir) { + const files = [] + for (const name of fs.readdirSync(dir)) { + const absPath = path.join(dir, name) + const stat = fs.statSync(absPath) + if (stat.isDirectory()) { + files.push(...walk(absPath)) + continue + } + if (absPath.endsWith(".md")) files.push(absPath) + } + return files +} + +function cleanupLine(line, removedCount) { + if (!removedCount) return line + + let next = line + next = next.replace(/[ \t]{2,}/g, " ") + next = next.replace(/\s+([,.;:!?])/g, "$1") + next = next.replace(/[ \t]+$/g, "") + if (!next.trim()) return "" + return next +} + +function shouldRemoveBadge(textValue) { + const badgeVersion = parseVersion(textValue) + if (!badgeVersion) return false + return compareVersions(badgeVersion, latestVersion) < 0 +} + +function cleanupFile(filePath) { + const source = fs.readFileSync(filePath, "utf8") + const eol = source.includes("\r\n") ? "\r\n" : "\n" + const lines = source.split(/\r?\n/) + let removed = 0 + + const nextLines = lines.map((line) => { + let removedOnLine = 0 + const nextLine = line.replace(/]*?)\/>/gi, (full, attrs) => { + const match = attrs.match(/\btext\s*=\s*(['"])(.*?)\1/i) + const textValue = match?.[2]?.trim() + if (!textValue || !shouldRemoveBadge(textValue)) return full + removed += 1 + removedOnLine += 1 + return "" + }) + return cleanupLine(nextLine, removedOnLine) + }) + + if (!removed) { + return { changed: false, removed: 0 } + } + + const nextSource = nextLines.join(eol) + if (!dryRun) { + fs.writeFileSync(filePath, nextSource, "utf8") + } + + return { changed: nextSource !== source, removed } +} + +const files = walk(docsDir) +const changedFiles = [] +let removedBadges = 0 + +for (const filePath of files) { + const result = cleanupFile(filePath) + if (!result.changed) continue + removedBadges += result.removed + changedFiles.push(path.relative(ROOT, filePath).replaceAll("\\", "/")) +} + +console.log(`Latest Xray-core tag: ${latestTag}`) +console.log(`Removed stale version badges: ${removedBadges}`) +console.log(`Changed files: ${changedFiles.length}`) + +for (const file of changedFiles) { + console.log(`- ${file}`) +}