Skip to content

Backfill Mod Version Tags #6

Backfill Mod Version Tags

Backfill Mod Version Tags #6

Workflow file for this run

# .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();