Backfill Mod Version Tags #5
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
| # .github/workflows/backfill-mod-version-tags.yml | |
| name: Backfill Mod Version Tags | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: "Branch or ref to scan (e.g., refs/heads/main)" | |
| required: false | |
| default: "refs/heads/main" | |
| since: | |
| description: 'Only scan commits since this date (e.g., "2024-01-01"), empty = all history' | |
| required: false | |
| default: "" | |
| limit: | |
| description: "Max commits to scan (to avoid rate limits). 0 = no limit" | |
| required: false | |
| default: "0" | |
| dry_run: | |
| description: "If true, only print what would be tagged" | |
| required: false | |
| default: "true" # start safe | |
| permissions: | |
| contents: write # needed to create tags | |
| jobs: | |
| backfill: | |
| runs-on: ubuntu-latest | |
| env: | |
| # Use the exact same pattern & tagging rules as the auto-tagger: | |
| COMMIT_PATTERN: '^(?<subject>(?<mod>.+?)\s+v(?<ver>\d+(?:\.\d+)*(?:[-+][A-Za-z0-9._-]+)?)(?:\s+\((?<qual>[^)]+)\))?)$' | |
| TAG_FORMAT: '${subject}' | |
| steps: | |
| - name: Checkout full history & tags | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # full history | |
| fetch-tags: true | |
| - name: Backfill tags over history | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { execSync } = require("node:child_process"); | |
| function sh(cmd) { return execSync(cmd, { encoding: "utf8" }).trim(); } | |
| // Inputs | |
| // const ref = (core.getInput("ref") || "refs/heads/main").trim(); | |
| // const since = (core.getInput("since") || "").trim(); | |
| // const limit = parseInt(core.getInput("limit") || "0", 10); | |
| // const dryRun = (core.getInput("dry_run") || "true").toLowerCase() === "true"; | |
| // Actual Inputs (read workflow_dispatch inputs from context.payload.inputs) | |
| const wi = context.payload.inputs || {}; | |
| const ref = (wi.ref && wi.ref.trim()) || "refs/heads/main"; | |
| const since = (wi.since && wi.since.trim()) || ""; | |
| const limit = Number.isFinite(parseInt(wi.limit, 10)) ? parseInt(wi.limit, 10) : 0; | |
| // const dryRun = String(wi.dry_run ?? "true").toLowerCase() === "true"; | |
| // Explicit boolean parsing with validation | |
| let dryRaw = (wi.dry_run ?? "true").toLowerCase(); | |
| if (dryRaw !== "true" && dryRaw !== "false") { | |
| throw new Error(`Invalid input for dry_run: "${dryRaw}". Use "true" or "false".`); | |
| } | |
| const dryRun = dryRaw === "true"; | |
| core.info(`Inputs -> ref="${ref}" since="${since}" limit=${limit} dry_run=${dryRun}`); | |
| // Same regex & sanitiser as your push workflow | |
| const pattern = new RegExp(process.env.COMMIT_PATTERN); | |
| const tagFormat = process.env.TAG_FORMAT; | |
| function sanitizeTag(raw) { | |
| let t = raw.trim() | |
| .replace(/\s+/g, "-") | |
| .replace(/\//g, "-") | |
| .replace(/[^0-9A-Za-z._\-+()]/g, "-") | |
| .replace(/-+/g, "-") | |
| .replace(/^[-.]+|[-.]+$/g, ""); | |
| if (!t) throw new Error(`Invalid tag from "${raw}"`); | |
| return t; | |
| } | |
| // Ensure ref exists locally | |
| try { sh(`git rev-parse --verify ${ref}`); } | |
| catch { | |
| // fetch exact ref if missing | |
| core.info(`Fetching ${ref} from origin...`); | |
| sh(`git fetch origin ${ref}:${ref}`); | |
| } | |
| // Build rev-list args | |
| const args = []; | |
| if (since) args.push(`--since="${since}"`); | |
| args.push(ref); | |
| let revList = sh(`git rev-list ${args.join(" ")}`); | |
| if (!revList) { | |
| core.info("No commits to scan."); | |
| return; | |
| } | |
| let shas = revList.split("\n"); | |
| // Oldest -> newest so tags appear chronologically if ever listed | |
| shas = shas.reverse(); | |
| if (limit > 0 && shas.length > limit) { | |
| core.info(`Limiting to first ${limit} of ${shas.length} commits`); | |
| shas = shas.slice(0, limit); | |
| } | |
| let created = 0, skippedNoMatch = 0, skippedExists = 0; | |
| for (const sha of shas) { | |
| const subject = sh(`git show -s --format=%s ${sha}`); | |
| const m = subject.match(pattern); | |
| if (!m) { skippedNoMatch++; continue; } | |
| const groups = m.groups || {}; | |
| let rawTag = tagFormat.replace(/\$\{([^}]+)\}/g, (_, name) => | |
| groups[name] != null ? groups[name] : "" | |
| ); | |
| if (!rawTag) rawTag = subject; | |
| const tagName = sanitizeTag(rawTag); | |
| // Check if tag exists remotely | |
| let exists = false; | |
| try { | |
| await github.rest.git.getRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `tags/${tagName}`, | |
| }); | |
| exists = true; | |
| } catch (_) {} | |
| if (exists) { | |
| core.info(`EXISTS : ${tagName} -> ${sha} (${subject})`); | |
| skippedExists++; | |
| continue; | |
| } | |
| if (dryRun) { | |
| core.notice(`[DRY RUN] Would create tag "${tagName}" -> ${sha} (${subject})`); | |
| continue; | |
| } | |
| await github.rest.git.createRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `refs/tags/${tagName}`, | |
| sha, | |
| }); | |
| core.notice(`CREATED : ${tagName} -> ${sha} (${subject})`); | |
| created++; | |
| } | |
| core.summary | |
| .addHeading("Backfill summary") | |
| .addRaw(`Ref scanned: ${ref}\n`) | |
| .addRaw(`Since : ${since || "(none)"}\n`) | |
| .addRaw(`Dry run : ${dryRun}\n`) | |
| .addRaw(`Total scanned: ${shas.length}\n`) | |
| .addRaw(`Created tags: ${created}\n`) | |
| .addRaw(`Skipped (exists): ${skippedExists}\n`) | |
| .addRaw(`Skipped (no match): ${skippedNoMatch}\n`) | |
| .write(); |