@doccov/api 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/package.json +3 -3
- package/src/index.ts +30 -2
- package/src/middleware/anonymous-rate-limit.ts +131 -0
- package/src/routes/ai.ts +353 -0
- package/src/routes/badge.ts +122 -32
- package/src/routes/billing.ts +65 -0
- package/src/routes/coverage.ts +34 -16
- package/src/routes/demo.ts +297 -0
- package/src/routes/github-app.ts +368 -0
- package/src/routes/invites.ts +90 -0
- package/src/routes/orgs.ts +249 -0
- package/src/utils/github-app.ts +196 -0
- package/src/utils/github-checks.ts +278 -0
- package/src/utils/remote-analyzer.ts +251 -0
- package/src/utils/github.ts +0 -5
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Check Runs and PR Comments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getTokenByInstallationId } from './github-app';
|
|
6
|
+
|
|
7
|
+
interface AnalysisResult {
|
|
8
|
+
coveragePercent: number;
|
|
9
|
+
documentedCount: number;
|
|
10
|
+
totalCount: number;
|
|
11
|
+
driftCount: number;
|
|
12
|
+
qualityErrors?: number;
|
|
13
|
+
qualityWarnings?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface AnalysisDiff {
|
|
17
|
+
coverageDelta: number;
|
|
18
|
+
documentedDelta: number;
|
|
19
|
+
totalDelta: number;
|
|
20
|
+
driftDelta: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format a delta value with sign and styling
|
|
25
|
+
*/
|
|
26
|
+
function formatDelta(delta: number, suffix = ''): string {
|
|
27
|
+
if (delta === 0) return '';
|
|
28
|
+
const sign = delta > 0 ? '+' : '';
|
|
29
|
+
return ` (${sign}${delta}${suffix})`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create or update a check run
|
|
34
|
+
*/
|
|
35
|
+
export async function createCheckRun(
|
|
36
|
+
installationId: string,
|
|
37
|
+
owner: string,
|
|
38
|
+
repo: string,
|
|
39
|
+
sha: string,
|
|
40
|
+
result: AnalysisResult,
|
|
41
|
+
diff?: AnalysisDiff | null,
|
|
42
|
+
): Promise<boolean> {
|
|
43
|
+
const token = await getTokenByInstallationId(installationId);
|
|
44
|
+
if (!token) return false;
|
|
45
|
+
|
|
46
|
+
const coverageDelta = diff ? formatDelta(diff.coverageDelta, '%') : '';
|
|
47
|
+
|
|
48
|
+
// Determine conclusion based on drift and quality
|
|
49
|
+
const hasIssues = result.driftCount > 0 || (result.qualityErrors ?? 0) > 0;
|
|
50
|
+
const conclusion = hasIssues ? 'neutral' : 'success';
|
|
51
|
+
|
|
52
|
+
const title = `Coverage: ${result.coveragePercent.toFixed(1)}%${coverageDelta}`;
|
|
53
|
+
|
|
54
|
+
const summaryLines = [
|
|
55
|
+
`## Documentation Coverage`,
|
|
56
|
+
'',
|
|
57
|
+
`\`\`\``,
|
|
58
|
+
`Coverage: ${result.coveragePercent.toFixed(1)}%${coverageDelta}`,
|
|
59
|
+
`├─ Documented: ${result.documentedCount}/${result.totalCount} exports`,
|
|
60
|
+
`├─ Drift: ${result.driftCount} issue${result.driftCount !== 1 ? 's' : ''}${diff ? formatDelta(-diff.driftDelta) : ''}`,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const qualityIssues = (result.qualityErrors ?? 0) + (result.qualityWarnings ?? 0);
|
|
64
|
+
if (qualityIssues > 0) {
|
|
65
|
+
summaryLines.push(
|
|
66
|
+
`└─ Quality: ${result.qualityErrors ?? 0} error${(result.qualityErrors ?? 0) !== 1 ? 's' : ''}, ${result.qualityWarnings ?? 0} warning${(result.qualityWarnings ?? 0) !== 1 ? 's' : ''}`,
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
summaryLines.push(`└─ Quality: ✓ No issues`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
summaryLines.push(`\`\`\``);
|
|
73
|
+
|
|
74
|
+
if (diff) {
|
|
75
|
+
summaryLines.push('', '### Changes vs base');
|
|
76
|
+
summaryLines.push('| Metric | Delta |');
|
|
77
|
+
summaryLines.push('|--------|-------|');
|
|
78
|
+
summaryLines.push(
|
|
79
|
+
`| Coverage | ${diff.coverageDelta >= 0 ? '📈' : '📉'} ${diff.coverageDelta >= 0 ? '+' : ''}${diff.coverageDelta.toFixed(1)}% |`,
|
|
80
|
+
);
|
|
81
|
+
if (diff.documentedDelta !== 0) {
|
|
82
|
+
summaryLines.push(
|
|
83
|
+
`| Documented | ${diff.documentedDelta >= 0 ? '+' : ''}${diff.documentedDelta} |`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (diff.driftDelta !== 0) {
|
|
87
|
+
const driftChange = -diff.driftDelta; // Positive driftDelta means drift increased (bad)
|
|
88
|
+
summaryLines.push(`| Drift issues | ${driftChange >= 0 ? '+' : ''}${driftChange} |`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
summaryLines.push('', `[View full report →](https://doccov.com/r/${owner}/${repo}/${sha})`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/check-runs`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
Authorization: `Bearer ${token}`,
|
|
99
|
+
Accept: 'application/vnd.github+json',
|
|
100
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
name: 'DocCov',
|
|
105
|
+
head_sha: sha,
|
|
106
|
+
status: 'completed',
|
|
107
|
+
conclusion,
|
|
108
|
+
output: {
|
|
109
|
+
title,
|
|
110
|
+
summary: summaryLines.join('\n'),
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
console.error('Failed to create check run:', await response.text());
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return true;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error('Error creating check run:', err);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Post or update a PR comment
|
|
129
|
+
*/
|
|
130
|
+
export async function postPRComment(
|
|
131
|
+
installationId: string,
|
|
132
|
+
owner: string,
|
|
133
|
+
repo: string,
|
|
134
|
+
prNumber: number,
|
|
135
|
+
result: AnalysisResult,
|
|
136
|
+
diff?: AnalysisDiff | null,
|
|
137
|
+
): Promise<boolean> {
|
|
138
|
+
const token = await getTokenByInstallationId(installationId);
|
|
139
|
+
if (!token) return false;
|
|
140
|
+
|
|
141
|
+
const coverageEmoji = diff
|
|
142
|
+
? diff.coverageDelta > 0
|
|
143
|
+
? '📈'
|
|
144
|
+
: diff.coverageDelta < 0
|
|
145
|
+
? '📉'
|
|
146
|
+
: '➡️'
|
|
147
|
+
: '📊';
|
|
148
|
+
|
|
149
|
+
const bodyLines = [`## ${coverageEmoji} DocCov Report`, ''];
|
|
150
|
+
|
|
151
|
+
// Summary table
|
|
152
|
+
if (diff) {
|
|
153
|
+
bodyLines.push('| Metric | This PR | Base | Δ |');
|
|
154
|
+
bodyLines.push('|--------|---------|------|---|');
|
|
155
|
+
bodyLines.push(
|
|
156
|
+
`| Coverage | **${result.coveragePercent.toFixed(1)}%** | ${(result.coveragePercent - diff.coverageDelta).toFixed(1)}% | ${diff.coverageDelta >= 0 ? '+' : ''}${diff.coverageDelta.toFixed(1)}% |`,
|
|
157
|
+
);
|
|
158
|
+
bodyLines.push(
|
|
159
|
+
`| Documented | ${result.documentedCount} | ${result.documentedCount - diff.documentedDelta} | ${diff.documentedDelta >= 0 ? '+' : ''}${diff.documentedDelta} |`,
|
|
160
|
+
);
|
|
161
|
+
bodyLines.push(
|
|
162
|
+
`| Total exports | ${result.totalCount} | ${result.totalCount - diff.totalDelta} | ${diff.totalDelta >= 0 ? '+' : ''}${diff.totalDelta} |`,
|
|
163
|
+
);
|
|
164
|
+
const baseDrift = result.driftCount - diff.driftDelta;
|
|
165
|
+
const driftSign = diff.driftDelta >= 0 ? '+' : '';
|
|
166
|
+
bodyLines.push(
|
|
167
|
+
`| Drift issues | ${result.driftCount} | ${baseDrift} | ${driftSign}${diff.driftDelta} |`,
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
bodyLines.push('| Metric | Value |');
|
|
171
|
+
bodyLines.push('|--------|-------|');
|
|
172
|
+
bodyLines.push(`| Coverage | **${result.coveragePercent.toFixed(1)}%** |`);
|
|
173
|
+
bodyLines.push(`| Documented | ${result.documentedCount} / ${result.totalCount} |`);
|
|
174
|
+
bodyLines.push(`| Drift issues | ${result.driftCount} |`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
bodyLines.push('');
|
|
178
|
+
|
|
179
|
+
// Quality summary
|
|
180
|
+
const qualityErrors = result.qualityErrors ?? 0;
|
|
181
|
+
const qualityWarnings = result.qualityWarnings ?? 0;
|
|
182
|
+
if (qualityErrors > 0 || qualityWarnings > 0) {
|
|
183
|
+
bodyLines.push(
|
|
184
|
+
`**Quality**: ${qualityErrors} error${qualityErrors !== 1 ? 's' : ''}, ${qualityWarnings} warning${qualityWarnings !== 1 ? 's' : ''}`,
|
|
185
|
+
);
|
|
186
|
+
bodyLines.push('');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Status message
|
|
190
|
+
if (result.driftCount > 0) {
|
|
191
|
+
bodyLines.push('⚠️ Documentation drift detected. Run `doccov check --fix` to update.');
|
|
192
|
+
} else if (qualityErrors > 0) {
|
|
193
|
+
bodyLines.push('❌ Quality errors found. Run `doccov check` for details.');
|
|
194
|
+
} else {
|
|
195
|
+
bodyLines.push('✅ Documentation is in sync.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
bodyLines.push('');
|
|
199
|
+
bodyLines.push('---');
|
|
200
|
+
bodyLines.push('*Generated by [DocCov](https://doccov.com)*');
|
|
201
|
+
|
|
202
|
+
const body = bodyLines.join('\n');
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// Check for existing comment
|
|
206
|
+
const existingId = await findExistingComment(token, owner, repo, prNumber);
|
|
207
|
+
|
|
208
|
+
if (existingId) {
|
|
209
|
+
// Update existing comment
|
|
210
|
+
const response = await fetch(
|
|
211
|
+
`https://api.github.com/repos/${owner}/${repo}/issues/comments/${existingId}`,
|
|
212
|
+
{
|
|
213
|
+
method: 'PATCH',
|
|
214
|
+
headers: {
|
|
215
|
+
Authorization: `Bearer ${token}`,
|
|
216
|
+
Accept: 'application/vnd.github+json',
|
|
217
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
218
|
+
'Content-Type': 'application/json',
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify({ body }),
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
return response.ok;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Create new comment
|
|
227
|
+
const response = await fetch(
|
|
228
|
+
`https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`,
|
|
229
|
+
{
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: {
|
|
232
|
+
Authorization: `Bearer ${token}`,
|
|
233
|
+
Accept: 'application/vnd.github+json',
|
|
234
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
235
|
+
'Content-Type': 'application/json',
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify({ body }),
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return response.ok;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error('Error posting PR comment:', err);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Find existing DocCov comment on a PR
|
|
250
|
+
*/
|
|
251
|
+
async function findExistingComment(
|
|
252
|
+
token: string,
|
|
253
|
+
owner: string,
|
|
254
|
+
repo: string,
|
|
255
|
+
prNumber: number,
|
|
256
|
+
): Promise<number | null> {
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch(
|
|
259
|
+
`https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`,
|
|
260
|
+
{
|
|
261
|
+
headers: {
|
|
262
|
+
Authorization: `Bearer ${token}`,
|
|
263
|
+
Accept: 'application/vnd.github+json',
|
|
264
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (!response.ok) return null;
|
|
270
|
+
|
|
271
|
+
const comments = (await response.json()) as Array<{ id: number; body: string }>;
|
|
272
|
+
const existing = comments.find((c) => c.body.includes('DocCov Report'));
|
|
273
|
+
|
|
274
|
+
return existing?.id ?? null;
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote repository analyzer - runs DocCov analysis on GitHub repos via webhooks.
|
|
3
|
+
* Uses shallow clone to temp dir for full TypeScript resolution.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { rm } from 'node:fs/promises';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { DocCov, enrichSpec, type OpenPkgSpec } from '@doccov/sdk';
|
|
10
|
+
import { getTokenByInstallationId } from './github-app';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Result from remote analysis.
|
|
14
|
+
*/
|
|
15
|
+
export interface RemoteAnalysisResult {
|
|
16
|
+
coveragePercent: number;
|
|
17
|
+
documentedCount: number;
|
|
18
|
+
totalCount: number;
|
|
19
|
+
driftCount: number;
|
|
20
|
+
qualityErrors: number;
|
|
21
|
+
qualityWarnings: number;
|
|
22
|
+
/** Full spec for detailed reports */
|
|
23
|
+
spec?: OpenPkgSpec;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shallow clone a GitHub repo to a temp directory.
|
|
28
|
+
*/
|
|
29
|
+
async function cloneRepo(
|
|
30
|
+
owner: string,
|
|
31
|
+
repo: string,
|
|
32
|
+
ref: string,
|
|
33
|
+
authToken: string,
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const tmpDir = join(tmpdir(), `doccov-${owner}-${repo}-${Date.now()}`);
|
|
36
|
+
|
|
37
|
+
// Use authenticated HTTPS URL for private repos
|
|
38
|
+
const cloneUrl = `https://x-access-token:${authToken}@github.com/${owner}/${repo}.git`;
|
|
39
|
+
|
|
40
|
+
const proc = Bun.spawn(['git', 'clone', '--depth', '1', '--branch', ref, cloneUrl, tmpDir], {
|
|
41
|
+
stdout: 'pipe',
|
|
42
|
+
stderr: 'pipe',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const exitCode = await proc.exited;
|
|
46
|
+
|
|
47
|
+
if (exitCode !== 0) {
|
|
48
|
+
const stderr = await new Response(proc.stderr).text();
|
|
49
|
+
|
|
50
|
+
// Try fetching specific SHA if branch clone fails
|
|
51
|
+
if (ref.length === 40) {
|
|
52
|
+
// Looks like a SHA
|
|
53
|
+
const shallowProc = Bun.spawn(['git', 'clone', '--depth', '1', cloneUrl, tmpDir], {
|
|
54
|
+
stdout: 'pipe',
|
|
55
|
+
stderr: 'pipe',
|
|
56
|
+
});
|
|
57
|
+
await shallowProc.exited;
|
|
58
|
+
|
|
59
|
+
const fetchProc = Bun.spawn(['git', '-C', tmpDir, 'fetch', 'origin', ref], {
|
|
60
|
+
stdout: 'pipe',
|
|
61
|
+
stderr: 'pipe',
|
|
62
|
+
});
|
|
63
|
+
await fetchProc.exited;
|
|
64
|
+
|
|
65
|
+
const checkoutProc = Bun.spawn(['git', '-C', tmpDir, 'checkout', ref], {
|
|
66
|
+
stdout: 'pipe',
|
|
67
|
+
stderr: 'pipe',
|
|
68
|
+
});
|
|
69
|
+
const checkoutExit = await checkoutProc.exited;
|
|
70
|
+
|
|
71
|
+
if (checkoutExit !== 0) {
|
|
72
|
+
throw new Error(`Failed to checkout ${ref}: ${stderr}`);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(`Failed to clone ${owner}/${repo}@${ref}: ${stderr}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return tmpDir;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Detect the entry point for a package.
|
|
84
|
+
*/
|
|
85
|
+
async function detectEntryPoint(repoDir: string): Promise<string | null> {
|
|
86
|
+
try {
|
|
87
|
+
const packageJsonPath = join(repoDir, 'package.json');
|
|
88
|
+
const packageJson = await Bun.file(packageJsonPath).json();
|
|
89
|
+
|
|
90
|
+
// Check exports first (modern packages)
|
|
91
|
+
if (packageJson.exports) {
|
|
92
|
+
const mainExport = packageJson.exports['.'];
|
|
93
|
+
if (typeof mainExport === 'string') {
|
|
94
|
+
return mainExport.replace(/^\.\//, '');
|
|
95
|
+
}
|
|
96
|
+
if (mainExport?.import) {
|
|
97
|
+
const importPath =
|
|
98
|
+
typeof mainExport.import === 'string' ? mainExport.import : mainExport.import.default;
|
|
99
|
+
if (importPath) return importPath.replace(/^\.\//, '');
|
|
100
|
+
}
|
|
101
|
+
if (mainExport?.types) {
|
|
102
|
+
return mainExport.types.replace(/^\.\//, '');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check types field
|
|
107
|
+
if (packageJson.types) {
|
|
108
|
+
return packageJson.types.replace(/^\.\//, '');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check main field
|
|
112
|
+
if (packageJson.main) {
|
|
113
|
+
// Convert .js to .ts if applicable
|
|
114
|
+
const main = packageJson.main.replace(/^\.\//, '');
|
|
115
|
+
const tsMain = main.replace(/\.js$/, '.ts');
|
|
116
|
+
const tsxMain = main.replace(/\.js$/, '.tsx');
|
|
117
|
+
|
|
118
|
+
// Check if TS version exists
|
|
119
|
+
const tsFile = Bun.file(join(repoDir, tsMain));
|
|
120
|
+
if (await tsFile.exists()) return tsMain;
|
|
121
|
+
|
|
122
|
+
const tsxFile = Bun.file(join(repoDir, tsxMain));
|
|
123
|
+
if (await tsxFile.exists()) return tsxMain;
|
|
124
|
+
|
|
125
|
+
return main;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Common fallbacks
|
|
129
|
+
const fallbacks = ['src/index.ts', 'src/index.tsx', 'index.ts', 'lib/index.ts'];
|
|
130
|
+
for (const fallback of fallbacks) {
|
|
131
|
+
const file = Bun.file(join(repoDir, fallback));
|
|
132
|
+
if (await file.exists()) return fallback;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Analyze a remote GitHub repository.
|
|
143
|
+
*
|
|
144
|
+
* @param installationId - GitHub App installation ID
|
|
145
|
+
* @param owner - Repository owner
|
|
146
|
+
* @param repo - Repository name
|
|
147
|
+
* @param ref - Git ref (branch, tag, or SHA)
|
|
148
|
+
* @param includeSpec - Whether to include full spec in result
|
|
149
|
+
* @returns Analysis result or null if failed
|
|
150
|
+
*/
|
|
151
|
+
export async function analyzeRemoteRepo(
|
|
152
|
+
installationId: string,
|
|
153
|
+
owner: string,
|
|
154
|
+
repo: string,
|
|
155
|
+
ref: string,
|
|
156
|
+
includeSpec = false,
|
|
157
|
+
): Promise<RemoteAnalysisResult | null> {
|
|
158
|
+
// Get installation token
|
|
159
|
+
const token = await getTokenByInstallationId(installationId);
|
|
160
|
+
if (!token) {
|
|
161
|
+
console.error(`No token for installation ${installationId}`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let tmpDir: string | null = null;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Clone repo to temp dir
|
|
169
|
+
tmpDir = await cloneRepo(owner, repo, ref, token);
|
|
170
|
+
|
|
171
|
+
// Detect entry point
|
|
172
|
+
const entryPoint = await detectEntryPoint(tmpDir);
|
|
173
|
+
if (!entryPoint) {
|
|
174
|
+
console.error(`No entry point found for ${owner}/${repo}`);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const entryPath = join(tmpDir, entryPoint);
|
|
179
|
+
|
|
180
|
+
// Run analysis
|
|
181
|
+
const doccov = new DocCov({
|
|
182
|
+
resolveExternalTypes: false, // Skip for speed
|
|
183
|
+
useCache: false, // No caching for webhook analysis
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const result = await doccov.analyzeFileWithDiagnostics(entryPath);
|
|
187
|
+
|
|
188
|
+
// Enrich with coverage metrics
|
|
189
|
+
const enriched = enrichSpec(result.spec);
|
|
190
|
+
|
|
191
|
+
// Extract metrics
|
|
192
|
+
const docs = enriched.docs;
|
|
193
|
+
const coveragePercent = docs?.coverageScore ?? 0;
|
|
194
|
+
const documentedCount = docs?.documented ?? 0;
|
|
195
|
+
const totalCount = docs?.total ?? 0;
|
|
196
|
+
const driftCount = docs?.drift?.length ?? 0;
|
|
197
|
+
|
|
198
|
+
// Count quality issues
|
|
199
|
+
let qualityErrors = 0;
|
|
200
|
+
let qualityWarnings = 0;
|
|
201
|
+
|
|
202
|
+
if (docs?.quality) {
|
|
203
|
+
for (const item of docs.quality) {
|
|
204
|
+
if (item.severity === 'error') qualityErrors++;
|
|
205
|
+
else if (item.severity === 'warning') qualityWarnings++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
coveragePercent,
|
|
211
|
+
documentedCount,
|
|
212
|
+
totalCount,
|
|
213
|
+
driftCount,
|
|
214
|
+
qualityErrors,
|
|
215
|
+
qualityWarnings,
|
|
216
|
+
spec: includeSpec ? enriched : undefined,
|
|
217
|
+
};
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error(`Analysis failed for ${owner}/${repo}@${ref}:`, err);
|
|
220
|
+
return null;
|
|
221
|
+
} finally {
|
|
222
|
+
// Cleanup temp dir
|
|
223
|
+
if (tmpDir) {
|
|
224
|
+
try {
|
|
225
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
226
|
+
} catch {
|
|
227
|
+
// Ignore cleanup errors
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Compute diff between two analysis results.
|
|
235
|
+
*/
|
|
236
|
+
export function computeAnalysisDiff(
|
|
237
|
+
base: RemoteAnalysisResult,
|
|
238
|
+
head: RemoteAnalysisResult,
|
|
239
|
+
): {
|
|
240
|
+
coverageDelta: number;
|
|
241
|
+
documentedDelta: number;
|
|
242
|
+
totalDelta: number;
|
|
243
|
+
driftDelta: number;
|
|
244
|
+
} {
|
|
245
|
+
return {
|
|
246
|
+
coverageDelta: Number((head.coveragePercent - base.coveragePercent).toFixed(1)),
|
|
247
|
+
documentedDelta: head.documentedCount - base.documentedCount,
|
|
248
|
+
totalDelta: head.totalCount - base.totalCount,
|
|
249
|
+
driftDelta: head.driftCount - base.driftCount,
|
|
250
|
+
};
|
|
251
|
+
}
|