Created
May 6, 2026 09:25
-
-
Save Greenheart/4235359671d1eaa65057d70ab5c20f69 to your computer and use it in GitHub Desktop.
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
| /** | |
| * This script uses a log file to make operations idempotent, so they only can run once. | |
| * Very useful for API calls that should not be repeated. | |
| * | |
| * This approach allows re-running the bulk update script without affecting already updated items. | |
| */ | |
| import { createWriteStream, readFileSync, existsSync } from "node:fs"; | |
| import { resolve } from "node:path"; | |
| const LOG_FILE = resolve("./updated-issues.txt"); | |
| /** | |
| * Parse a log file from previous runs to make sure issues are only updated once. | |
| * | |
| * ## Format of the log file | |
| * | |
| * Each line contains the type of entry, | |
| * and the GitLab `iid` for the issue or note (comment): | |
| * | |
| * [type] [iid] | |
| * | |
| * ```txt | |
| * issue 221 | |
| * note 35 | |
| * note 49 | |
| * issue 164 | |
| * note 12 | |
| * ``` | |
| * | |
| * By only adding entries to this file after they have been successfully updated, we can | |
| * run the script multiple times without overwriting anything that has already been updated. | |
| * | |
| * @param path Path to the log file. | |
| * @returns Two sets with the already updated issues and notes. | |
| */ | |
| function parseAlreadyUpdatedFromLogFile(path: string) { | |
| const log = existsSync(path) ? readFileSync(path, "utf-8").trim() : ""; | |
| const alreadyUpdated = { | |
| issues: new Set<number>(), | |
| notes: new Set<number>(), | |
| }; | |
| if (log.length) { | |
| for (const line of log.split("\n")) { | |
| const entry = line.split(" "); | |
| if (entry.length !== 2) { | |
| throw new Error("Invalid log entry: " + entry); | |
| } | |
| const [type, iid] = entry; | |
| if (type === "issue") { | |
| alreadyUpdated.issues.add(parseInt(iid, 10)); | |
| } else if (type === "note") { | |
| alreadyUpdated.notes.add(parseInt(iid, 10)); | |
| } else { | |
| throw new Error("Invalid log entry type: " + type); | |
| } | |
| } | |
| } | |
| return alreadyUpdated; | |
| } | |
| /** | |
| * Read the previously updated issues from the log file. | |
| * This ensures we only update each issue once. | |
| */ | |
| const alreadyUpdated = parseAlreadyUpdatedFromLogFile(LOG_FILE); | |
| // Open the log file with append-mode ("a"), | |
| // create it if it doesn't already exist (by using "a+") | |
| const logStream = createWriteStream(LOG_FILE, { flags: "a+" }); | |
| console.log("Already updated from previous runs:"); | |
| console.dir(alreadyUpdated); | |
| /** Realistic fake data */ | |
| const issuesToUpdate = [ | |
| { | |
| iid: 57, | |
| notes: [ | |
| { | |
| iid: 1838, | |
| }, | |
| { | |
| iid: 4950, | |
| }, | |
| { | |
| iid: 3832, | |
| }, | |
| { | |
| iid: 4184, | |
| }, | |
| { | |
| iid: 4756, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 46, | |
| notes: [ | |
| { | |
| iid: 1783, | |
| }, | |
| { | |
| iid: 3241, | |
| }, | |
| { | |
| iid: 555, | |
| }, | |
| { | |
| iid: 3261, | |
| }, | |
| { | |
| iid: 2352, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 43, | |
| notes: [ | |
| { | |
| iid: 4170, | |
| }, | |
| { | |
| iid: 3605, | |
| }, | |
| { | |
| iid: 2181, | |
| }, | |
| { | |
| iid: 1652, | |
| }, | |
| { | |
| iid: 4492, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 39, | |
| notes: [ | |
| { | |
| iid: 1097, | |
| }, | |
| { | |
| iid: 4494, | |
| }, | |
| { | |
| iid: 4337, | |
| }, | |
| { | |
| iid: 3830, | |
| }, | |
| { | |
| iid: 4124, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 23, | |
| notes: [ | |
| { | |
| iid: 4289, | |
| }, | |
| { | |
| iid: 4970, | |
| }, | |
| { | |
| iid: 4417, | |
| }, | |
| { | |
| iid: 1176, | |
| }, | |
| { | |
| iid: 1403, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 17, | |
| notes: [ | |
| { | |
| iid: 851, | |
| }, | |
| { | |
| iid: 109, | |
| }, | |
| { | |
| iid: 441, | |
| }, | |
| { | |
| iid: 1344, | |
| }, | |
| { | |
| iid: 1979, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 16, | |
| notes: [ | |
| { | |
| iid: 2324, | |
| }, | |
| { | |
| iid: 3726, | |
| }, | |
| { | |
| iid: 2526, | |
| }, | |
| { | |
| iid: 3623, | |
| }, | |
| { | |
| iid: 2250, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 15, | |
| notes: [ | |
| { | |
| iid: 377, | |
| }, | |
| { | |
| iid: 852, | |
| }, | |
| { | |
| iid: 4313, | |
| }, | |
| { | |
| iid: 1053, | |
| }, | |
| { | |
| iid: 3768, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 14, | |
| notes: [ | |
| { | |
| iid: 3081, | |
| }, | |
| { | |
| iid: 3044, | |
| }, | |
| { | |
| iid: 4253, | |
| }, | |
| { | |
| iid: 2324, | |
| }, | |
| { | |
| iid: 4157, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 13, | |
| notes: [ | |
| { | |
| iid: 1943, | |
| }, | |
| { | |
| iid: 2008, | |
| }, | |
| { | |
| iid: 3257, | |
| }, | |
| { | |
| iid: 590, | |
| }, | |
| { | |
| iid: 3089, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 12, | |
| notes: [ | |
| { | |
| iid: 3114, | |
| }, | |
| { | |
| iid: 711, | |
| }, | |
| { | |
| iid: 1723, | |
| }, | |
| { | |
| iid: 2524, | |
| }, | |
| { | |
| iid: 4775, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 11, | |
| notes: [ | |
| { | |
| iid: 1926, | |
| }, | |
| { | |
| iid: 2421, | |
| }, | |
| { | |
| iid: 3630, | |
| }, | |
| { | |
| iid: 4918, | |
| }, | |
| { | |
| iid: 4466, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 9, | |
| notes: [ | |
| { | |
| iid: 3970, | |
| }, | |
| { | |
| iid: 2847, | |
| }, | |
| { | |
| iid: 4216, | |
| }, | |
| { | |
| iid: 3994, | |
| }, | |
| { | |
| iid: 3436, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 8, | |
| notes: [ | |
| { | |
| iid: 1791, | |
| }, | |
| { | |
| iid: 1928, | |
| }, | |
| { | |
| iid: 2056, | |
| }, | |
| { | |
| iid: 3180, | |
| }, | |
| { | |
| iid: 2105, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 7, | |
| notes: [ | |
| { | |
| iid: 4379, | |
| }, | |
| { | |
| iid: 3571, | |
| }, | |
| { | |
| iid: 3734, | |
| }, | |
| { | |
| iid: 2120, | |
| }, | |
| { | |
| iid: 810, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 6, | |
| notes: [ | |
| { | |
| iid: 4605, | |
| }, | |
| { | |
| iid: 45, | |
| }, | |
| { | |
| iid: 868, | |
| }, | |
| { | |
| iid: 2353, | |
| }, | |
| { | |
| iid: 66, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 5, | |
| notes: [ | |
| { | |
| iid: 2400, | |
| }, | |
| { | |
| iid: 198, | |
| }, | |
| { | |
| iid: 3771, | |
| }, | |
| { | |
| iid: 4033, | |
| }, | |
| { | |
| iid: 498, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 4, | |
| notes: [ | |
| { | |
| iid: 1261, | |
| }, | |
| { | |
| iid: 2032, | |
| }, | |
| { | |
| iid: 4373, | |
| }, | |
| { | |
| iid: 2778, | |
| }, | |
| { | |
| iid: 2262, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 3, | |
| notes: [ | |
| { | |
| iid: 3121, | |
| }, | |
| { | |
| iid: 2506, | |
| }, | |
| { | |
| iid: 2317, | |
| }, | |
| { | |
| iid: 540, | |
| }, | |
| { | |
| iid: 1249, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 2, | |
| notes: [ | |
| { | |
| iid: 3814, | |
| }, | |
| { | |
| iid: 954, | |
| }, | |
| { | |
| iid: 4441, | |
| }, | |
| { | |
| iid: 4755, | |
| }, | |
| { | |
| iid: 379, | |
| }, | |
| ], | |
| }, | |
| { | |
| iid: 1, | |
| notes: [ | |
| { | |
| iid: 3243, | |
| }, | |
| { | |
| iid: 650, | |
| }, | |
| { | |
| iid: 3103, | |
| }, | |
| { | |
| iid: 2622, | |
| }, | |
| { | |
| iid: 4957, | |
| }, | |
| ], | |
| }, | |
| ]; | |
| /** Simulate delays (network requests) */ | |
| function sleep(ms: number) { | |
| return new Promise((resolve) => setTimeout(resolve, ms)); | |
| } | |
| function randomInt(min: number, max: number) { | |
| return Math.random() * (max - min) + min; | |
| } | |
| function appendToLog(type: "issue" | "note", iid: number) { | |
| logStream.write(`${type} ${iid}\n`); | |
| } | |
| async function updateIssue(iid: number) { | |
| console.log("Updating issue", iid); | |
| await sleep(randomInt(1, 3) * 1000); | |
| const success = Math.random() < 0.5; | |
| if (success) { | |
| appendToLog("issue", iid); | |
| } else { | |
| throw new Error(`Failed to update issue ${iid} due to a simulated NetworkError`); | |
| } | |
| } | |
| async function updateNote(iid: number) { | |
| console.log("Updating note", iid); | |
| await sleep(randomInt(1, 2) * 1000); | |
| const success = Math.random() < 0.95; | |
| if (success) { | |
| appendToLog("note", iid); | |
| } else { | |
| throw new Error(`Failed to update note ${iid} due to a simulated NetworkError`); | |
| } | |
| } | |
| async function updateAllIssues() { | |
| try { | |
| for (const issue of issuesToUpdate) { | |
| // Skip updating issues if they exist in the log file. | |
| if (!alreadyUpdated.issues.has(issue.iid)) { | |
| await updateIssue(issue.iid); | |
| } else { | |
| console.log("Skipping issue", issue.iid, "because it was already updated"); | |
| } | |
| for (const note of issue.notes) { | |
| // Skip updating notes if they exist in the log file. | |
| if (!alreadyUpdated.notes.has(note.iid)) { | |
| await updateNote(note.iid); | |
| } else { | |
| console.log("Skipping note", note.iid, "because it was already updated"); | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| // Catch all errors to be able to exit gracefully | |
| console.error("Aborting due to error:", error); | |
| } finally { | |
| logStream.end(); | |
| } | |
| } | |
| async function main() { | |
| await updateAllIssues(); | |
| } | |
| await main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment