Blame · Line-by-line history
migration-onboarding.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 8ae1b1f | 1 | /** |
| 2 | * Post-import onboarding for a team migrating from GitHub. | |
| 3 | * | |
| 4 | * `POST /import/bulk` cloned repositories and inserted rows, and stopped | |
| 5 | * there. A team that migrated an org therefore landed on a set of bare | |
| 6 | * repositories with no branch protection, no labels, no CODEOWNERS, no gate | |
| 7 | * settings, and no evidence that any of it was worth doing. The platform's | |
| 8 | * entire value proposition — that it finds problems in your code — was | |
| 9 | * something they had to go and discover for themselves, one repo at a time. | |
| 10 | * | |
| 11 | * This runs the work for them at import time and returns a report: | |
| 12 | * | |
| 13 | * 1. bootstrapRepository() green defaults: gates on, branch | |
| 14 | * protection, labels, CODEOWNERS. | |
| 15 | * 2. runSecretAndSecurityScan() the actual proof — committed secrets and | |
| 16 | * security findings in the code they just | |
| 17 | * brought over. | |
| 18 | * | |
| 19 | * Everything here is best-effort by design. A migration that imported 40 | |
| 20 | * repositories must not be reported as a failure because the scanner choked | |
| 21 | * on one of them, so each repo's outcome is captured independently and a | |
| 22 | * thrown error becomes a per-repo `error` field rather than a rejected | |
| 23 | * promise. | |
| 24 | * | |
| 25 | * Concurrency is bounded (DB_FANOUT_LIMIT). Scanning is IO-heavy — a tree | |
| 26 | * walk plus blob reads per repo — and an unbounded Promise.all over a | |
| 27 | * 100-repo org would open 100 concurrent walks against the same disk and | |
| 28 | * connection pool. | |
| 29 | */ | |
| 30 | ||
| 31 | import { mapWithConcurrency, DB_FANOUT_LIMIT } from "./concurrency"; | |
| 32 | ||
| 33 | /** One repository's post-import outcome. */ | |
| 34 | export interface MigrationRepoReport { | |
| 35 | owner: string; | |
| 36 | name: string; | |
| 37 | /** Green-default bootstrap applied (settings, protection, labels). */ | |
| 38 | bootstrapped: boolean; | |
| 39 | /** Number of branch-protection rules / labels created, for the summary. */ | |
| 40 | labelsCreated: number; | |
| 41 | protectionCreated: boolean; | |
| 42 | /** Committed secrets found in the imported code. */ | |
| 43 | secretsFound: number; | |
| 44 | /** Security findings (vulnerable patterns) in the imported code. */ | |
| 45 | securityIssues: number; | |
| 46 | /** Human-readable scan detail, already scrubbed by the scanner. */ | |
| 47 | scanDetail: string; | |
| 48 | /** Set when this repo's onboarding failed; the others still ran. */ | |
| 49 | error?: string; | |
| 50 | } | |
| 51 | ||
| 52 | export interface MigrationReport { | |
| 53 | repos: MigrationRepoReport[]; | |
| 54 | /** Totals, precomputed so the view stays markup. */ | |
| 55 | totalRepos: number; | |
| 56 | totalSecrets: number; | |
| 57 | totalSecurityIssues: number; | |
| 58 | reposBootstrapped: number; | |
| 59 | reposWithFindings: number; | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Bootstrap and scan every freshly imported repository. | |
| 64 | * | |
| 65 | * `repos` is the subset of a bulk import that actually landed — callers pass | |
| 66 | * only successful imports, since there is nothing to scan otherwise. | |
| 67 | */ | |
| 68 | export async function runMigrationOnboarding( | |
| 69 | repos: Array<{ id: string; owner: string; name: string; defaultBranch?: string | null }>, | |
| 70 | ownerUserId: string | |
| 71 | ): Promise<MigrationReport> { | |
| 72 | const results = await mapWithConcurrency( | |
| 73 | repos, | |
| 74 | DB_FANOUT_LIMIT, | |
| 75 | async (repo): Promise<MigrationRepoReport> => { | |
| 76 | const base: MigrationRepoReport = { | |
| 77 | owner: repo.owner, | |
| 78 | name: repo.name, | |
| 79 | bootstrapped: false, | |
| 80 | labelsCreated: 0, | |
| 81 | protectionCreated: false, | |
| 82 | secretsFound: 0, | |
| 83 | securityIssues: 0, | |
| 84 | scanDetail: "", | |
| 85 | }; | |
| 86 | ||
| 87 | const branch = repo.defaultBranch || "main"; | |
| 88 | ||
| 89 | // 1. Green defaults. Skips the welcome issue — an imported repo has | |
| 90 | // real history and real issues; a "welcome" issue on top of 400 | |
| 91 | // migrated ones is noise, not onboarding. | |
| 92 | try { | |
| 93 | const { bootstrapRepository } = await import("./repo-bootstrap"); | |
| 94 | const r = await bootstrapRepository({ | |
| 95 | repositoryId: repo.id, | |
| 96 | ownerUserId, | |
| 97 | defaultBranch: branch, | |
| 98 | skipWelcomeIssue: true, | |
| 99 | }); | |
| 100 | base.bootstrapped = true; | |
| 101 | base.labelsCreated = r.labelsCreated ?? 0; | |
| 102 | base.protectionCreated = r.protectionCreated ?? false; | |
| 103 | } catch (err) { | |
| 104 | base.error = `bootstrap: ${err instanceof Error ? err.message : String(err)}`; | |
| 105 | } | |
| 106 | ||
| 107 | // 2. The proof. Scan the code they just brought over. | |
| 108 | try { | |
| 109 | const { runSecretAndSecurityScan } = await import("./gate"); | |
| 110 | const scan = await runSecretAndSecurityScan( | |
| 111 | repo.owner, | |
| 112 | repo.name, | |
| 113 | `refs/heads/${branch}`, | |
| 114 | branch, | |
| 115 | { scanSecrets: true, scanSecurity: true } | |
| 116 | ); | |
| 117 | base.secretsFound = scan.secrets.length; | |
| 118 | base.securityIssues = scan.securityIssues.length; | |
| 119 | base.scanDetail = [scan.secretResult.details, scan.securityResult.details] | |
| 120 | .filter(Boolean) | |
| 121 | .join(" · "); | |
| 122 | } catch (err) { | |
| 123 | const msg = `scan: ${err instanceof Error ? err.message : String(err)}`; | |
| 124 | base.error = base.error ? `${base.error}; ${msg}` : msg; | |
| 125 | } | |
| 126 | ||
| 127 | return base; | |
| 128 | } | |
| 129 | ); | |
| 130 | ||
| 131 | return { | |
| 132 | repos: results, | |
| 133 | totalRepos: results.length, | |
| 134 | totalSecrets: results.reduce((n, r) => n + r.secretsFound, 0), | |
| 135 | totalSecurityIssues: results.reduce((n, r) => n + r.securityIssues, 0), | |
| 136 | reposBootstrapped: results.filter((r) => r.bootstrapped).length, | |
| 137 | reposWithFindings: results.filter( | |
| 138 | (r) => r.secretsFound > 0 || r.securityIssues > 0 | |
| 139 | ).length, | |
| 140 | }; | |
| 141 | } |