Created
March 22, 2024 02:19
-
-
Save ThomasChan/e0cc95a8353a311d0d4ef4cdba4a0ae1 to your computer and use it in GitHub Desktop.
Batch move self-hosted gitlab issues between milestones
This file contains 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
/** | |
* @author ThomasChan | |
* @usage | |
deno run --allow-net moveIssues.ts \ | |
--host=https://gitlab.test.org \ | |
--token=xxxx \ | |
--projectId=1 \ | |
--milestoneName="1.1" \ | |
--labelsToMatch="P2" \ | |
--labelsToExclude="customer,enhancement,feature" \ | |
--perPage=100 > log | |
*/ | |
import { Gitlab } from 'https://esm.sh/@gitbeaker/rest?dts'; | |
// parsing args | |
function parseArgs(args: string[]) { | |
const params: { [key: string]: string } = {}; | |
args.forEach(arg => { | |
const [key, value] = arg.split('='); | |
if (key.startsWith('--')) { | |
params[key.slice(2)] = value; | |
} | |
}); | |
return params; | |
} | |
const args = parseArgs(Deno.args); | |
// read variables | |
const host = args.host; | |
const token = args.token; | |
const projectId = parseInt(args.projectId); | |
const milestoneName = args.milestoneName; | |
const labelsToMatch = args.labelsToMatch.split(','); | |
const labelsToExclude = args.labelsToExclude.split(','); | |
const perPage = parseInt(args.perPage); | |
// GitLab API config | |
const api = new Gitlab({ | |
host: host, | |
token: token, | |
}); | |
if (Object.keys(args).length < 7) { | |
console.error(`Usage: | |
deno run --allow-net updateIssues.ts \ | |
--host=https://gitlab.test.org \ | |
--token=YOUR_PERSONAL_ACCESS_TOKEN \ | |
--projectId=YOUR_PROJECT_ID\ | |
--milestoneName=your_milestone_name \ | |
--labelsToMatch="P2,Auto Bug" \ | |
--labelsToExclude="customer,enhancement,feature" \ | |
--perPage=100 | |
Move all P3 issue to milestone 5.2: | |
deno run --allow-net updateIssues.ts \ | |
--host=https://gitlab.test.org \ | |
--token=YOUR_PERSONAL_ACCESS_TOKEN \ | |
--projectId=21 \ | |
--milestoneName="5.2" \ | |
--labelsToMatch="P3" \ | |
--labelsToExclude="customer,enhancement,feature" \ | |
--perPage=100 | |
`); | |
Deno.exit(1); | |
} | |
// record processed issues | |
const processedIssues = new Set(); | |
// get Milestone ID | |
async function getMilestoneId() { | |
const milestones = await api.ProjectMilestones.all(projectId); | |
const milestone = milestones.find(m => m.title === milestoneName); | |
return milestone ? milestone.id : null; | |
} | |
// get and update issues | |
async function updateIssues() { | |
let page = 1; | |
let issuesUpdated = 0; | |
while (true) { | |
try { | |
console.log(`Processing page ${page}...`); | |
const issues = await api.Issues.all({ | |
projectId: projectId, | |
labels: labelsToMatch.join(','), | |
scope: 'all', | |
state: 'opened', | |
perPage: perPage, | |
page: page, | |
}); | |
if (issues.length === 0) { | |
break; // no more issues to process, end | |
} | |
for (const issue of issues) { | |
// skip already processed issues | |
if (processedIssues.has(issue.iid)) { | |
continue; | |
} | |
// skip issues which already in target milestone | |
if (issue.milestone && issue.milestone.id === milestoneId) { | |
continue; | |
} | |
// skip issues which labels matches exclude | |
const issueLabels = issue.labels || []; | |
const shouldExclude = labelsToExclude.some(excludeLabel => issueLabels.includes(excludeLabel)); | |
if (shouldExclude) { | |
continue; | |
} | |
// update issue milestone | |
await api.Issues.edit(projectId, issue.iid, { | |
milestone_id: milestoneId, | |
}); | |
console.log(`Issue #${issue.iid} updated to milestone ${milestoneName}`); | |
issuesUpdated++; | |
processedIssues.add(issue.iid); // record issue iid | |
} | |
page++; | |
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for 1 sec | |
} catch (error) { | |
console.error(`Error fetching issues: ${error}`); | |
throw error; | |
} | |
} | |
console.log(`Total issues updated: ${issuesUpdated}`); | |
} | |
const milestoneId = await getMilestoneId(); | |
if (!milestoneId) { | |
console.error('Milestone not found'); | |
Deno.exit(1); | |
} | |
async function retryUpdateIssues(retries = 5) { | |
try { | |
await updateIssues(); | |
} catch (error) { | |
console.error(`Error updating issues: ${error}`); | |
if (retries > 0) { | |
console.log(`Retrying... (${retries} retries left)`); | |
await new Promise(resolve => setTimeout(resolve, 10000)); | |
await retryUpdateIssues(retries - 1); | |
} else { | |
console.error('Max retries reached. Exiting.'); | |
} | |
} | |
} | |
// start | |
retryUpdateIssues() | |
.then(() => { | |
console.log('Processed total issues', processedIssues.size); | |
Deno.exit(0); | |
}) | |
.catch(e => { | |
console.error(e); | |
console.log('Processed total issues', processedIssues.size); | |
Deno.exit(1); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment