feat(CF-762): Complete Jira migration - consolidate projects, cleanup
- 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>
This commit is contained in:
213
scripts/jira-admin.ts
Normal file
213
scripts/jira-admin.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/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); });
|
||||
Reference in New Issue
Block a user