Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.

migration-onboarding.tsBlame141 lines · 1 contributor
8ae1b1fccantynz-alt1/**
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
31import { mapWithConcurrency, DB_FANOUT_LIMIT } from "./concurrency";
32
33/** One repository's post-import outcome. */
34export 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
52export 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 */
68export 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}