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.tsBlame171 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;
5d13cf5ccantynz-alt46 /** Code files embedded into the semantic index. */
47 filesIndexed: number;
8ae1b1fccantynz-alt48 /** Human-readable scan detail, already scrubbed by the scanner. */
49 scanDetail: string;
50 /** Set when this repo's onboarding failed; the others still ran. */
51 error?: string;
52}
53
54export interface MigrationReport {
55 repos: MigrationRepoReport[];
56 /** Totals, precomputed so the view stays markup. */
57 totalRepos: number;
58 totalSecrets: number;
59 totalSecurityIssues: number;
60 reposBootstrapped: number;
5d13cf5ccantynz-alt61 /** Total code files embedded — what makes semantic search work at all. */
62 totalFilesIndexed: number;
8ae1b1fccantynz-alt63 reposWithFindings: number;
64}
65
66/**
67 * Bootstrap and scan every freshly imported repository.
68 *
69 * `repos` is the subset of a bulk import that actually landed — callers pass
70 * only successful imports, since there is nothing to scan otherwise.
71 */
72export async function runMigrationOnboarding(
73 repos: Array<{ id: string; owner: string; name: string; defaultBranch?: string | null }>,
74 ownerUserId: string
75): Promise<MigrationReport> {
76 const results = await mapWithConcurrency(
77 repos,
78 DB_FANOUT_LIMIT,
79 async (repo): Promise<MigrationRepoReport> => {
80 const base: MigrationRepoReport = {
81 owner: repo.owner,
82 name: repo.name,
83 bootstrapped: false,
84 labelsCreated: 0,
85 protectionCreated: false,
86 secretsFound: 0,
87 securityIssues: 0,
5d13cf5ccantynz-alt88 filesIndexed: 0,
8ae1b1fccantynz-alt89 scanDetail: "",
90 };
91
92 const branch = repo.defaultBranch || "main";
93
94 // 1. Green defaults. Skips the welcome issue — an imported repo has
95 // real history and real issues; a "welcome" issue on top of 400
96 // migrated ones is noise, not onboarding.
97 try {
98 const { bootstrapRepository } = await import("./repo-bootstrap");
99 const r = await bootstrapRepository({
100 repositoryId: repo.id,
101 ownerUserId,
102 defaultBranch: branch,
103 skipWelcomeIssue: true,
104 });
105 base.bootstrapped = true;
106 base.labelsCreated = r.labelsCreated ?? 0;
107 base.protectionCreated = r.protectionCreated ?? false;
108 } catch (err) {
109 base.error = `bootstrap: ${err instanceof Error ? err.message : String(err)}`;
110 }
111
112 // 2. The proof. Scan the code they just brought over.
113 try {
114 const { runSecretAndSecurityScan } = await import("./gate");
115 const scan = await runSecretAndSecurityScan(
116 repo.owner,
117 repo.name,
118 `refs/heads/${branch}`,
119 branch,
120 { scanSecrets: true, scanSecurity: true }
121 );
122 base.secretsFound = scan.secrets.length;
123 base.securityIssues = scan.securityIssues.length;
124 base.scanDetail = [scan.secretResult.details, scan.securityResult.details]
125 .filter(Boolean)
126 .join(" · ");
127 } catch (err) {
128 const msg = `scan: ${err instanceof Error ? err.message : String(err)}`;
129 base.error = base.error ? `${base.error}; ${msg}` : msg;
130 }
131
5d13cf5ccantynz-alt132 // 3. Semantic index. indexChangedFiles() is called only from the git
133 // post-receive hook, and a bulk import CLONES rather than pushes —
134 // so without this every imported repo had an empty index and
135 // semantic search, repo chat and "ask this repo" all returned
136 // nothing. Silently, because an empty index looks exactly like
137 // "no matches".
138 try {
139 const { indexExistingRepo } = await import("./index-existing-repo");
140 const idx = await indexExistingRepo({
141 repositoryId: repo.id,
142 owner: repo.owner,
143 repoName: repo.name,
144 ref: branch,
145 commitSha: branch,
146 });
147 base.filesIndexed = idx.indexed;
148 if (idx.error) {
149 base.error = base.error ? `${base.error}; index: ${idx.error}` : `index: ${idx.error}`;
150 }
151 } catch (err) {
152 const msg = `index: ${err instanceof Error ? err.message : String(err)}`;
153 base.error = base.error ? `${base.error}; ${msg}` : msg;
154 }
155
8ae1b1fccantynz-alt156 return base;
157 }
158 );
159
160 return {
161 repos: results,
162 totalRepos: results.length,
163 totalSecrets: results.reduce((n, r) => n + r.secretsFound, 0),
164 totalSecurityIssues: results.reduce((n, r) => n + r.securityIssues, 0),
5d13cf5ccantynz-alt165 totalFilesIndexed: results.reduce((n, r) => n + r.filesIndexed, 0),
8ae1b1fccantynz-alt166 reposBootstrapped: results.filter((r) => r.bootstrapped).length,
167 reposWithFindings: results.filter(
168 (r) => r.secretsFound > 0 || r.securityIssues > 0
169 ).length,
170 };
171}