- Remove task CRUD/epic/search/relation/version tools (moved to Jira) - Add migration scripts: migrate-tasks-to-jira, jira-admin, prepare-all-projects - Add consolidate-projects.ts for merging duplicate Jira projects - Add validate-migration.ts for post-migration integrity checks - Add jira_issue_key columns migration (030) - Consolidate 11 duplicate projects (LIT→LITE, CARD→CS, etc.) - Delete 92 placeholder issues, 11 empty source projects - Remove SG project completely - 2,798 tasks migrated across 46 Jira projects Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
214 lines
7.4 KiB
TypeScript
214 lines
7.4 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
/**
|
|
* Jira admin helper for migration (CF-762)
|
|
* Usage:
|
|
* npx tsx scripts/jira-admin.ts get-project CF
|
|
* npx tsx scripts/jira-admin.ts delete-project CF
|
|
* npx tsx scripts/jira-admin.ts create-project CF "Claude Framework"
|
|
* npx tsx scripts/jira-admin.ts count-issues CF
|
|
* npx tsx scripts/jira-admin.ts delete-all-issues CF
|
|
*/
|
|
|
|
import dotenv from 'dotenv';
|
|
import { dirname, join } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
dotenv.config({ path: join(__dirname, '..', '.env'), override: true });
|
|
|
|
const JIRA_URL = process.env.JIRA_URL || 'https://agiliton.atlassian.net';
|
|
const JIRA_USER = process.env.JIRA_USERNAME || process.env.JIRA_EMAIL || '';
|
|
const JIRA_TOKEN = process.env.JIRA_API_TOKEN || '';
|
|
const JIRA_AUTH = Buffer.from(`${JIRA_USER}:${JIRA_TOKEN}`).toString('base64');
|
|
|
|
async function jiraFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
const url = path.startsWith('http') ? path : `${JIRA_URL}/rest/api/3${path}`;
|
|
return fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Authorization': `Basic ${JIRA_AUTH}`,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
}
|
|
|
|
function delay(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
const [command, ...cmdArgs] = process.argv.slice(2);
|
|
|
|
async function main() {
|
|
switch (command) {
|
|
case 'get-project': {
|
|
const key = cmdArgs[0];
|
|
const res = await jiraFetch(`/project/${key}`);
|
|
if (!res.ok) {
|
|
console.error(`Failed: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
const data = await res.json() as Record<string, unknown>;
|
|
console.log(JSON.stringify(data, null, 2));
|
|
break;
|
|
}
|
|
|
|
case 'list-projects': {
|
|
const res = await jiraFetch('/project');
|
|
if (!res.ok) {
|
|
console.error(`Failed: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
const projects = await res.json() as Array<{ key: string; name: string; id: string; projectTypeKey: string }>;
|
|
console.log(`Total: ${projects.length} projects`);
|
|
for (const p of projects) {
|
|
console.log(` ${p.key}: ${p.name} (id=${p.id}, type=${p.projectTypeKey})`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'count-issues': {
|
|
const key = cmdArgs[0];
|
|
const res = await jiraFetch(`/search/jql`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ jql: `project="${key}"`, maxResults: 1 }),
|
|
});
|
|
if (!res.ok) {
|
|
console.error(`Failed: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
const data = await res.json() as { total: number };
|
|
console.log(`${key}: ${data.total} issues`);
|
|
break;
|
|
}
|
|
|
|
case 'list-issues': {
|
|
const key = cmdArgs[0];
|
|
const max = parseInt(cmdArgs[1] || '20');
|
|
const res = await jiraFetch(`/search/jql`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ jql: `project="${key}" ORDER BY key ASC`, maxResults: max, fields: ['key', 'summary', 'issuetype', 'status'] }),
|
|
});
|
|
if (!res.ok) {
|
|
console.error(`Failed: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
const data = await res.json() as { total: number; issues: Array<{ key: string; fields: { summary: string; issuetype: { name: string }; status: { name: string } } }> };
|
|
console.log(`${key}: ${data.total} total issues (showing ${data.issues.length})`);
|
|
for (const i of data.issues) {
|
|
console.log(` ${i.key} [${i.fields.issuetype.name}] ${i.fields.status.name}: ${i.fields.summary.substring(0, 60)}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'delete-all-issues': {
|
|
const key = cmdArgs[0];
|
|
if (!key) { console.error('Usage: delete-all-issues <PROJECT_KEY>'); return; }
|
|
|
|
// Get all issues
|
|
let startAt = 0;
|
|
const allKeys: string[] = [];
|
|
while (true) {
|
|
const res = await jiraFetch(`/search/jql`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ jql: `project="${key}" ORDER BY key ASC`, maxResults: 100, startAt, fields: ['key'] }),
|
|
});
|
|
if (!res.ok) { console.error(`Failed: ${res.status} ${await res.text()}`); return; }
|
|
const data = await res.json() as { total: number; issues: Array<{ key: string }> };
|
|
if (data.issues.length === 0) break;
|
|
allKeys.push(...data.issues.map(i => i.key));
|
|
startAt += data.issues.length;
|
|
if (startAt >= data.total) break;
|
|
}
|
|
|
|
console.log(`Found ${allKeys.length} issues to delete in ${key}`);
|
|
|
|
for (let i = 0; i < allKeys.length; i++) {
|
|
await delay(300);
|
|
const res = await jiraFetch(`/issue/${allKeys[i]}`, { method: 'DELETE' });
|
|
if (!res.ok) {
|
|
console.error(` FAIL delete ${allKeys[i]}: ${res.status}`);
|
|
}
|
|
if (i % 10 === 0) console.log(` [${i + 1}/${allKeys.length}] Deleted ${allKeys[i]}`);
|
|
}
|
|
console.log(`Deleted ${allKeys.length} issues from ${key}`);
|
|
break;
|
|
}
|
|
|
|
case 'delete-project': {
|
|
const key = cmdArgs[0];
|
|
if (!key) { console.error('Usage: delete-project <PROJECT_KEY>'); return; }
|
|
|
|
// enableUndo=false for permanent deletion
|
|
const res = await jiraFetch(`/project/${key}?enableUndo=false`, { method: 'DELETE' });
|
|
if (res.status === 204) {
|
|
console.log(`Project ${key} deleted permanently`);
|
|
} else {
|
|
console.error(`Failed: ${res.status} ${await res.text()}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'create-project': {
|
|
const key = cmdArgs[0];
|
|
const name = cmdArgs[1] || key;
|
|
if (!key) { console.error('Usage: create-project <KEY> <NAME>'); return; }
|
|
|
|
// Get current user account ID for lead
|
|
const meRes = await jiraFetch('/myself');
|
|
const me = await meRes.json() as { accountId: string };
|
|
|
|
const body = {
|
|
key,
|
|
name,
|
|
projectTypeKey: 'business',
|
|
leadAccountId: me.accountId,
|
|
assigneeType: 'UNASSIGNED',
|
|
};
|
|
|
|
const res = await jiraFetch('/project', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (res.ok || res.status === 201) {
|
|
const data = await res.json() as { id: string; key: string };
|
|
console.log(`Project created: ${data.key} (id=${data.id})`);
|
|
} else {
|
|
console.error(`Failed: ${res.status} ${await res.text()}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'get-schemes': {
|
|
const key = cmdArgs[0];
|
|
// Get issue type scheme for project
|
|
const res = await jiraFetch(`/project/${key}`);
|
|
if (!res.ok) {
|
|
console.error(`Failed: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
const data = await res.json() as Record<string, unknown>;
|
|
console.log('Project type:', (data as any).projectTypeKey);
|
|
console.log('Style:', (data as any).style);
|
|
|
|
// Get issue types
|
|
const itRes = await jiraFetch(`/project/${key}/statuses`);
|
|
if (itRes.ok) {
|
|
const itData = await itRes.json() as Array<{ name: string; id: string; statuses: Array<{ name: string }> }>;
|
|
console.log('\nIssue types and statuses:');
|
|
for (const it of itData) {
|
|
console.log(` ${it.name} (id=${it.id}): ${it.statuses.map(s => s.name).join(', ')}`);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
console.log('Commands: list-projects, get-project, count-issues, list-issues, delete-all-issues, delete-project, create-project, get-schemes');
|
|
}
|
|
}
|
|
|
|
main().catch(err => { console.error(err); process.exit(1); });
|