Files
session-mcp/scripts/prepare-all-projects.ts
Christian Gick 1227e5b339 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>
2026-02-07 12:33:49 +02:00

222 lines
7.0 KiB
TypeScript

#!/usr/bin/env npx tsx
/**
* Prepare all projects for exact-key migration (CF-762)
* For each project: delete → recreate → assign shared issue type scheme
* Then the migration script can run for all projects at once.
*
* Usage:
* npx tsx scripts/prepare-all-projects.ts [--dry-run] [--exclude CF]
*/
import pg from 'pg';
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 { Pool } = pg;
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');
const SHARED_SCHEME_ID = '10329'; // Agiliton Software Issue Type Scheme
const pool = new Pool({
host: process.env.POSTGRES_HOST || 'postgres.agiliton.internal',
port: 5432, database: 'agiliton', user: 'agiliton',
password: 'QtqiwCOAUpQNF6pjzOMAREzUny2bY8V1', max: 3,
});
const args = process.argv.slice(2);
const DRY_RUN = args.includes('--dry-run');
const excludeIdx = args.indexOf('--exclude');
const EXCLUDE = excludeIdx >= 0 ? args[excludeIdx + 1]?.split(',') || [] : [];
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function jiraFetch(path: string, options: RequestInit = {}): Promise<Response> {
return fetch(`${JIRA_URL}/rest/api/3${path}`, {
...options,
headers: {
Authorization: `Basic ${JIRA_AUTH}`,
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
},
});
}
async function getJiraProjects(): Promise<Array<{ key: string; name: string; id: string }>> {
const res = await jiraFetch('/project');
if (!res.ok) return [];
return res.json() as Promise<Array<{ key: string; name: string; id: string }>>;
}
async function deleteProject(key: string): Promise<boolean> {
const res = await jiraFetch(`/project/${key}?enableUndo=false`, { method: 'DELETE' });
return res.status === 204;
}
async function createProject(key: string, name: string, leadAccountId: string): Promise<string | null> {
const res = await jiraFetch('/project', {
method: 'POST',
body: JSON.stringify({
key,
name,
projectTypeKey: 'business',
leadAccountId,
assigneeType: 'UNASSIGNED',
}),
});
if (res.ok || res.status === 201) {
const data = await res.json() as { id: string };
return data.id;
}
console.error(` FAIL create ${key}: ${res.status} ${await res.text()}`);
return null;
}
async function assignScheme(projectId: string): Promise<boolean> {
const res = await jiraFetch('/issuetypescheme/project', {
method: 'PUT',
body: JSON.stringify({
issueTypeSchemeId: SHARED_SCHEME_ID,
projectId,
}),
});
return res.ok || res.status === 204;
}
async function verifyScheme(key: string): Promise<boolean> {
const res = await jiraFetch(`/project/${key}/statuses`);
if (!res.ok) return false;
const statuses = await res.json() as Array<{ name: string }>;
const names = statuses.map(s => s.name);
return names.includes('Epic') && names.includes('Task') && names.includes('Bug');
}
async function main() {
console.log('=== Prepare Projects for Migration ===');
console.log(`Mode: ${DRY_RUN ? 'DRY RUN' : 'LIVE'}`);
console.log(`Exclude: ${EXCLUDE.length > 0 ? EXCLUDE.join(', ') : 'none'}`);
console.log('');
// Get current user for project lead
const meRes = await jiraFetch('/myself');
const me = await meRes.json() as { accountId: string };
// Get existing Jira projects
const jiraProjects = await getJiraProjects();
const jiraProjectMap = new Map(jiraProjects.map(p => [p.key, p]));
console.log(`Jira projects: ${jiraProjects.length}`);
// Get DB projects with tasks
const dbProjects = await pool.query(
`SELECT p.key, p.name, COUNT(t.id) as task_count,
MAX(CAST(REGEXP_REPLACE(t.id, '^' || p.key || '-', '') AS INTEGER)) as max_id
FROM projects p
JOIN tasks t ON t.project = p.key
WHERE p.key ~ '^[A-Z]{2,5}$'
GROUP BY p.key, p.name
ORDER BY p.key`
);
console.log(`DB projects with tasks: ${dbProjects.rows.length}`);
console.log('');
// Filter: must exist in Jira, not excluded
const toProcess = dbProjects.rows.filter((p: any) => {
if (EXCLUDE.includes(p.key)) return false;
if (!jiraProjectMap.has(p.key)) return false;
return true;
});
console.log(`Projects to prepare: ${toProcess.length}`);
console.log('');
// Summary table
console.log('Project | Tasks | Max ID | Placeholders | Status');
console.log('--------|-------|--------|-------------|-------');
let totalTasks = 0;
let totalPlaceholders = 0;
for (const p of toProcess) {
const placeholders = p.max_id - p.task_count;
totalTasks += parseInt(p.task_count);
totalPlaceholders += placeholders;
console.log(`${p.key.padEnd(7)} | ${String(p.task_count).padStart(5)} | ${String(p.max_id).padStart(6)} | ${String(placeholders).padStart(11)} | pending`);
}
console.log(`TOTAL | ${String(totalTasks).padStart(5)} | ${String(totalTasks + totalPlaceholders).padStart(6)} | ${String(totalPlaceholders).padStart(11)} |`);
console.log('');
if (DRY_RUN) {
console.log('[DRY RUN] Would process above projects. Run without --dry-run to execute.');
await pool.end();
return;
}
// Process each project
let success = 0;
let failed = 0;
for (let i = 0; i < toProcess.length; i++) {
const p = toProcess[i];
const jiraProject = jiraProjectMap.get(p.key)!;
console.log(`[${i + 1}/${toProcess.length}] ${p.key} (${p.name})...`);
// 1. Delete
await delay(1000);
const deleted = await deleteProject(p.key);
if (!deleted) {
console.error(` FAIL delete ${p.key}`);
failed++;
continue;
}
console.log(` Deleted`);
// 2. Wait a bit for Jira to process
await delay(2000);
// 3. Recreate
const newId = await createProject(p.key, jiraProject.name || p.name, me.accountId);
if (!newId) {
console.error(` FAIL recreate ${p.key}`);
failed++;
continue;
}
console.log(` Recreated (id=${newId})`);
// 4. Assign shared scheme
await delay(1000);
const schemeOk = await assignScheme(newId);
if (!schemeOk) {
console.error(` FAIL assign scheme for ${p.key}`);
failed++;
continue;
}
// 5. Verify
const verified = await verifyScheme(p.key);
if (!verified) {
console.error(` FAIL verify scheme for ${p.key} (missing Epic/Task/Bug)`);
failed++;
continue;
}
console.log(` Scheme OK (Epic/Task/Bug)`);
success++;
}
console.log(`\n=== Preparation Summary ===`);
console.log(`Success: ${success}`);
console.log(`Failed: ${failed}`);
console.log(`\nRun migration: npx tsx scripts/migrate-tasks-to-jira.ts --skip-preflight`);
await pool.end();
}
main().catch(err => { console.error(err); process.exit(1); });