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