Skip to content

Instantly share code, notes, and snippets.

@ThomasChan
Created March 22, 2024 02:19
Show Gist options
  • Save ThomasChan/e0cc95a8353a311d0d4ef4cdba4a0ae1 to your computer and use it in GitHub Desktop.
Save ThomasChan/e0cc95a8353a311d0d4ef4cdba4a0ae1 to your computer and use it in GitHub Desktop.
Batch move self-hosted gitlab issues between milestones
/**
* @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