Skip to content

Commit 087a644

Browse files
committed
Rework to handle duplicates...
1 parent fd64fc1 commit 087a644

File tree

1 file changed

+61
-35
lines changed

1 file changed

+61
-35
lines changed

.github/workflows/backfill-tags.yml

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -106,71 +106,97 @@ jobs:
106106
}
107107
108108
// ---- Scan & collect tags to create ----
109-
let created = 0, skippedNoMatch = 0, skippedExists = 0;
110-
const toCreate = []; // { name, sha, subject }
109+
const planned = new Map(); // tagName -> { sha, subject }
110+
let created = 0, skippedNoMatch = 0, skippedAlreadyTagged = 0;
111+
112+
// Use local tags (fetched with actions/checkout fetch-tags: true) as our "remote view".
113+
function localTagExists(name) {
114+
try { sh(`git rev-parse -q --verify "refs/tags/${name}"`); return true; } catch { return false; }
115+
}
116+
117+
// Does ANY tag for this base already exist? (base, -a, -b, -aa, ...)
118+
// If yes, we skip creating more for this base.
119+
function anyLocalTagForBase(base) {
120+
const out = sh(`git tag -l "${base}*"`);
121+
return out && out.length > 0;
122+
}
123+
124+
// Suffix maker: 0 -> "", 1 -> "-a", 2 -> "-b", ..., 26 -> "-z", 27 -> "-aa", etc.
125+
function letterSuffix(n) {
126+
if (n <= 0) return "";
127+
let s = "";
128+
while (n > 0) { n--; s = String.fromCharCode(97 + (n % 26)) + s; n = Math.floor(n / 26); }
129+
return "-" + s;
130+
}
131+
132+
// Find the next free name considering BOTH local tags and what's already planned in this run.
133+
function nextAvailableNameLocal(base) {
134+
let n = 0;
135+
for (;;) {
136+
const candidate = base + letterSuffix(n);
137+
if (!localTagExists(candidate) && !planned.has(candidate)) return candidate;
138+
n++;
139+
}
140+
}
111141
112142
for (const sha of shas) {
113143
const subject = sh(`git show -s --format=%s ${sha}`);
114144
const m = subject.match(pattern);
115145
if (!m) { skippedNoMatch++; continue; }
116146
117147
const groups = m.groups || {};
118-
let rawTag = tagFormat.replace(/\$\{([^}]+)\}/g, (_, name) =>
119-
groups[name] != null ? groups[name] : ""
120-
);
148+
let rawTag = tagFormat.replace(/\$\{([^}]+)\}/g, (_, name) => groups[name] ?? "");
121149
if (!rawTag) rawTag = subject;
122150
123-
const tagName = sanitizeTag(rawTag);
124-
125-
// Skip if tag already exists remotely
126-
let exists = false;
127-
try {
128-
await github.rest.git.getRef({
129-
owner: context.repo.owner,
130-
repo: context.repo.repo,
131-
ref: `tags/${tagName}`,
132-
});
133-
exists = true;
134-
} catch (_) {}
135-
136-
if (exists) {
137-
core.info(`EXISTS : ${tagName} -> ${sha} (${subject})`);
138-
skippedExists++;
151+
const baseName = sanitizeTag(rawTag);
152+
153+
// RULE: if ANY tag already exists for this base (base or suffixed), skip entirely.
154+
if (anyLocalTagForBase(baseName)) {
155+
skippedAlreadyTagged++;
156+
core.info(`SKIP : Base already tagged remotely "${baseName}*" -> ${sha} (${subject})`);
139157
continue;
140158
}
141159
142160
if (dryRun) {
143-
core.notice(`[DRY RUN] Would create tag "${tagName}" -> ${sha} (${subject})`);
161+
const preview = nextAvailableNameLocal(baseName);
162+
core.notice(`[DRY RUN] Would create tag "${preview}" -> ${sha} (${subject})`);
144163
continue;
145164
}
146165
147-
toCreate.push({ name: tagName, sha, subject });
166+
// No existing tag for this base -> assign the first free name for this run
167+
const finalName = nextAvailableNameLocal(baseName);
168+
planned.set(finalName, { sha, subject });
148169
}
149170
150171
// ---- Create locally & push (batched) ----
151-
if (!dryRun && toCreate.length) {
152-
for (const t of toCreate) {
153-
sh(`git tag --force "${t.name}" ${t.sha}`);
172+
if (!dryRun && planned.size) {
173+
// Create tags locally
174+
for (const [name, { sha, subject }] of planned.entries()) {
175+
sh(`git tag --force "${name}" ${sha}`);
154176
created++;
155-
core.info(`Tagged locally: ${t.name} -> ${t.sha} (${t.subject})`);
156-
}
157-
const chunkSize = 50;
158-
for (let i = 0; i < toCreate.length; i += chunkSize) {
159-
const chunk = toCreate.slice(i, i + chunkSize)
160-
.map(t => `refs/tags/${t.name}:refs/tags/${t.name}`);
161-
sh(`git push origin ${chunk.join(' ')}`);
162-
core.notice(`Pushed ${chunk.length} tags`);
177+
core.info(`Tagged locally: ${name} -> ${sha} (${subject})`);
163178
}
179+
// Push in chunks
180+
const entries = Array.from(planned.keys());
181+
const chunkSize = 50;
182+
for (let i = 0; i < entries.length; i += chunkSize) {
183+
const chunk = entries.slice(i, i + chunkSize)
184+
.map(name => `refs/tags/${name}:refs/tags/${name}`);
185+
sh(`git push origin ${chunk.join(' ')}`);
186+
core.notice(`Pushed ${chunk.length} tags`);
187+
}
164188
}
165189
166190
// ---- Summary ----
191+
const suffixedCount = Array.from(planned.keys()).filter(n => /-[a-z]+$/.test(n)).length;
167192
core.summary
168193
.addHeading("Backfill summary")
169194
.addRaw(`Ref scanned: ${ref}\n`)
170195
.addRaw(`Since : ${since || "(none)"}\n`)
171196
.addRaw(`Dry run : ${dryRun}\n`)
172197
.addRaw(`Total scanned: ${shas.length}\n`)
173198
.addRaw(`Created tags: ${created}\n`)
174-
.addRaw(`Skipped (exists): ${skippedExists}\n`)
199+
.addRaw(`Skipped (already tagged base): ${skippedAlreadyTagged}\n`)
175200
.addRaw(`Skipped (no match): ${skippedNoMatch}\n`)
201+
.addRaw(`Suffixed (a/b/c/...): ${suffixedCount}\n`)
176202
.write();

0 commit comments

Comments
 (0)