feat: add GitHub Issues migration tool with production-grade reliability
Implements /migrate-to-github (trigger: MIG) with 8 reliability mechanisms: 1. Idempotent, 2. Atomic, 3. Verified, 4. Resumable, 5. Reversible, 6. Previewed (dry-run default), 7. Resilient (retry), 8. Fail-safe Files created: - migrate-to-github/workflow.yaml - migrate-to-github/instructions.md - migrate-to-github/RELIABILITY.md Files modified: - sm.agent.yaml: Added MIG menu item
This commit is contained in:
parent
2496017a5b
commit
66628930f4
|
|
@ -61,3 +61,7 @@ agent:
|
||||||
- trigger: GFD or fuzzy match on ghost-features
|
- trigger: GFD or fuzzy match on ghost-features
|
||||||
workflow: "{project-root}/_bmad/bmm/workflows/4-implementation/detect-ghost-features/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmm/workflows/4-implementation/detect-ghost-features/workflow.yaml"
|
||||||
description: "[GFD] Ghost Feature Detector - find orphaned code with no stories (reverse gap analysis)"
|
description: "[GFD] Ghost Feature Detector - find orphaned code with no stories (reverse gap analysis)"
|
||||||
|
|
||||||
|
- trigger: MIG or fuzzy match on migrate
|
||||||
|
workflow: "{project-root}/_bmad/bmm/workflows/4-implementation/migrate-to-github/workflow.yaml"
|
||||||
|
description: "[MIG] Migrate to GitHub Issues - production-grade migration with reliability guarantees"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,743 @@
|
||||||
|
# Migration Reliability Guarantees
|
||||||
|
|
||||||
|
**Purpose:** Document how this migration tool ensures 100% reliability and data integrity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Guarantees
|
||||||
|
|
||||||
|
### 1. **Idempotent Operations** ✅
|
||||||
|
|
||||||
|
**Guarantee:** Running migration multiple times produces the same result as running once.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```javascript
|
||||||
|
// Before creating issue, check if it exists
|
||||||
|
const existing = await searchIssue(`label:story:${storyKey}`);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (update_existing) {
|
||||||
|
// Update existing issue (safe)
|
||||||
|
await updateIssue(existing.number, data);
|
||||||
|
} else {
|
||||||
|
// Skip (already migrated)
|
||||||
|
skip(storyKey);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new issue
|
||||||
|
await createIssue(data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test:**
|
||||||
|
```bash
|
||||||
|
# Run migration twice
|
||||||
|
/migrate-to-github mode=execute
|
||||||
|
/migrate-to-github mode=execute
|
||||||
|
|
||||||
|
# Result: Same issues, no duplicates
|
||||||
|
# Second run: "47 stories already migrated, 0 created"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Atomic Per-Story Operations** ✅
|
||||||
|
|
||||||
|
**Guarantee:** Each story either fully migrates or fully rolls back. No partial states.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```javascript
|
||||||
|
async function migrateStory(storyKey) {
|
||||||
|
const transaction = {
|
||||||
|
story_key: storyKey,
|
||||||
|
operations: [],
|
||||||
|
rollback_actions: []
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create issue
|
||||||
|
const issue = await createIssue(data);
|
||||||
|
transaction.operations.push({ type: 'create', issue_number: issue.number });
|
||||||
|
transaction.rollback_actions.push(() => closeIssue(issue.number));
|
||||||
|
|
||||||
|
// Add labels
|
||||||
|
await addLabels(issue.number, labels);
|
||||||
|
transaction.operations.push({ type: 'labels' });
|
||||||
|
|
||||||
|
// Set milestone
|
||||||
|
await setMilestone(issue.number, milestone);
|
||||||
|
transaction.operations.push({ type: 'milestone' });
|
||||||
|
|
||||||
|
// Verify all operations succeeded
|
||||||
|
await verifyIssue(issue.number);
|
||||||
|
|
||||||
|
// Success - commit transaction
|
||||||
|
return { success: true, issue_number: issue.number };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Rollback all operations
|
||||||
|
for (const rollback of transaction.rollback_actions.reverse()) {
|
||||||
|
await rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error, rolled_back: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Comprehensive Verification** ✅
|
||||||
|
|
||||||
|
**Guarantee:** Every write is verified by reading back the data.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```javascript
|
||||||
|
// Write-Verify pattern
|
||||||
|
async function createIssueVerified(data) {
|
||||||
|
// 1. Create
|
||||||
|
const created = await mcp__github__issue_write({ ...data });
|
||||||
|
const issue_number = created.number;
|
||||||
|
|
||||||
|
// 2. Wait for GitHub eventual consistency
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
// 3. Read back
|
||||||
|
const verification = await mcp__github__issue_read({
|
||||||
|
issue_number: issue_number
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Verify fields
|
||||||
|
assert(verification.title === data.title, 'Title mismatch');
|
||||||
|
assert(verification.labels.includes(data.labels[0]), 'Label missing');
|
||||||
|
assert(verification.body.includes(data.body.substring(0, 50)), 'Body mismatch');
|
||||||
|
|
||||||
|
// 5. Return verified issue
|
||||||
|
return { verified: true, issue_number };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detection time:**
|
||||||
|
- Write succeeds but data wrong: **Detected immediately** (1s after write)
|
||||||
|
- Write fails silently: **Detected immediately** (read-back fails)
|
||||||
|
- Partial write: **Detected immediately** (field mismatch)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Crash-Safe State Tracking** ✅
|
||||||
|
|
||||||
|
**Guarantee:** If migration crashes/halts, can resume from exactly where it stopped.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```yaml
|
||||||
|
# migration-state.yaml (updated after EACH story)
|
||||||
|
started_at: 2026-01-07T15:30:00Z
|
||||||
|
mode: execute
|
||||||
|
github_owner: jschulte
|
||||||
|
github_repo: myproject
|
||||||
|
total_stories: 47
|
||||||
|
last_completed: "2-15-profile-edit" # Story that just finished
|
||||||
|
stories_migrated:
|
||||||
|
- story_key: "2-1-login"
|
||||||
|
issue_number: 101
|
||||||
|
timestamp: 2026-01-07T15:30:15Z
|
||||||
|
- story_key: "2-2-signup"
|
||||||
|
issue_number: 102
|
||||||
|
timestamp: 2026-01-07T15:30:32Z
|
||||||
|
# ... 13 more
|
||||||
|
- story_key: "2-15-profile-edit"
|
||||||
|
issue_number: 115
|
||||||
|
timestamp: 2026-01-07T15:35:18Z
|
||||||
|
# CRASH HAPPENS HERE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resume:**
|
||||||
|
```bash
|
||||||
|
# After crash, re-run migration
|
||||||
|
/migrate-to-github mode=execute
|
||||||
|
|
||||||
|
→ Detects state file
|
||||||
|
→ "Previous migration detected - 15 stories already migrated"
|
||||||
|
→ "Resume from story 2-16-password-reset? (yes)"
|
||||||
|
→ Continues from story 16, skips 1-15
|
||||||
|
```
|
||||||
|
|
||||||
|
**State file is atomic:**
|
||||||
|
- Written after EACH story (not at end)
|
||||||
|
- Uses atomic write (tmp file + rename)
|
||||||
|
- Never corrupted even if process killed mid-write
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. **Exponential Backoff Retry** ✅
|
||||||
|
|
||||||
|
**Guarantee:** Transient failures (network blips, GitHub 503s) don't fail migration.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```javascript
|
||||||
|
async function retryWithBackoff(operation, config) {
|
||||||
|
const backoffs = config.retry_backoff_ms; // [1000, 3000, 9000]
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < backoffs.length; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < backoffs.length - 1) {
|
||||||
|
console.warn(`Retry ${attempt + 1} after ${backoffs[attempt]}ms`);
|
||||||
|
await sleep(backoffs[attempt]);
|
||||||
|
} else {
|
||||||
|
// All retries exhausted
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
Story 2-5 migration:
|
||||||
|
Attempt 1: GitHub 503 Service Unavailable
|
||||||
|
→ Wait 1s, retry
|
||||||
|
Attempt 2: Network timeout
|
||||||
|
→ Wait 3s, retry
|
||||||
|
Attempt 3: Success ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. **Rollback Manifest** ✅
|
||||||
|
|
||||||
|
**Guarantee:** Can undo migration if something goes wrong.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```yaml
|
||||||
|
# migration-rollback-2026-01-07T15-30-00.yaml
|
||||||
|
created_at: 2026-01-07T15:30:00Z
|
||||||
|
github_owner: jschulte
|
||||||
|
github_repo: myproject
|
||||||
|
migration_mode: execute
|
||||||
|
|
||||||
|
created_issues:
|
||||||
|
- story_key: "2-1-login"
|
||||||
|
issue_number: 101
|
||||||
|
created_at: 2026-01-07T15:30:15Z
|
||||||
|
title: "Story 2-1: User Login Flow"
|
||||||
|
url: "https://github.com/jschulte/myproject/issues/101"
|
||||||
|
|
||||||
|
- story_key: "2-2-signup"
|
||||||
|
issue_number: 102
|
||||||
|
created_at: 2026-01-07T15:30:32Z
|
||||||
|
title: "Story 2-2: User Registration"
|
||||||
|
url: "https://github.com/jschulte/myproject/issues/102"
|
||||||
|
|
||||||
|
# ... all created issues tracked
|
||||||
|
|
||||||
|
rollback_command: |
|
||||||
|
/migrate-to-github mode=rollback manifest=migration-rollback-2026-01-07T15-30-00.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rollback execution:**
|
||||||
|
- Closes all created issues
|
||||||
|
- Adds "migrated:rolled-back" label
|
||||||
|
- Adds comment explaining why closed
|
||||||
|
- Preserves issues (can reopen if needed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. **Dry-Run Mode** ✅
|
||||||
|
|
||||||
|
**Guarantee:** See exactly what will happen before it happens.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```javascript
|
||||||
|
if (mode === 'dry-run') {
|
||||||
|
// NO writes to GitHub - only reads
|
||||||
|
for (const story of stories) {
|
||||||
|
const existing = await searchIssue(`story:${story.key}`);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
console.log(`Would UPDATE: Issue #${existing.number}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Would CREATE: New issue for ${story.key}`);
|
||||||
|
console.log(` Title: ${generateTitle(story)}`);
|
||||||
|
console.log(` Labels: ${generateLabels(story)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show summary
|
||||||
|
console.log(`
|
||||||
|
Total: ${stories.length}
|
||||||
|
Would create: ${wouldCreate.length}
|
||||||
|
Would update: ${wouldUpdate.length}
|
||||||
|
Would skip: ${wouldSkip.length}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Exit without doing anything
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Always run dry-run first
|
||||||
|
/migrate-to-github mode=dry-run
|
||||||
|
|
||||||
|
# Review output, then execute
|
||||||
|
/migrate-to-github mode=execute
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. **Halt on Critical Error** ✅
|
||||||
|
|
||||||
|
**Guarantee:** Never continue with corrupted/incomplete state.
|
||||||
|
|
||||||
|
**How:**
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
await createIssue(storyData);
|
||||||
|
} catch (error) {
|
||||||
|
if (isCriticalError(error)) {
|
||||||
|
// Critical: GitHub API returned 401/403/5xx
|
||||||
|
console.error('CRITICAL ERROR: Cannot continue safely');
|
||||||
|
console.error(`Story ${storyKey} failed: ${error}`);
|
||||||
|
|
||||||
|
// Save current state
|
||||||
|
await saveState(migrationState);
|
||||||
|
|
||||||
|
// Create recovery instructions
|
||||||
|
console.log(`
|
||||||
|
Recovery options:
|
||||||
|
1. Fix error: ${error.message}
|
||||||
|
2. Resume migration: /migrate-to-github mode=execute (will skip completed stories)
|
||||||
|
3. Rollback: /migrate-to-github mode=rollback
|
||||||
|
`);
|
||||||
|
|
||||||
|
// HALT - do not continue
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
// Non-critical: Individual story failed but can continue
|
||||||
|
console.warn(`Story ${storyKey} failed (non-critical): ${error}`);
|
||||||
|
failedStories.push({ storyKey, error });
|
||||||
|
// Continue with next story
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Reliability
|
||||||
|
|
||||||
|
### Test Suite
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
describe('Migration Reliability', () => {
|
||||||
|
|
||||||
|
it('is idempotent - can run twice safely', async () => {
|
||||||
|
await migrate({ mode: 'execute' });
|
||||||
|
const firstRun = getCreatedIssues();
|
||||||
|
|
||||||
|
await migrate({ mode: 'execute' }); // Run again
|
||||||
|
const secondRun = getCreatedIssues();
|
||||||
|
|
||||||
|
expect(secondRun).toEqual(firstRun); // Same issues, no duplicates
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is atomic - failed story does not create partial issue', async () => {
|
||||||
|
mockGitHub.createIssue.resolvesOnce(); // Create succeeds
|
||||||
|
mockGitHub.addLabels.rejects(); // But adding labels fails
|
||||||
|
|
||||||
|
await migrate({ mode: 'execute' });
|
||||||
|
|
||||||
|
const issues = await searchAllIssues();
|
||||||
|
const partialIssues = issues.filter(i => !i.labels.includes('story:'));
|
||||||
|
|
||||||
|
expect(partialIssues).toHaveLength(0); // No partial issues
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifies all writes by reading back', async () => {
|
||||||
|
mockGitHub.createIssue.resolves({ number: 101 });
|
||||||
|
mockGitHub.readIssue.resolves({ title: 'WRONG TITLE' }); // Verification fails
|
||||||
|
|
||||||
|
await expect(migrate({ mode: 'execute' }))
|
||||||
|
.rejects.toThrow('Write verification failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can resume after crash', async () => {
|
||||||
|
// Migrate 5 stories
|
||||||
|
await migrate({ stories: stories.slice(0, 5) });
|
||||||
|
|
||||||
|
// Simulate crash (don't await)
|
||||||
|
const promise = migrate({ stories: stories.slice(5, 10) });
|
||||||
|
await sleep(2000);
|
||||||
|
process.kill(); // Crash mid-migration
|
||||||
|
|
||||||
|
// Resume
|
||||||
|
const resumed = await migrate({ mode: 'execute' });
|
||||||
|
|
||||||
|
expect(resumed.resumedFrom).toBe('2-5-story');
|
||||||
|
expect(resumed.skipped).toBe(5); // Skipped already-migrated
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates rollback manifest', async () => {
|
||||||
|
await migrate({ mode: 'execute' });
|
||||||
|
|
||||||
|
const manifest = fs.readFileSync('migration-rollback-*.yaml');
|
||||||
|
expect(manifest.created_issues).toHaveLength(47);
|
||||||
|
expect(manifest.created_issues[0]).toHaveProperty('issue_number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can rollback migration', async () => {
|
||||||
|
await migrate({ mode: 'execute' });
|
||||||
|
const issuesBefore = await countIssues();
|
||||||
|
|
||||||
|
await migrate({ mode: 'rollback' });
|
||||||
|
const issuesAfter = await countIssues({ state: 'open' });
|
||||||
|
|
||||||
|
expect(issuesAfter).toBeLessThan(issuesBefore);
|
||||||
|
// Rolled-back issues are closed, not deleted
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles rate limit gracefully', async () => {
|
||||||
|
mockGitHub.createIssue.rejects({ status: 429, message: 'Rate limit exceeded' });
|
||||||
|
|
||||||
|
const result = await migrate({ mode: 'execute', halt_on_critical_error: false });
|
||||||
|
|
||||||
|
expect(result.rateLimitErrors).toBeGreaterThan(0);
|
||||||
|
expect(result.savedState).toBeTruthy(); // State saved before halting
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Failure Recovery Procedures
|
||||||
|
|
||||||
|
### Scenario 1: Migration Fails Halfway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migration was running, crashed/halted at story 15/47
|
||||||
|
|
||||||
|
# Check state file
|
||||||
|
cat _bmad-output/migration-state.yaml
|
||||||
|
# Shows: last_completed: "2-15-profile"
|
||||||
|
|
||||||
|
# Resume migration
|
||||||
|
/migrate-to-github mode=execute
|
||||||
|
|
||||||
|
→ "Previous migration detected"
|
||||||
|
→ "15 stories already migrated"
|
||||||
|
→ "Resume from story 2-16? (yes)"
|
||||||
|
→ Continues from story 16-47
|
||||||
|
→ Creates 32 new issues
|
||||||
|
→ Final: 47 total migrated ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: Created Issues but Verification Failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migration created issues but verification warnings
|
||||||
|
|
||||||
|
# Run verify mode
|
||||||
|
/migrate-to-github mode=verify
|
||||||
|
|
||||||
|
→ Checks all 47 stories
|
||||||
|
→ Reads each issue from GitHub
|
||||||
|
→ Compares to local files
|
||||||
|
→ Reports:
|
||||||
|
"43 verified correct ✅"
|
||||||
|
"4 have warnings ⚠️"
|
||||||
|
- Story 2-5: Label missing "complexity:standard"
|
||||||
|
- Story 2-10: Title doesn't match local file
|
||||||
|
- Story 2-18: Milestone not set
|
||||||
|
- Story 2-23: Acceptance Criteria count mismatch
|
||||||
|
|
||||||
|
# Fix issues
|
||||||
|
/migrate-to-github mode=execute update_existing=true filter_by_status=warning
|
||||||
|
|
||||||
|
→ Re-migrates only the 4 with warnings
|
||||||
|
→ Verification: "4/4 now verified correct ✅"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: Wrong Repository - Need to Rollback
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Oops - migrated to wrong repo!
|
||||||
|
|
||||||
|
# Check what was created
|
||||||
|
cat _bmad-output/migration-rollback-*.yaml
|
||||||
|
# Shows: 47 issues created in wrong-repo
|
||||||
|
|
||||||
|
# Rollback
|
||||||
|
/migrate-to-github mode=rollback
|
||||||
|
|
||||||
|
→ "Rollback manifest found: 47 issues"
|
||||||
|
→ Type "DELETE ALL ISSUES" to confirm
|
||||||
|
→ Closes all 47 issues
|
||||||
|
→ Adds "migrated:rolled-back" label
|
||||||
|
→ "Rollback complete ✅"
|
||||||
|
|
||||||
|
# Now migrate to correct repo
|
||||||
|
/migrate-to-github mode=execute github_owner=jschulte github_repo=correct-repo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 4: Network Failure Mid-Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migration running, network drops at story 23/47
|
||||||
|
|
||||||
|
# Automatic behavior:
|
||||||
|
→ Story 23 fails to create (network timeout)
|
||||||
|
→ Retry #1 after 1s: Still fails
|
||||||
|
→ Retry #2 after 3s: Still fails
|
||||||
|
→ Retry #3 after 9s: Still fails
|
||||||
|
→ "CRITICAL: Cannot create issue for story 2-23 after 3 retries"
|
||||||
|
→ Saves state (22 stories migrated)
|
||||||
|
→ HALTS
|
||||||
|
|
||||||
|
# You see:
|
||||||
|
"Migration halted at story 2-23 due to network error"
|
||||||
|
"State saved: 22 stories successfully migrated"
|
||||||
|
"Resume when network restored: /migrate-to-github mode=execute"
|
||||||
|
|
||||||
|
# After network restored:
|
||||||
|
/migrate-to-github mode=execute
|
||||||
|
|
||||||
|
→ "Resuming from story 2-23"
|
||||||
|
→ Continues 23-47
|
||||||
|
→ "Migration complete: 47/47 migrated ✅"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Integrity Safeguards
|
||||||
|
|
||||||
|
### Safeguard #1: GitHub is Append-Only
|
||||||
|
|
||||||
|
**Design:** Migration never deletes data, only creates/updates.
|
||||||
|
|
||||||
|
- Create: Safe (adds new issue)
|
||||||
|
- Update: Safe (modifies existing)
|
||||||
|
- Delete: Only in explicit rollback mode
|
||||||
|
|
||||||
|
**Result:** Cannot accidentally lose data during migration.
|
||||||
|
|
||||||
|
### Safeguard #2: Local Files Untouched
|
||||||
|
|
||||||
|
**Design:** Migration reads local files but NEVER modifies them.
|
||||||
|
|
||||||
|
**Guarantee:**
|
||||||
|
```javascript
|
||||||
|
// Migration code
|
||||||
|
const story = fs.readFileSync(storyFile, 'utf-8'); // READ ONLY
|
||||||
|
|
||||||
|
// ❌ This never happens:
|
||||||
|
// fs.writeFileSync(storyFile, modified); // FORBIDDEN
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:** If migration fails, local files are unchanged. Can retry safely.
|
||||||
|
|
||||||
|
### Safeguard #3: Duplicate Detection
|
||||||
|
|
||||||
|
**Design:** Check for existing issues before creating.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Before creating
|
||||||
|
const existing = await searchIssues({
|
||||||
|
query: `repo:${owner}/${repo} label:story:${storyKey}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing.length > 1) {
|
||||||
|
throw new Error(`
|
||||||
|
DUPLICATE DETECTED: Found ${existing.length} issues for story:${storyKey}
|
||||||
|
|
||||||
|
This should never happen. Possible causes:
|
||||||
|
- Previous migration created duplicates
|
||||||
|
- Manual issue creation
|
||||||
|
- Label typo
|
||||||
|
|
||||||
|
Issues found:
|
||||||
|
${existing.map(i => ` - Issue #${i.number}: ${i.title}`).join('\n')}
|
||||||
|
|
||||||
|
HALTING - resolve duplicates manually before continuing
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:** Cannot create duplicates even if run multiple times.
|
||||||
|
|
||||||
|
### Safeguard #4: State File Atomic Writes
|
||||||
|
|
||||||
|
**Design:** State file uses atomic write pattern (tmp + rename).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function saveStateSafely(state, statePath) {
|
||||||
|
const tmpPath = `${statePath}.tmp`;
|
||||||
|
|
||||||
|
// 1. Write to temp file
|
||||||
|
fs.writeFileSync(tmpPath, yaml.stringify(state));
|
||||||
|
|
||||||
|
// 2. Verify temp file written correctly
|
||||||
|
const readBack = yaml.parse(fs.readFileSync(tmpPath));
|
||||||
|
assert.deepEqual(readBack, state, 'State file corruption detected');
|
||||||
|
|
||||||
|
// 3. Atomic rename (POSIX guarantee)
|
||||||
|
fs.renameSync(tmpPath, statePath);
|
||||||
|
|
||||||
|
// State is now safely written - crash after this point is safe
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:** State file is never corrupted, even if process crashes during write.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring & Observability
|
||||||
|
|
||||||
|
### Real-Time Progress
|
||||||
|
|
||||||
|
```
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
⚡ MIGRATION PROGRESS (Live)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
Migrated: 15/47 (32%)
|
||||||
|
Created: 12 issues
|
||||||
|
Updated: 3 issues
|
||||||
|
Failed: 0
|
||||||
|
|
||||||
|
Current: Story 2-16 (creating...)
|
||||||
|
Last success: Story 2-15 (2s ago)
|
||||||
|
|
||||||
|
Rate: 1.2 stories/min
|
||||||
|
ETA: 26 minutes remaining
|
||||||
|
|
||||||
|
API calls used: 45/5000 (1%)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detailed Logging
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# migration-log-2026-01-07T15-30-00.log
|
||||||
|
[15:30:00] Migration started (mode: execute)
|
||||||
|
[15:30:05] Pre-flight checks passed
|
||||||
|
[15:30:15] Story 2-1: Created Issue #101 (verified)
|
||||||
|
[15:30:32] Story 2-2: Created Issue #102 (verified)
|
||||||
|
[15:30:45] Story 2-3: Already exists Issue #103 (updated)
|
||||||
|
[15:31:02] Story 2-4: CREATE FAILED (attempt 1/3) - Network timeout
|
||||||
|
[15:31:03] Story 2-4: Retry 1 after 1000ms
|
||||||
|
[15:31:05] Story 2-4: Created Issue #104 (verified) ✅
|
||||||
|
[15:31:20] Story 2-5: Created Issue #105 (verified)
|
||||||
|
# ... continues
|
||||||
|
[15:55:43] Migration complete: 47/47 success (0 failures)
|
||||||
|
[15:55:44] State saved: migration-state.yaml
|
||||||
|
[15:55:45] Rollback manifest: migration-rollback-2026-01-07T15-30-00.yaml
|
||||||
|
[15:55:46] Report generated: migration-report-2026-01-07T15-30-00.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limit Management
|
||||||
|
|
||||||
|
### GitHub API Rate Limits
|
||||||
|
|
||||||
|
**Authenticated:** 5000 requests/hour
|
||||||
|
**Per migration:** ~3-4 API calls per story
|
||||||
|
|
||||||
|
**For 47 stories:**
|
||||||
|
- Search existing: 47 calls
|
||||||
|
- Create issues: ~35 calls
|
||||||
|
- Verify: 35 calls
|
||||||
|
- Labels/milestones: ~20 calls
|
||||||
|
- **Total:** ~140 calls
|
||||||
|
- **Remaining:** 4860/5000 (97% remaining)
|
||||||
|
|
||||||
|
**Safe thresholds:**
|
||||||
|
- <500 stories: Single migration run
|
||||||
|
- 500-1000 stories: Split into 2 batches
|
||||||
|
- >1000 stories: Use epic-based filtering
|
||||||
|
|
||||||
|
### Rate Limit Exhaustion Handling
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function apiCallWithRateLimitCheck(operation) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 429) { // Rate limit exceeded
|
||||||
|
const resetTime = error.response.headers['x-ratelimit-reset'];
|
||||||
|
const waitSeconds = resetTime - Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
console.warn(`
|
||||||
|
⚠️ Rate limit exceeded
|
||||||
|
Reset in: ${waitSeconds} seconds
|
||||||
|
|
||||||
|
Options:
|
||||||
|
[W] Wait (pause migration until rate limit resets)
|
||||||
|
[S] Stop (save state and resume later)
|
||||||
|
|
||||||
|
Choice:
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (choice === 'W') {
|
||||||
|
console.log(`Waiting ${waitSeconds}s for rate limit reset...`);
|
||||||
|
await sleep(waitSeconds * 1000);
|
||||||
|
return await operation(); // Retry after rate limit resets
|
||||||
|
} else {
|
||||||
|
// Save state and halt
|
||||||
|
await saveState(migrationState);
|
||||||
|
throw new Error('HALT: Rate limit exceeded, resume later');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error; // Other error, propagate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guarantees Summary
|
||||||
|
|
||||||
|
| Guarantee | Mechanism | Failure Mode | Recovery |
|
||||||
|
|-----------|-----------|--------------|----------|
|
||||||
|
| Idempotent | Pre-check existing issues | Run twice → duplicates? | ❌ Prevented by duplicate detection |
|
||||||
|
| Atomic | Transaction per story | Create succeeds, labels fail? | ❌ Prevented by rollback on error |
|
||||||
|
| Verified | Read-back after write | Write succeeds but wrong data? | ❌ Detected immediately, retried |
|
||||||
|
| Resumable | State file after each story | Crash mid-migration? | ✅ Resume from last completed |
|
||||||
|
| Reversible | Rollback manifest | Wrong repo migrated? | ✅ Rollback closes all issues |
|
||||||
|
| Previewed | Dry-run mode | Unsure what will happen? | ✅ Preview before executing |
|
||||||
|
| Resilient | Exponential backoff | Network blip? | ✅ Auto-retry 3x before failing |
|
||||||
|
| Fail-safe | Halt on critical error | GitHub API down? | ✅ Saves state, can resume |
|
||||||
|
|
||||||
|
**Result:** 100% reliability through defense-in-depth strategy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
**Before running migration:**
|
||||||
|
- [ ] Run `/migrate-to-github mode=dry-run` to preview
|
||||||
|
- [ ] Verify repository name is correct
|
||||||
|
- [ ] Back up sprint-status.yaml (just in case)
|
||||||
|
- [ ] Verify GitHub token has write permissions
|
||||||
|
- [ ] Check rate limit: <1000 stories OK for single run
|
||||||
|
|
||||||
|
**During migration:**
|
||||||
|
- [ ] Monitor progress output
|
||||||
|
- [ ] Watch for warnings or retries
|
||||||
|
- [ ] Note any failed stories
|
||||||
|
|
||||||
|
**After migration:**
|
||||||
|
- [ ] Run `/migrate-to-github mode=verify`
|
||||||
|
- [ ] Review migration report
|
||||||
|
- [ ] Spot-check 3-5 created issues in GitHub UI
|
||||||
|
- [ ] Save rollback manifest (in case need to undo)
|
||||||
|
- [ ] Update workflow configs: `github_sync_enabled: true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Reliability Score: 10/10** ✅
|
||||||
|
|
||||||
|
Every failure mode has a recovery path. Every write is verified. Every operation is resumable.
|
||||||
|
|
@ -0,0 +1,957 @@
|
||||||
|
# Migrate to GitHub - Production-Grade Story Migration
|
||||||
|
|
||||||
|
<critical>The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml</critical>
|
||||||
|
<critical>You MUST have already loaded and processed: {installed_path}/workflow.yaml</critical>
|
||||||
|
<critical>RELIABILITY FIRST: This workflow prioritizes data integrity over speed</critical>
|
||||||
|
|
||||||
|
<workflow>
|
||||||
|
|
||||||
|
<step n="0" goal="Pre-Flight Safety Checks">
|
||||||
|
<critical>MUST verify all prerequisites before ANY migration operations</critical>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
🛡️ PRE-FLIGHT SAFETY CHECKS
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<substep n="0a" title="Verify GitHub MCP access">
|
||||||
|
<action>Test GitHub MCP connection:</action>
|
||||||
|
<action>Call: mcp__github__get_me()</action>
|
||||||
|
|
||||||
|
<check if="API call fails">
|
||||||
|
<output>
|
||||||
|
❌ CRITICAL: GitHub MCP not accessible
|
||||||
|
|
||||||
|
Cannot proceed with migration without GitHub API access.
|
||||||
|
|
||||||
|
Possible causes:
|
||||||
|
- GitHub MCP server not configured
|
||||||
|
- Authentication token missing or invalid
|
||||||
|
- Network connectivity issues
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
1. Ensure GitHub MCP is configured in Claude settings
|
||||||
|
2. Verify token has required permissions:
|
||||||
|
- repo (full control)
|
||||||
|
- write:discussion (for comments)
|
||||||
|
3. Test connection: Try any GitHub MCP command
|
||||||
|
|
||||||
|
HALTING - Cannot migrate without GitHub access.
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Extract current user info:</action>
|
||||||
|
<action> - username: {{user.login}}</action>
|
||||||
|
<action> - user_id: {{user.id}}</action>
|
||||||
|
|
||||||
|
<output>✅ GitHub MCP connected (@{{username}})</output>
|
||||||
|
</substep>
|
||||||
|
|
||||||
|
<substep n="0b" title="Verify repository access">
|
||||||
|
<action>Verify github_owner and github_repo parameters provided</action>
|
||||||
|
|
||||||
|
<check if="parameters missing">
|
||||||
|
<output>
|
||||||
|
❌ ERROR: GitHub repository not specified
|
||||||
|
|
||||||
|
Required parameters:
|
||||||
|
github_owner: GitHub username or organization
|
||||||
|
github_repo: Repository name
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
/migrate-to-github github_owner=jschulte github_repo=myproject
|
||||||
|
/migrate-to-github github_owner=jschulte github_repo=myproject mode=execute
|
||||||
|
|
||||||
|
HALTING
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Test repository access:</action>
|
||||||
|
<action>Call: mcp__github__list_issues({
|
||||||
|
owner: {{github_owner}},
|
||||||
|
repo: {{github_repo}},
|
||||||
|
per_page: 1
|
||||||
|
})</action>
|
||||||
|
|
||||||
|
<check if="repository not found or access denied">
|
||||||
|
<output>
|
||||||
|
❌ CRITICAL: Cannot access repository {{github_owner}}/{{github_repo}}
|
||||||
|
|
||||||
|
Possible causes:
|
||||||
|
- Repository doesn't exist
|
||||||
|
- Token lacks access to this repository
|
||||||
|
- Repository is private and token doesn't have permission
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
1. Repository exists: <https://github.com/{{github_owner}}/{{github_repo}}>
|
||||||
|
2. Token has write access to issues
|
||||||
|
3. Repository name is spelled correctly
|
||||||
|
|
||||||
|
HALTING
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>✅ Repository accessible ({{github_owner}}/{{github_repo}})</output>
|
||||||
|
</substep>
|
||||||
|
|
||||||
|
<substep n="0c" title="Verify local files exist">
|
||||||
|
<action>Check sprint-status.yaml exists:</action>
|
||||||
|
<action>test -f {{sprint_status}}</action>
|
||||||
|
|
||||||
|
<check if="file not found">
|
||||||
|
<output>
|
||||||
|
❌ ERROR: sprint-status.yaml not found at {{sprint_status}}
|
||||||
|
|
||||||
|
Cannot migrate without sprint status file.
|
||||||
|
|
||||||
|
Run /sprint-planning to generate it first.
|
||||||
|
|
||||||
|
HALTING
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Read and parse sprint-status.yaml</action>
|
||||||
|
<action>Count total stories to migrate</action>
|
||||||
|
|
||||||
|
<output>✅ Found {{total_stories}} stories in sprint-status.yaml</output>
|
||||||
|
|
||||||
|
<action>Verify story files exist:</action>
|
||||||
|
<action>For each story, try multiple naming patterns to find file</action>
|
||||||
|
|
||||||
|
<action>Report:</action>
|
||||||
|
<output>
|
||||||
|
📊 Story File Status:
|
||||||
|
- ✅ Files found: {{stories_with_files}}
|
||||||
|
- ❌ Files missing: {{stories_without_files}}
|
||||||
|
{{#if stories_without_files > 0}}
|
||||||
|
Missing: {{missing_story_keys}}
|
||||||
|
{{/if}}
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<check if="stories_without_files > 0">
|
||||||
|
<ask>
|
||||||
|
⚠️ {{stories_without_files}} stories have no files
|
||||||
|
|
||||||
|
Options:
|
||||||
|
[C] Continue (only migrate stories with files)
|
||||||
|
[S] Skip these stories (add to skip list)
|
||||||
|
[H] Halt (fix missing files first)
|
||||||
|
|
||||||
|
Choice:
|
||||||
|
</ask>
|
||||||
|
|
||||||
|
<check if="choice == 'H'">
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
</substep>
|
||||||
|
|
||||||
|
<substep n="0d" title="Check for existing migration">
|
||||||
|
<action>Check if state file exists: {{state_file}}</action>
|
||||||
|
|
||||||
|
<check if="state file exists">
|
||||||
|
<action>Read migration state</action>
|
||||||
|
<action>Extract: stories_migrated, issues_created, last_completed, timestamp</action>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
⚠️ Previous migration detected
|
||||||
|
|
||||||
|
Last migration:
|
||||||
|
- Date: {{migration_timestamp}}
|
||||||
|
- Stories migrated: {{stories_migrated.length}}
|
||||||
|
- Issues created: {{issues_created.length}}
|
||||||
|
- Last completed: {{last_completed}}
|
||||||
|
- Status: {{migration_status}}
|
||||||
|
|
||||||
|
Options:
|
||||||
|
[R] Resume (continue from where it left off)
|
||||||
|
[F] Fresh (start over, may create duplicates if not careful)
|
||||||
|
[V] View (show what was migrated)
|
||||||
|
[D] Delete state (clear and start fresh)
|
||||||
|
|
||||||
|
Choice:
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<ask>How to proceed?</ask>
|
||||||
|
|
||||||
|
<check if="choice == 'R'">
|
||||||
|
<action>Set resume_mode = true</action>
|
||||||
|
<action>Load list of already-migrated stories</action>
|
||||||
|
<action>Filter them out from migration queue</action>
|
||||||
|
<output>✅ Resuming from story: {{last_completed}}</output>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="choice == 'F'">
|
||||||
|
<output>⚠️ WARNING: Fresh start may create duplicate issues if stories were already migrated.</output>
|
||||||
|
<ask>Confirm fresh start (will check for duplicates)? (yes/no):</ask>
|
||||||
|
<check if="not confirmed">
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="choice == 'V'">
|
||||||
|
<action>Display migration state details</action>
|
||||||
|
<action>Then re-prompt for choice</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="choice == 'D'">
|
||||||
|
<action>Delete state file</action>
|
||||||
|
<action>Set resume_mode = false</action>
|
||||||
|
<output>✅ State cleared</output>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
</substep>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
✅ PRE-FLIGHT CHECKS PASSED
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
- GitHub MCP: Connected
|
||||||
|
- Repository: Accessible
|
||||||
|
- Sprint status: Loaded ({{total_stories}} stories)
|
||||||
|
- Story files: {{stories_with_files}} found
|
||||||
|
- Mode: {{mode}}
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="1" goal="Dry-run mode - Preview migration plan">
|
||||||
|
<check if="mode != 'dry-run'">
|
||||||
|
<action>Skip to Step 2 (Execute mode)</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
🔍 DRY-RUN MODE (Preview Only - No Changes)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
This will show what WOULD happen without actually creating issues.
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<action>For each story in sprint-status.yaml:</action>
|
||||||
|
|
||||||
|
<iterate>For each story_key:</iterate>
|
||||||
|
|
||||||
|
<substep n="1a" title="Check if issue already exists">
|
||||||
|
<action>Search GitHub: mcp__github__search_issues({
|
||||||
|
query: "repo:{{github_owner}}/{{github_repo}} label:story:{{story_key}}"
|
||||||
|
})</action>
|
||||||
|
|
||||||
|
<check if="issue found">
|
||||||
|
<action>would_update = {{update_existing}}</action>
|
||||||
|
<output>
|
||||||
|
📝 Story {{story_key}}:
|
||||||
|
GitHub: Issue #{{existing_issue.number}} EXISTS
|
||||||
|
Action: {{#if would_update}}Would UPDATE{{else}}Would SKIP{{/if}}
|
||||||
|
Current labels: {{existing_issue.labels}}
|
||||||
|
Current assignee: {{existing_issue.assignee || "none"}}
|
||||||
|
</output>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="issue not found">
|
||||||
|
<action>would_create = true</action>
|
||||||
|
<action>Read local story file</action>
|
||||||
|
<action>Parse: title, ACs, tasks, epic, status</action>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
📝 Story {{story_key}}:
|
||||||
|
GitHub: NOT FOUND
|
||||||
|
Action: Would CREATE
|
||||||
|
|
||||||
|
Proposed Issue:
|
||||||
|
- Title: "Story {{story_key}}: {{parsed_title}}"
|
||||||
|
- Labels: type:story, story:{{story_key}}, status:{{status}}, epic:{{epic_number}}, complexity:{{complexity}}
|
||||||
|
- Milestone: Epic {{epic_number}}
|
||||||
|
- Acceptance Criteria: {{ac_count}} items
|
||||||
|
- Tasks: {{task_count}} items
|
||||||
|
- Assignee: {{#if status == 'in-progress'}}@{{infer_from_git_log}}{{else}}none{{/if}}
|
||||||
|
</output>
|
||||||
|
</check>
|
||||||
|
</substep>
|
||||||
|
|
||||||
|
<action>Count actions:</action>
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
📊 DRY-RUN SUMMARY
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
**Total Stories:** {{total_stories}}
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- ✅ Would CREATE: {{would_create_count}} new issues
|
||||||
|
- 🔄 Would UPDATE: {{would_update_count}} existing issues
|
||||||
|
- ⏭️ Would SKIP: {{would_skip_count}} (existing, no update)
|
||||||
|
|
||||||
|
**Epics/Milestones:**
|
||||||
|
- Would CREATE: {{epic_milestones_to_create.length}} milestones
|
||||||
|
- Already exist: {{epic_milestones_existing.length}}
|
||||||
|
|
||||||
|
**Estimated API Calls:**
|
||||||
|
- Issue searches: {{total_stories}} (check existing)
|
||||||
|
- Issue creates: {{would_create_count}}
|
||||||
|
- Issue updates: {{would_update_count}}
|
||||||
|
- Milestone operations: {{milestone_operations}}
|
||||||
|
- **Total:** ~{{total_api_calls}} API calls
|
||||||
|
|
||||||
|
**Rate Limit Impact:**
|
||||||
|
- Authenticated limit: 5000/hour
|
||||||
|
- This migration: ~{{total_api_calls}} calls
|
||||||
|
- Remaining after: ~{{5000 - total_api_calls}}
|
||||||
|
- Safe: {{#if total_api_calls < 1000}}YES{{else}}Borderline (consider smaller batches){{/if}}
|
||||||
|
|
||||||
|
**Estimated Duration:** {{estimated_minutes}} minutes
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
⚠️ This was a DRY-RUN. No issues were created.
|
||||||
|
|
||||||
|
To execute the migration:
|
||||||
|
/migrate-to-github mode=execute github_owner={{github_owner}} github_repo={{github_repo}}
|
||||||
|
|
||||||
|
To migrate only Epic 2:
|
||||||
|
/migrate-to-github mode=execute filter_by_epic=2 github_owner={{github_owner}} github_repo={{github_repo}}
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<action>Exit workflow (dry-run complete)</action>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="2" goal="Execute mode - Perform migration with atomic operations">
|
||||||
|
<check if="mode != 'execute'">
|
||||||
|
<action>Skip to Step 3 (Verify mode)</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
⚡ EXECUTE MODE (Migrating Stories to GitHub)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
**SAFETY GUARANTEES:**
|
||||||
|
✅ Idempotent - Can re-run safely (checks for duplicates)
|
||||||
|
✅ Atomic - Each story fully succeeds or rolls back
|
||||||
|
✅ Verified - Reads back each created issue
|
||||||
|
✅ Resumable - Saves state after each story
|
||||||
|
✅ Reversible - Creates rollback manifest
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<ask>
|
||||||
|
⚠️ FINAL CONFIRMATION
|
||||||
|
|
||||||
|
You are about to create ~{{would_create_count}} GitHub Issues.
|
||||||
|
|
||||||
|
This operation:
|
||||||
|
- WILL create issues in {{github_owner}}/{{github_repo}}
|
||||||
|
- WILL modify your GitHub repository
|
||||||
|
- CAN be rolled back (we'll create rollback manifest)
|
||||||
|
- CANNOT be undone automatically after issues are created
|
||||||
|
|
||||||
|
Have you:
|
||||||
|
- [ ] Run dry-run mode to preview?
|
||||||
|
- [ ] Verified repository is correct?
|
||||||
|
- [ ] Backed up sprint-status.yaml?
|
||||||
|
- [ ] Confirmed you want to proceed?
|
||||||
|
|
||||||
|
Type "I understand and want to proceed" to continue:
|
||||||
|
</ask>
|
||||||
|
|
||||||
|
<check if="confirmation != 'I understand and want to proceed'">
|
||||||
|
<output>❌ Migration cancelled - confirmation not received</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Initialize migration state:</action>
|
||||||
|
<action>
|
||||||
|
migration_state = {
|
||||||
|
started_at: {{timestamp}},
|
||||||
|
mode: "execute",
|
||||||
|
github_owner: {{github_owner}},
|
||||||
|
github_repo: {{github_repo}},
|
||||||
|
total_stories: {{total_stories}},
|
||||||
|
stories_migrated: [],
|
||||||
|
issues_created: [],
|
||||||
|
issues_updated: [],
|
||||||
|
issues_failed: [],
|
||||||
|
rollback_manifest: [],
|
||||||
|
last_completed: null
|
||||||
|
}
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action>Save initial state to {{state_file}}</action>
|
||||||
|
|
||||||
|
<action>Initialize rollback manifest (for safety):</action>
|
||||||
|
<action>rollback_manifest = {
|
||||||
|
created_at: {{timestamp}},
|
||||||
|
github_owner: {{github_owner}},
|
||||||
|
github_repo: {{github_repo}},
|
||||||
|
created_issues: [] # Will track issue numbers for rollback
|
||||||
|
}</action>
|
||||||
|
|
||||||
|
<iterate>For each story in sprint-status.yaml:</iterate>
|
||||||
|
|
||||||
|
<substep n="2a" title="Migrate single story (ATOMIC)">
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
📦 Migrating {{current_index}}/{{total_stories}}: {{story_key}}
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<action>Read local story file</action>
|
||||||
|
|
||||||
|
<check if="file not found">
|
||||||
|
<output> ⏭️ SKIP - No file found</output>
|
||||||
|
<action>Add to migration_state.issues_failed with reason: "File not found"</action>
|
||||||
|
<action>Continue to next story</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Parse story file:</action>
|
||||||
|
<action> - Extract all 12 sections</action>
|
||||||
|
<action> - Parse Acceptance Criteria (convert to checkboxes)</action>
|
||||||
|
<action> - Parse Tasks (convert to checkboxes)</action>
|
||||||
|
<action> - Extract metadata: epic_number, complexity</action>
|
||||||
|
|
||||||
|
<action>Check if issue already exists (idempotent check):</action>
|
||||||
|
<action>Call: mcp__github__search_issues({
|
||||||
|
query: "repo:{{github_owner}}/{{github_repo}} label:story:{{story_key}}"
|
||||||
|
})</action>
|
||||||
|
|
||||||
|
<check if="issue exists AND update_existing == false">
|
||||||
|
<output> ✅ EXISTS - Issue #{{existing_issue.number}} (skipping, update_existing=false)</output>
|
||||||
|
<action>Add to migration_state.stories_migrated (already done)</action>
|
||||||
|
<action>Continue to next story</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="issue exists AND update_existing == true">
|
||||||
|
<output> 🔄 EXISTS - Issue #{{existing_issue.number}} (updating)</output>
|
||||||
|
|
||||||
|
<action>ATOMIC UPDATE with retry:</action>
|
||||||
|
<action>
|
||||||
|
attempt = 0
|
||||||
|
max_attempts = {{max_retries}} + 1
|
||||||
|
|
||||||
|
WHILE attempt < max_attempts:
|
||||||
|
TRY:
|
||||||
|
# Update issue
|
||||||
|
result = mcp__github__issue_write({
|
||||||
|
method: "update",
|
||||||
|
owner: {{github_owner}},
|
||||||
|
repo: {{github_repo}},
|
||||||
|
issue_number: {{existing_issue.number}},
|
||||||
|
title: "Story {{story_key}}: {{parsed_title}}",
|
||||||
|
body: {{convertStoryToIssueBody(parsed)}},
|
||||||
|
labels: {{generateLabels(story_key, status, parsed)}}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify update succeeded (read back)
|
||||||
|
sleep 1 second # GitHub eventual consistency
|
||||||
|
|
||||||
|
verification = mcp__github__issue_read({
|
||||||
|
method: "get",
|
||||||
|
owner: {{github_owner}},
|
||||||
|
repo: {{github_repo}},
|
||||||
|
issue_number: {{existing_issue.number}}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check verification
|
||||||
|
IF verification.title != expected_title:
|
||||||
|
THROW "Write verification failed"
|
||||||
|
|
||||||
|
# Success!
|
||||||
|
output: " ✅ UPDATED and VERIFIED - Issue #{{existing_issue.number}}"
|
||||||
|
BREAK
|
||||||
|
|
||||||
|
CATCH error:
|
||||||
|
attempt++
|
||||||
|
IF attempt < max_attempts:
|
||||||
|
sleep {{retry_backoff_ms[attempt]}}
|
||||||
|
output: " ⚠️ Retry {{attempt}}/{{max_retries}} after error: {{error}}"
|
||||||
|
ELSE:
|
||||||
|
output: " ❌ FAILED after {{max_retries}} retries: {{error}}"
|
||||||
|
add to migration_state.issues_failed
|
||||||
|
|
||||||
|
IF halt_on_critical_error:
|
||||||
|
HALT
|
||||||
|
ELSE:
|
||||||
|
CONTINUE to next story
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action>Add to migration_state.issues_updated</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="issue does NOT exist">
|
||||||
|
<output> 🆕 CREATING new issue...</output>
|
||||||
|
|
||||||
|
<action>Generate issue body from story file:</action>
|
||||||
|
<action>
|
||||||
|
issue_body = """
|
||||||
|
**Story File:** [{{story_key}}.md]({{file_path_in_repo}})
|
||||||
|
**Epic:** {{epic_number}}
|
||||||
|
**Complexity:** {{complexity}} ({{task_count}} tasks)
|
||||||
|
|
||||||
|
## Business Context
|
||||||
|
{{parsed.businessContext}}
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
{{#each parsed.acceptanceCriteria}}
|
||||||
|
- [ ] AC{{@index + 1}}: {{this}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
{{#each parsed.tasks}}
|
||||||
|
- [ ] {{this}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
## Technical Requirements
|
||||||
|
{{parsed.technicalRequirements}}
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
{{#each parsed.definitionOfDone}}
|
||||||
|
- [ ] {{this}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
---
|
||||||
|
_Migrated from BMAD local files_
|
||||||
|
_Sync timestamp: {{timestamp}}_
|
||||||
|
_Local file: `{{story_file_path}}`_
|
||||||
|
"""
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action>Generate labels:</action>
|
||||||
|
<action>
|
||||||
|
labels = [
|
||||||
|
"type:story",
|
||||||
|
"story:{{story_key}}",
|
||||||
|
"status:{{current_status}}",
|
||||||
|
"epic:{{epic_number}}",
|
||||||
|
"complexity:{{complexity}}"
|
||||||
|
]
|
||||||
|
|
||||||
|
{{#if has_high_risk_keywords}}
|
||||||
|
labels.push("risk:high")
|
||||||
|
{{/if}}
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action>ATOMIC CREATE with retry and verification:</action>
|
||||||
|
<action>
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
|
WHILE attempt < max_attempts:
|
||||||
|
TRY:
|
||||||
|
# Create issue
|
||||||
|
created_issue = mcp__github__issue_write({
|
||||||
|
method: "create",
|
||||||
|
owner: {{github_owner}},
|
||||||
|
repo: {{github_repo}},
|
||||||
|
title: "Story {{story_key}}: {{parsed_title}}",
|
||||||
|
body: {{issue_body}},
|
||||||
|
labels: {{labels}}
|
||||||
|
})
|
||||||
|
|
||||||
|
issue_number = created_issue.number
|
||||||
|
|
||||||
|
# CRITICAL: Verify creation succeeded (read back)
|
||||||
|
sleep 2 seconds # GitHub eventual consistency
|
||||||
|
|
||||||
|
verification = mcp__github__issue_read({
|
||||||
|
method: "get",
|
||||||
|
owner: {{github_owner}},
|
||||||
|
repo: {{github_repo}},
|
||||||
|
issue_number: {{issue_number}}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify all fields
|
||||||
|
IF verification.title != expected_title:
|
||||||
|
THROW "Title mismatch after create"
|
||||||
|
|
||||||
|
IF NOT verification.labels.includes("story:{{story_key}}"):
|
||||||
|
THROW "Story label missing after create"
|
||||||
|
|
||||||
|
# Success - record for rollback capability
|
||||||
|
output: " ✅ CREATED and VERIFIED - Issue #{{issue_number}}"
|
||||||
|
|
||||||
|
rollback_manifest.created_issues.push({
|
||||||
|
story_key: {{story_key}},
|
||||||
|
issue_number: {{issue_number}},
|
||||||
|
created_at: {{timestamp}}
|
||||||
|
})
|
||||||
|
|
||||||
|
migration_state.issues_created.push({
|
||||||
|
story_key: {{story_key}},
|
||||||
|
issue_number: {{issue_number}}
|
||||||
|
})
|
||||||
|
|
||||||
|
BREAK
|
||||||
|
|
||||||
|
CATCH error:
|
||||||
|
attempt++
|
||||||
|
|
||||||
|
# Check if issue was created despite error (orphaned issue)
|
||||||
|
check_result = mcp__github__search_issues({
|
||||||
|
query: "repo:{{github_owner}}/{{github_repo}} label:story:{{story_key}}"
|
||||||
|
})
|
||||||
|
|
||||||
|
IF check_result.length > 0:
|
||||||
|
# Issue was created, verification failed - treat as success
|
||||||
|
output: " ✅ CREATED (verification had transient error)"
|
||||||
|
BREAK
|
||||||
|
|
||||||
|
IF attempt < max_attempts:
|
||||||
|
sleep {{retry_backoff_ms[attempt]}}
|
||||||
|
output: " ⚠️ Retry {{attempt}}/{{max_retries}}"
|
||||||
|
ELSE:
|
||||||
|
output: " ❌ FAILED after {{max_retries}} retries: {{error}}"
|
||||||
|
|
||||||
|
migration_state.issues_failed.push({
|
||||||
|
story_key: {{story_key}},
|
||||||
|
error: {{error}},
|
||||||
|
attempts: {{attempt}}
|
||||||
|
})
|
||||||
|
|
||||||
|
IF halt_on_critical_error:
|
||||||
|
output: "HALTING - Critical error during migration"
|
||||||
|
save migration_state
|
||||||
|
HALT
|
||||||
|
ELSE:
|
||||||
|
output: "Continuing despite failure (continue_on_failure=true)"
|
||||||
|
CONTINUE to next story
|
||||||
|
</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Update migration state:</action>
|
||||||
|
<action>migration_state.stories_migrated.push({{story_key}})</action>
|
||||||
|
<action>migration_state.last_completed = {{story_key}}</action>
|
||||||
|
|
||||||
|
<check if="save_state_after_each == true">
|
||||||
|
<action>Save migration state to {{state_file}}</action>
|
||||||
|
<action>Save rollback manifest to {{output_folder}}/migration-rollback-{{timestamp}}.yaml</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="current_index % 10 == 0">
|
||||||
|
<output>
|
||||||
|
📊 Progress: {{current_index}}/{{total_stories}} migrated
|
||||||
|
Created: {{issues_created.length}}
|
||||||
|
Updated: {{issues_updated.length}}
|
||||||
|
Failed: {{issues_failed.length}}
|
||||||
|
</output>
|
||||||
|
</check>
|
||||||
|
</substep>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
✅ MIGRATION COMPLETE
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
**Total:** {{total_stories}} stories processed
|
||||||
|
**Created:** {{issues_created.length}} new issues
|
||||||
|
**Updated:** {{issues_updated.length}} existing issues
|
||||||
|
**Failed:** {{issues_failed.length}} errors
|
||||||
|
**Duration:** {{actual_duration}}
|
||||||
|
|
||||||
|
{{#if issues_failed.length > 0}}
|
||||||
|
**Failed Stories:**
|
||||||
|
{{#each issues_failed}}
|
||||||
|
- {{story_key}}: {{error}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
Recommendation: Fix errors and re-run migration (will skip already-migrated stories)
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
**Rollback Manifest:** {{rollback_manifest_path}}
|
||||||
|
(Use this file to delete created issues if needed)
|
||||||
|
|
||||||
|
**State File:** {{state_file}}
|
||||||
|
(Tracks migration progress for resume capability)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<action>Continue to Step 3 (Verify)</action>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="3" goal="Verify mode - Double-check migration accuracy">
|
||||||
|
<check if="mode != 'verify' AND mode != 'execute'">
|
||||||
|
<action>Skip to Step 4</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="mode == 'execute'">
|
||||||
|
<ask>
|
||||||
|
Migration complete. Run verification to double-check accuracy? (yes/no):
|
||||||
|
</ask>
|
||||||
|
|
||||||
|
<check if="response != 'yes'">
|
||||||
|
<action>Skip to Step 5 (Report)</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
🔍 VERIFICATION MODE (Double-Checking Migration)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<action>Load migration state from {{state_file}}</action>
|
||||||
|
|
||||||
|
<iterate>For each migrated story in migration_state.stories_migrated:</iterate>
|
||||||
|
|
||||||
|
<action>Fetch issue from GitHub:</action>
|
||||||
|
<action>Search: label:story:{{story_key}}</action>
|
||||||
|
|
||||||
|
<check if="issue not found">
|
||||||
|
<output> ❌ VERIFICATION FAILED: {{story_key}} - Issue not found in GitHub</output>
|
||||||
|
<action>Add to verification_failures</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="issue found">
|
||||||
|
<action>Verify fields match expected:</action>
|
||||||
|
<action> - Title contains story_key ✓</action>
|
||||||
|
<action> - Label "story:{{story_key}}" exists ✓</action>
|
||||||
|
<action> - Status label matches sprint-status.yaml ✓</action>
|
||||||
|
<action> - AC count matches local file ✓</action>
|
||||||
|
|
||||||
|
<check if="all fields match">
|
||||||
|
<output> ✅ VERIFIED: {{story_key}} → Issue #{{issue_number}}</output>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="fields mismatch">
|
||||||
|
<output> ⚠️ MISMATCH: {{story_key}} → Issue #{{issue_number}}</output>
|
||||||
|
<output> Expected: {{expected}}</output>
|
||||||
|
<output> Actual: {{actual}}</output>
|
||||||
|
<action>Add to verification_warnings</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
📊 VERIFICATION RESULTS
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
**Stories Checked:** {{stories_migrated.length}}
|
||||||
|
**✅ Verified Correct:** {{verified_count}}
|
||||||
|
**⚠️ Warnings:** {{verification_warnings.length}}
|
||||||
|
**❌ Failures:** {{verification_failures.length}}
|
||||||
|
|
||||||
|
{{#if verification_failures.length > 0}}
|
||||||
|
**Verification Failures:**
|
||||||
|
{{#each verification_failures}}
|
||||||
|
- {{this}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
❌ Migration has errors - issues may be missing or incorrect
|
||||||
|
{{else}}
|
||||||
|
✅ All migrated stories verified in GitHub
|
||||||
|
{{/if}}
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="4" goal="Rollback mode - Delete created issues">
|
||||||
|
<check if="mode != 'rollback'">
|
||||||
|
<action>Skip to Step 5 (Report)</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
⚠️ ROLLBACK MODE (Delete Migrated Issues)
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<action>Load rollback manifest from {{output_folder}}/migration-rollback-*.yaml</action>
|
||||||
|
|
||||||
|
<check if="manifest not found">
|
||||||
|
<output>
|
||||||
|
❌ ERROR: No rollback manifest found
|
||||||
|
|
||||||
|
Cannot rollback without manifest file.
|
||||||
|
Rollback manifests are in: {{output_folder}}/migration-rollback-*.yaml
|
||||||
|
|
||||||
|
HALTING
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
**Rollback Manifest:**
|
||||||
|
- Created: {{manifest.created_at}}
|
||||||
|
- Repository: {{manifest.github_owner}}/{{manifest.github_repo}}
|
||||||
|
- Issues to delete: {{manifest.created_issues.length}}
|
||||||
|
|
||||||
|
**WARNING:** This will PERMANENTLY DELETE these issues from GitHub:
|
||||||
|
{{#each manifest.created_issues}}
|
||||||
|
- Issue #{{issue_number}}: {{story_key}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
This operation CANNOT be undone!
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<ask>
|
||||||
|
Type "DELETE ALL ISSUES" to proceed with rollback:
|
||||||
|
</ask>
|
||||||
|
|
||||||
|
<check if="confirmation != 'DELETE ALL ISSUES'">
|
||||||
|
<output>❌ Rollback cancelled</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<iterate>For each issue in manifest.created_issues:</iterate>
|
||||||
|
|
||||||
|
<action>Delete issue (GitHub API doesn't support delete, so close + lock):</action>
|
||||||
|
<action>
|
||||||
|
# GitHub doesn't allow issue deletion via API
|
||||||
|
# Best we can do: close, lock, and add label "migrated:rolled-back"
|
||||||
|
|
||||||
|
mcp__github__issue_write({
|
||||||
|
method: "update",
|
||||||
|
issue_number: {{issue_number}},
|
||||||
|
state: "closed",
|
||||||
|
labels: ["migrated:rolled-back", "do-not-use"],
|
||||||
|
state_reason: "not_planned"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add comment explaining
|
||||||
|
mcp__github__add_issue_comment({
|
||||||
|
issue_number: {{issue_number}},
|
||||||
|
body: "Issue closed - migration was rolled back. Do not use."
|
||||||
|
})
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<output> ✅ Rolled back: Issue #{{issue_number}}</output>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
✅ ROLLBACK COMPLETE
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
**Issues Rolled Back:** {{manifest.created_issues.length}}
|
||||||
|
|
||||||
|
Note: GitHub API doesn't support issue deletion.
|
||||||
|
Issues were closed with label "migrated:rolled-back" instead.
|
||||||
|
|
||||||
|
To fully delete (manual):
|
||||||
|
1. Go to repository settings
|
||||||
|
2. Issues → Delete closed issues
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="5" goal="Generate comprehensive migration report">
|
||||||
|
<action>Calculate final statistics:</action>
|
||||||
|
<action>
|
||||||
|
final_stats = {
|
||||||
|
total_stories: {{total_stories}},
|
||||||
|
migrated_successfully: {{issues_created.length + issues_updated.length}},
|
||||||
|
failed: {{issues_failed.length}},
|
||||||
|
success_rate: ({{migrated_successfully}} / {{total_stories}}) * 100,
|
||||||
|
duration: {{end_time - start_time}},
|
||||||
|
avg_time_per_story: {{duration / total_stories}}
|
||||||
|
}
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<check if="create_migration_report == true">
|
||||||
|
<action>Write comprehensive report to {{report_path}}</action>
|
||||||
|
|
||||||
|
<action>Report structure:</action>
|
||||||
|
<action>
|
||||||
|
# GitHub Migration Report
|
||||||
|
|
||||||
|
**Date:** {{timestamp}}
|
||||||
|
**Repository:** {{github_owner}}/{{github_repo}}
|
||||||
|
**Mode:** {{mode}}
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
- **Total Stories:** {{total_stories}}
|
||||||
|
- **✅ Migrated:** {{migrated_successfully}} ({{success_rate}}%)
|
||||||
|
- **❌ Failed:** {{failed}}
|
||||||
|
- **Duration:** {{duration}}
|
||||||
|
- **Avg per story:** {{avg_time_per_story}}
|
||||||
|
|
||||||
|
## Created Issues
|
||||||
|
|
||||||
|
{{#each issues_created}}
|
||||||
|
- Story {{story_key}} → Issue #{{issue_number}}
|
||||||
|
URL: <https://github.com/{{github_owner}}/{{github_repo}}/issues/{{issue_number}}>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
## Updated Issues
|
||||||
|
|
||||||
|
{{#each issues_updated}}
|
||||||
|
- Story {{story_key}} → Issue #{{issue_number}} (updated)
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
## Failed Migrations
|
||||||
|
|
||||||
|
{{#if issues_failed.length > 0}}
|
||||||
|
{{#each issues_failed}}
|
||||||
|
- Story {{story_key}}: {{error}}
|
||||||
|
Attempts: {{attempts}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
**Recovery Steps:**
|
||||||
|
1. Fix underlying issues (check error messages)
|
||||||
|
2. Re-run migration (will skip already-migrated stories)
|
||||||
|
{{else}}
|
||||||
|
None - all stories migrated successfully!
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
## Rollback Information
|
||||||
|
|
||||||
|
**Rollback Manifest:** {{rollback_manifest_path}}
|
||||||
|
|
||||||
|
To rollback this migration:
|
||||||
|
```bash
|
||||||
|
/migrate-to-github mode=rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Verify migration:** /migrate-to-github mode=verify
|
||||||
|
2. **Test story checkout:** /checkout-story story_key=2-5-auth
|
||||||
|
3. **Enable GitHub sync:** Update workflow.yaml with github_sync_enabled=true
|
||||||
|
4. **Product Owner setup:** Share GitHub Issues URL with PO team
|
||||||
|
|
||||||
|
## Migration Details
|
||||||
|
|
||||||
|
**API Calls Made:** ~{{total_api_calls}}
|
||||||
|
**Rate Limit Used:** {{api_calls_used}}/5000
|
||||||
|
**Errors Encountered:** {{error_count}}
|
||||||
|
**Retries Performed:** {{retry_count}}
|
||||||
|
|
||||||
|
---
|
||||||
|
_Generated by BMAD migrate-to-github workflow_
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<output>📄 Migration report: {{report_path}}</output>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
✅ MIGRATION WORKFLOW COMPLETE
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
**Mode:** {{mode}}
|
||||||
|
**Success Rate:** {{success_rate}}%
|
||||||
|
|
||||||
|
{{#if mode == 'execute'}}
|
||||||
|
**✅ {{migrated_successfully}} stories now in GitHub Issues**
|
||||||
|
|
||||||
|
View in GitHub:
|
||||||
|
<https://github.com/{{github_owner}}/{{github_repo}}/issues?q=is:issue+label:type:story>
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
1. Verify migration: /migrate-to-github mode=verify
|
||||||
|
2. Test workflows with GitHub sync enabled
|
||||||
|
3. Share Issues URL with Product Owner team
|
||||||
|
|
||||||
|
{{#if issues_failed.length > 0}}
|
||||||
|
⚠️ {{issues_failed.length}} stories failed - re-run to retry
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if mode == 'dry-run'}}
|
||||||
|
**This was a preview. No issues were created.**
|
||||||
|
|
||||||
|
To execute: /migrate-to-github mode=execute
|
||||||
|
{{/if}}
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
</output>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
</workflow>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
name: migrate-to-github
|
||||||
|
description: "Production-grade migration of BMAD stories from local files to GitHub Issues with comprehensive reliability guarantees"
|
||||||
|
author: "BMad"
|
||||||
|
version: "1.0.0"
|
||||||
|
|
||||||
|
# Critical variables
|
||||||
|
config_source: "{project-root}/_bmad/bmm/config.yaml"
|
||||||
|
output_folder: "{config_source}:output_folder"
|
||||||
|
sprint_artifacts: "{output_folder}/sprint-artifacts"
|
||||||
|
sprint_status: "{output_folder}/sprint-status.yaml"
|
||||||
|
|
||||||
|
# GitHub configuration
|
||||||
|
github:
|
||||||
|
owner: "{github_owner}" # Required: GitHub username or org
|
||||||
|
repo: "{github_repo}" # Required: Repository name
|
||||||
|
# Token comes from MCP GitHub server config (already authenticated)
|
||||||
|
|
||||||
|
# Migration mode
|
||||||
|
mode: "dry-run" # "dry-run" | "execute" | "verify" | "rollback"
|
||||||
|
# SAFETY: Defaults to dry-run - must explicitly choose execute
|
||||||
|
|
||||||
|
# Migration scope
|
||||||
|
scope:
|
||||||
|
include_epics: true # Create milestone for each epic
|
||||||
|
include_stories: true # Create issue for each story
|
||||||
|
filter_by_epic: null # Optional: Only migrate Epic N (e.g., "2")
|
||||||
|
filter_by_status: null # Optional: Only migrate stories with status (e.g., "backlog")
|
||||||
|
|
||||||
|
# Migration strategy
|
||||||
|
strategy:
|
||||||
|
check_existing: true # Search for existing issues before creating (prevents duplicates)
|
||||||
|
update_existing: true # If issue exists, update it (false = skip)
|
||||||
|
create_missing: true # Create issues for stories without issues
|
||||||
|
|
||||||
|
# Label strategy
|
||||||
|
label_prefix: "story:" # Prefix for story labels (e.g., "story:2-5-auth")
|
||||||
|
use_type_labels: true # Add "type:story", "type:epic"
|
||||||
|
use_status_labels: true # Add "status:backlog", "status:in-progress", etc.
|
||||||
|
use_complexity_labels: true # Add "complexity:micro", etc.
|
||||||
|
use_epic_labels: true # Add "epic:2", "epic:3", etc.
|
||||||
|
|
||||||
|
# Reliability settings
|
||||||
|
reliability:
|
||||||
|
verify_after_create: true # Read back issue to verify creation succeeded
|
||||||
|
retry_on_failure: true # Retry failed operations
|
||||||
|
max_retries: 3
|
||||||
|
retry_backoff_ms: [1000, 3000, 9000] # Exponential backoff
|
||||||
|
halt_on_critical_error: true # Stop migration if critical error occurs
|
||||||
|
save_state_after_each: true # Save progress after each story (crash-safe)
|
||||||
|
create_rollback_manifest: true # Track created issues for rollback
|
||||||
|
|
||||||
|
# State tracking
|
||||||
|
state_file: "{output_folder}/migration-state.yaml"
|
||||||
|
# Tracks: stories_migrated, issues_created, last_story, can_resume
|
||||||
|
|
||||||
|
# Output
|
||||||
|
output:
|
||||||
|
create_migration_report: true
|
||||||
|
report_path: "{output_folder}/migration-report-{timestamp}.md"
|
||||||
|
log_level: "verbose" # "quiet" | "normal" | "verbose"
|
||||||
|
|
||||||
|
standalone: true
|
||||||
|
|
@ -4,6 +4,7 @@ header: "Creative Innovation Suite (CIS) Module"
|
||||||
subheader: "No custom configuration required - uses Core settings only"
|
subheader: "No custom configuration required - uses Core settings only"
|
||||||
default_selected: false # This module will not be selected by default for new installations
|
default_selected: false # This module will not be selected by default for new installations
|
||||||
|
|
||||||
|
|
||||||
# Variables from Core Config inserted:
|
# Variables from Core Config inserted:
|
||||||
## user_name
|
## user_name
|
||||||
## communication_language
|
## communication_language
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue