@oss-autopilot/core 0.53.1 → 0.55.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/dist/cli.bundle.cjs +63 -63
- package/dist/commands/comments.js +0 -1
- package/dist/commands/config.js +45 -5
- package/dist/commands/daily.js +197 -162
- package/dist/commands/dashboard-data.js +37 -30
- package/dist/commands/dashboard-server.js +8 -1
- package/dist/commands/dismiss.js +0 -6
- package/dist/commands/init.js +0 -1
- package/dist/commands/local-repos.js +1 -2
- package/dist/commands/move.js +12 -11
- package/dist/commands/setup.d.ts +2 -1
- package/dist/commands/setup.js +166 -130
- package/dist/commands/shelve.js +10 -10
- package/dist/commands/startup.js +30 -14
- package/dist/core/ci-analysis.d.ts +6 -0
- package/dist/core/ci-analysis.js +91 -12
- package/dist/core/daily-logic.js +24 -33
- package/dist/core/display-utils.js +22 -2
- package/dist/core/github-stats.d.ts +1 -1
- package/dist/core/github-stats.js +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/issue-discovery.d.ts +7 -44
- package/dist/core/issue-discovery.js +83 -188
- package/dist/core/issue-eligibility.d.ts +35 -0
- package/dist/core/issue-eligibility.js +126 -0
- package/dist/core/issue-vetting.d.ts +6 -21
- package/dist/core/issue-vetting.js +15 -279
- package/dist/core/pr-monitor.d.ts +14 -16
- package/dist/core/pr-monitor.js +26 -90
- package/dist/core/repo-health.d.ts +24 -0
- package/dist/core/repo-health.js +193 -0
- package/dist/core/repo-score-manager.js +2 -0
- package/dist/core/search-phases.d.ts +55 -0
- package/dist/core/search-phases.js +155 -0
- package/dist/core/state.d.ts +11 -0
- package/dist/core/state.js +63 -4
- package/dist/core/status-determination.d.ts +2 -0
- package/dist/core/status-determination.js +82 -22
- package/dist/core/types.d.ts +23 -2
- package/dist/core/types.js +7 -0
- package/dist/formatters/json.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,41 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Issue Vetting —
|
|
3
|
-
*
|
|
2
|
+
* Issue Vetting — orchestrates individual issue checks and computes
|
|
3
|
+
* recommendation + viability score.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Delegates to focused modules (#621):
|
|
6
|
+
* - issue-eligibility.ts — PR existence, claim detection, requirements analysis
|
|
7
|
+
* - repo-health.ts — project health, contribution guidelines
|
|
6
8
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { parseGitHubUrl, daysBetween, DEFAULT_CONCURRENCY } from './utils.js';
|
|
9
|
+
import { parseGitHubUrl, DEFAULT_CONCURRENCY } from './utils.js';
|
|
9
10
|
import { ValidationError, errorMessage, isRateLimitError } from './errors.js';
|
|
10
11
|
import { warn } from './logger.js';
|
|
11
|
-
import { getHttpCache, cachedRequest, cachedTimeBased } from './http-cache.js';
|
|
12
12
|
import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
|
|
13
13
|
import { repoBelongsToCategory } from './category-mapping.js';
|
|
14
|
+
import { checkNoExistingPR, checkNotClaimed, checkUserMergedPRsInRepo, analyzeRequirements, } from './issue-eligibility.js';
|
|
15
|
+
import { checkProjectHealth, fetchContributionGuidelines } from './repo-health.js';
|
|
14
16
|
const MODULE = 'issue-vetting';
|
|
15
17
|
const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
|
|
16
|
-
// Cache for contribution guidelines (expires after 1 hour, max 100 entries)
|
|
17
|
-
const guidelinesCache = new Map();
|
|
18
|
-
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
19
|
-
/** TTL for cached project health results (4 hours). Health data (stars, commits, CI) changes slowly. */
|
|
20
|
-
const HEALTH_CACHE_TTL_MS = 4 * 60 * 60 * 1000;
|
|
21
|
-
const CACHE_MAX_SIZE = 100;
|
|
22
|
-
function pruneCache() {
|
|
23
|
-
const now = Date.now();
|
|
24
|
-
// First, remove expired entries (older than CACHE_TTL_MS)
|
|
25
|
-
for (const [key, value] of guidelinesCache.entries()) {
|
|
26
|
-
if (now - value.fetchedAt > CACHE_TTL_MS) {
|
|
27
|
-
guidelinesCache.delete(key);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Then, if still over size limit, remove oldest entries
|
|
31
|
-
if (guidelinesCache.size > CACHE_MAX_SIZE) {
|
|
32
|
-
const entries = Array.from(guidelinesCache.entries()).sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
|
|
33
|
-
const toRemove = entries.slice(0, guidelinesCache.size - CACHE_MAX_SIZE);
|
|
34
|
-
for (const [key] of toRemove) {
|
|
35
|
-
guidelinesCache.delete(key);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
18
|
export class IssueVetter {
|
|
40
19
|
octokit;
|
|
41
20
|
stateManager;
|
|
@@ -60,18 +39,18 @@ export class IssueVetter {
|
|
|
60
39
|
repo,
|
|
61
40
|
issue_number: number,
|
|
62
41
|
});
|
|
63
|
-
// Run all vetting checks in parallel
|
|
42
|
+
// Run all vetting checks in parallel — delegates to standalone functions
|
|
64
43
|
const [existingPRCheck, claimCheck, projectHealth, contributionGuidelines, userMergedPRCount] = await Promise.all([
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
44
|
+
checkNoExistingPR(this.octokit, owner, repo, number),
|
|
45
|
+
checkNotClaimed(this.octokit, owner, repo, number, ghIssue.comments),
|
|
46
|
+
checkProjectHealth(this.octokit, owner, repo),
|
|
47
|
+
fetchContributionGuidelines(this.octokit, owner, repo),
|
|
48
|
+
checkUserMergedPRsInRepo(this.octokit, owner, repo),
|
|
70
49
|
]);
|
|
71
50
|
const noExistingPR = existingPRCheck.passed;
|
|
72
51
|
const notClaimed = claimCheck.passed;
|
|
73
52
|
// Analyze issue quality
|
|
74
|
-
const clearRequirements =
|
|
53
|
+
const clearRequirements = analyzeRequirements(ghIssue.body || '');
|
|
75
54
|
// When the health check itself failed (API error), use a neutral default:
|
|
76
55
|
// don't penalize the repo as inactive, but don't credit it as active either.
|
|
77
56
|
const projectActive = projectHealth.checkFailed ? true : projectHealth.isActive;
|
|
@@ -285,249 +264,6 @@ export class IssueVetter {
|
|
|
285
264
|
}
|
|
286
265
|
return { candidates: candidates.slice(0, maxResults), allFailed, rateLimitHit: rateLimitFailures > 0 };
|
|
287
266
|
}
|
|
288
|
-
async checkNoExistingPR(owner, repo, issueNumber) {
|
|
289
|
-
try {
|
|
290
|
-
// Search for PRs that mention this issue
|
|
291
|
-
const { data } = await this.octokit.search.issuesAndPullRequests({
|
|
292
|
-
q: `repo:${owner}/${repo} is:pr ${issueNumber}`,
|
|
293
|
-
per_page: 5,
|
|
294
|
-
});
|
|
295
|
-
// Also check timeline for linked PRs
|
|
296
|
-
const timeline = await paginateAll((page) => this.octokit.issues.listEventsForTimeline({
|
|
297
|
-
owner,
|
|
298
|
-
repo,
|
|
299
|
-
issue_number: issueNumber,
|
|
300
|
-
per_page: 100,
|
|
301
|
-
page,
|
|
302
|
-
}));
|
|
303
|
-
const linkedPRs = timeline.filter((event) => {
|
|
304
|
-
const e = event;
|
|
305
|
-
return e.event === 'cross-referenced' && e.source?.issue?.pull_request;
|
|
306
|
-
});
|
|
307
|
-
return { passed: data.total_count === 0 && linkedPRs.length === 0 };
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
const errMsg = errorMessage(error);
|
|
311
|
-
warn(MODULE, `Failed to check for existing PRs on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming no existing PR.`);
|
|
312
|
-
return { passed: true, inconclusive: true, reason: errMsg };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Check how many merged PRs the authenticated user has in a repo.
|
|
317
|
-
* Uses GitHub Search API. Returns 0 on error (non-fatal).
|
|
318
|
-
*/
|
|
319
|
-
async checkUserMergedPRsInRepo(owner, repo) {
|
|
320
|
-
try {
|
|
321
|
-
// Use @me to search as the authenticated user
|
|
322
|
-
const { data } = await this.octokit.search.issuesAndPullRequests({
|
|
323
|
-
q: `repo:${owner}/${repo} is:pr is:merged author:@me`,
|
|
324
|
-
per_page: 1, // We only need total_count
|
|
325
|
-
});
|
|
326
|
-
return data.total_count;
|
|
327
|
-
}
|
|
328
|
-
catch (error) {
|
|
329
|
-
const errMsg = errorMessage(error);
|
|
330
|
-
warn(MODULE, `Could not check merged PRs in ${owner}/${repo}: ${errMsg}. Defaulting to 0.`);
|
|
331
|
-
return 0;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
async checkNotClaimed(owner, repo, issueNumber, commentCount) {
|
|
335
|
-
if (commentCount === 0)
|
|
336
|
-
return { passed: true };
|
|
337
|
-
try {
|
|
338
|
-
// Paginate through all comments (up to 100)
|
|
339
|
-
const comments = await this.octokit.paginate(this.octokit.issues.listComments, {
|
|
340
|
-
owner,
|
|
341
|
-
repo,
|
|
342
|
-
issue_number: issueNumber,
|
|
343
|
-
per_page: 100,
|
|
344
|
-
}, (response) => response.data);
|
|
345
|
-
// Limit to last 100 comments to avoid excessive processing
|
|
346
|
-
const recentComments = comments.slice(-100);
|
|
347
|
-
// Look for claiming phrases
|
|
348
|
-
const claimPhrases = [
|
|
349
|
-
"i'm working on this",
|
|
350
|
-
'i am working on this',
|
|
351
|
-
"i'll take this",
|
|
352
|
-
'i will take this',
|
|
353
|
-
'working on it',
|
|
354
|
-
"i'd like to work on",
|
|
355
|
-
'i would like to work on',
|
|
356
|
-
'can i work on',
|
|
357
|
-
'may i work on',
|
|
358
|
-
'assigned to me',
|
|
359
|
-
"i'm on it",
|
|
360
|
-
"i'll submit a pr",
|
|
361
|
-
'i will submit a pr',
|
|
362
|
-
'working on a fix',
|
|
363
|
-
'working on a pr',
|
|
364
|
-
];
|
|
365
|
-
for (const comment of recentComments) {
|
|
366
|
-
const body = (comment.body || '').toLowerCase();
|
|
367
|
-
if (claimPhrases.some((phrase) => body.includes(phrase))) {
|
|
368
|
-
return { passed: false };
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return { passed: true };
|
|
372
|
-
}
|
|
373
|
-
catch (error) {
|
|
374
|
-
const errMsg = errorMessage(error);
|
|
375
|
-
warn(MODULE, `Failed to check claim status on ${owner}/${repo}#${issueNumber}: ${errMsg}. Assuming not claimed.`);
|
|
376
|
-
return { passed: true, inconclusive: true, reason: errMsg };
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async checkProjectHealth(owner, repo) {
|
|
380
|
-
const cache = getHttpCache();
|
|
381
|
-
const healthCacheKey = `health:${owner}/${repo}`;
|
|
382
|
-
try {
|
|
383
|
-
return await cachedTimeBased(cache, healthCacheKey, HEALTH_CACHE_TTL_MS, async () => {
|
|
384
|
-
// Get repo info (with ETag caching — repo metadata changes infrequently)
|
|
385
|
-
const url = `/repos/${owner}/${repo}`;
|
|
386
|
-
const repoData = await cachedRequest(cache, url, (headers) => this.octokit.repos.get({ owner, repo, headers }));
|
|
387
|
-
// Get recent commits
|
|
388
|
-
const { data: commits } = await this.octokit.repos.listCommits({
|
|
389
|
-
owner,
|
|
390
|
-
repo,
|
|
391
|
-
per_page: 1,
|
|
392
|
-
});
|
|
393
|
-
const lastCommit = commits[0];
|
|
394
|
-
const lastCommitAt = lastCommit?.commit?.author?.date || repoData.pushed_at;
|
|
395
|
-
const daysSinceLastCommit = daysBetween(new Date(lastCommitAt));
|
|
396
|
-
// Check CI status (simplified - just check if workflows exist)
|
|
397
|
-
let ciStatus = 'unknown';
|
|
398
|
-
try {
|
|
399
|
-
const { data: workflows } = await this.octokit.actions.listRepoWorkflows({
|
|
400
|
-
owner,
|
|
401
|
-
repo,
|
|
402
|
-
per_page: 1,
|
|
403
|
-
});
|
|
404
|
-
if (workflows.total_count > 0) {
|
|
405
|
-
ciStatus = 'passing'; // Assume passing if workflows exist
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
const errMsg = errorMessage(error);
|
|
410
|
-
warn(MODULE, `Failed to check CI status for ${owner}/${repo}: ${errMsg}. Defaulting to unknown.`);
|
|
411
|
-
}
|
|
412
|
-
return {
|
|
413
|
-
repo: `${owner}/${repo}`,
|
|
414
|
-
lastCommitAt,
|
|
415
|
-
daysSinceLastCommit,
|
|
416
|
-
openIssuesCount: repoData.open_issues_count,
|
|
417
|
-
avgIssueResponseDays: 0, // Would need more API calls to calculate
|
|
418
|
-
ciStatus,
|
|
419
|
-
isActive: daysSinceLastCommit < 30,
|
|
420
|
-
stargazersCount: repoData.stargazers_count,
|
|
421
|
-
forksCount: repoData.forks_count,
|
|
422
|
-
};
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
catch (error) {
|
|
426
|
-
const errMsg = errorMessage(error);
|
|
427
|
-
warn(MODULE, `Error checking project health for ${owner}/${repo}: ${errMsg}`);
|
|
428
|
-
return {
|
|
429
|
-
repo: `${owner}/${repo}`,
|
|
430
|
-
lastCommitAt: '',
|
|
431
|
-
daysSinceLastCommit: 999,
|
|
432
|
-
openIssuesCount: 0,
|
|
433
|
-
avgIssueResponseDays: 0,
|
|
434
|
-
ciStatus: 'unknown',
|
|
435
|
-
isActive: false,
|
|
436
|
-
checkFailed: true,
|
|
437
|
-
failureReason: errMsg,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
async fetchContributionGuidelines(owner, repo) {
|
|
442
|
-
const cacheKey = `${owner}/${repo}`;
|
|
443
|
-
// Check cache first
|
|
444
|
-
const cached = guidelinesCache.get(cacheKey);
|
|
445
|
-
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
446
|
-
return cached.guidelines;
|
|
447
|
-
}
|
|
448
|
-
const filesToCheck = ['CONTRIBUTING.md', '.github/CONTRIBUTING.md', 'docs/CONTRIBUTING.md', 'contributing.md'];
|
|
449
|
-
// Probe all paths in parallel — take the first success in priority order
|
|
450
|
-
const results = await Promise.allSettled(filesToCheck.map((file) => this.octokit.repos.getContent({ owner, repo, path: file }).then(({ data }) => {
|
|
451
|
-
if ('content' in data) {
|
|
452
|
-
return Buffer.from(data.content, 'base64').toString('utf-8');
|
|
453
|
-
}
|
|
454
|
-
return null;
|
|
455
|
-
})));
|
|
456
|
-
for (let i = 0; i < results.length; i++) {
|
|
457
|
-
const result = results[i];
|
|
458
|
-
if (result.status === 'fulfilled' && result.value) {
|
|
459
|
-
const guidelines = this.parseContributionGuidelines(result.value);
|
|
460
|
-
guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
|
|
461
|
-
pruneCache();
|
|
462
|
-
return guidelines;
|
|
463
|
-
}
|
|
464
|
-
if (result.status === 'rejected') {
|
|
465
|
-
const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
466
|
-
if (!msg.includes('404') && !msg.includes('Not Found')) {
|
|
467
|
-
warn(MODULE, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${msg}`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
// Cache the negative result too and prune if needed
|
|
472
|
-
guidelinesCache.set(cacheKey, { guidelines: undefined, fetchedAt: Date.now() });
|
|
473
|
-
pruneCache();
|
|
474
|
-
return undefined;
|
|
475
|
-
}
|
|
476
|
-
parseContributionGuidelines(content) {
|
|
477
|
-
const guidelines = {
|
|
478
|
-
rawContent: content,
|
|
479
|
-
};
|
|
480
|
-
const lowerContent = content.toLowerCase();
|
|
481
|
-
// Detect branch naming conventions
|
|
482
|
-
if (lowerContent.includes('branch')) {
|
|
483
|
-
const branchMatch = content.match(/branch[^\n]*(?:named?|format|convention)[^\n]*[`"]([^`"]+)[`"]/i);
|
|
484
|
-
if (branchMatch) {
|
|
485
|
-
guidelines.branchNamingConvention = branchMatch[1];
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
// Detect commit message format
|
|
489
|
-
if (lowerContent.includes('conventional commit')) {
|
|
490
|
-
guidelines.commitMessageFormat = 'conventional commits';
|
|
491
|
-
}
|
|
492
|
-
else if (lowerContent.includes('commit message')) {
|
|
493
|
-
const commitMatch = content.match(/commit message[^\n]*[`"]([^`"]+)[`"]/i);
|
|
494
|
-
if (commitMatch) {
|
|
495
|
-
guidelines.commitMessageFormat = commitMatch[1];
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
// Detect test framework
|
|
499
|
-
if (lowerContent.includes('jest'))
|
|
500
|
-
guidelines.testFramework = 'Jest';
|
|
501
|
-
else if (lowerContent.includes('rspec'))
|
|
502
|
-
guidelines.testFramework = 'RSpec';
|
|
503
|
-
else if (lowerContent.includes('pytest'))
|
|
504
|
-
guidelines.testFramework = 'pytest';
|
|
505
|
-
else if (lowerContent.includes('mocha'))
|
|
506
|
-
guidelines.testFramework = 'Mocha';
|
|
507
|
-
// Detect linter
|
|
508
|
-
if (lowerContent.includes('eslint'))
|
|
509
|
-
guidelines.linter = 'ESLint';
|
|
510
|
-
else if (lowerContent.includes('rubocop'))
|
|
511
|
-
guidelines.linter = 'RuboCop';
|
|
512
|
-
else if (lowerContent.includes('prettier'))
|
|
513
|
-
guidelines.formatter = 'Prettier';
|
|
514
|
-
// Detect CLA requirement
|
|
515
|
-
if (lowerContent.includes('cla') || lowerContent.includes('contributor license agreement')) {
|
|
516
|
-
guidelines.claRequired = true;
|
|
517
|
-
}
|
|
518
|
-
return guidelines;
|
|
519
|
-
}
|
|
520
|
-
analyzeRequirements(body) {
|
|
521
|
-
if (!body || body.length < 50)
|
|
522
|
-
return false;
|
|
523
|
-
// Check for clear structure
|
|
524
|
-
const hasSteps = /\d\.|[-*]\s/.test(body);
|
|
525
|
-
const hasCodeBlock = /```/.test(body);
|
|
526
|
-
const hasExpectedBehavior = /expect|should|must|want/i.test(body);
|
|
527
|
-
// Must have at least two indicators of clarity
|
|
528
|
-
const indicators = [hasSteps, hasCodeBlock, hasExpectedBehavior, body.length > 200];
|
|
529
|
-
return indicators.filter(Boolean).length >= 2;
|
|
530
|
-
}
|
|
531
267
|
/**
|
|
532
268
|
* Get the repo score from state, or return null if not evaluated
|
|
533
269
|
*/
|
|
@@ -4,20 +4,25 @@
|
|
|
4
4
|
* Score methods still write to state.
|
|
5
5
|
*
|
|
6
6
|
* Decomposed into focused modules (#263):
|
|
7
|
-
* - ci-analysis.ts: CI check classification and analysis
|
|
7
|
+
* - ci-analysis.ts: CI status fetching, check classification and analysis
|
|
8
8
|
* - review-analysis.ts: Review decision and comment detection
|
|
9
9
|
* - checklist-analysis.ts: PR body checklist analysis
|
|
10
10
|
* - maintainer-analysis.ts: Maintainer action hint extraction
|
|
11
11
|
* - display-utils.ts: Display label computation
|
|
12
|
-
* - github-stats.ts: Merged/closed PR counts and star
|
|
12
|
+
* - github-stats.ts: Merged/closed PR counts and star-based filtering
|
|
13
13
|
* - status-determination.ts: PR status classification logic
|
|
14
14
|
*/
|
|
15
15
|
import { FetchedPR, DailyDigest, ClosedPR, MergedPR, StarFilter } from './types.js';
|
|
16
16
|
import { type PRCountsResult } from './github-stats.js';
|
|
17
17
|
export { computeDisplayLabel } from './display-utils.js';
|
|
18
|
-
export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
|
|
18
|
+
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
19
19
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
20
20
|
export { determineStatus } from './status-determination.js';
|
|
21
|
+
/**
|
|
22
|
+
* Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
|
|
23
|
+
* Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
|
|
24
|
+
*/
|
|
25
|
+
export declare function hasMergeConflict(mergeable: boolean | null, mergeableState: string | null): boolean;
|
|
21
26
|
export interface PRCheckFailure {
|
|
22
27
|
prUrl: string;
|
|
23
28
|
error: string;
|
|
@@ -44,16 +49,6 @@ export declare class PRMonitor {
|
|
|
44
49
|
* Centralizes PR construction and display label computation (#79).
|
|
45
50
|
*/
|
|
46
51
|
private buildFetchedPR;
|
|
47
|
-
/**
|
|
48
|
-
* Check if PR has merge conflict
|
|
49
|
-
*/
|
|
50
|
-
private hasMergeConflict;
|
|
51
|
-
/**
|
|
52
|
-
* Get CI status from combined status API and check runs.
|
|
53
|
-
* Returns status and names of failing checks for diagnostics.
|
|
54
|
-
* Delegates analysis to ci-analysis module.
|
|
55
|
-
*/
|
|
56
|
-
private getCIStatus;
|
|
57
52
|
/**
|
|
58
53
|
* Fetch merged PR counts and latest merge dates per repository for the configured user.
|
|
59
54
|
* Delegates to github-stats module.
|
|
@@ -68,10 +63,13 @@ export declare class PRMonitor {
|
|
|
68
63
|
*/
|
|
69
64
|
fetchUserClosedPRCounts(starFilter?: StarFilter): Promise<PRCountsResult<number>>;
|
|
70
65
|
/**
|
|
71
|
-
* Fetch
|
|
72
|
-
*
|
|
66
|
+
* Fetch metadata (star count and primary language) for a list of repositories.
|
|
67
|
+
* Both fields come from the same `repos.get()` call — zero additional API cost.
|
|
73
68
|
*/
|
|
74
|
-
|
|
69
|
+
fetchRepoMetadata(repos: string[]): Promise<Map<string, {
|
|
70
|
+
stars: number;
|
|
71
|
+
language: string | null;
|
|
72
|
+
}>>;
|
|
75
73
|
/**
|
|
76
74
|
* Fetch PRs closed without merge in the last N days.
|
|
77
75
|
* Delegates to github-stats module.
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Score methods still write to state.
|
|
5
5
|
*
|
|
6
6
|
* Decomposed into focused modules (#263):
|
|
7
|
-
* - ci-analysis.ts: CI check classification and analysis
|
|
7
|
+
* - ci-analysis.ts: CI status fetching, check classification and analysis
|
|
8
8
|
* - review-analysis.ts: Review decision and comment detection
|
|
9
9
|
* - checklist-analysis.ts: PR body checklist analysis
|
|
10
10
|
* - maintainer-analysis.ts: Maintainer action hint extraction
|
|
11
11
|
* - display-utils.ts: Display label computation
|
|
12
|
-
* - github-stats.ts: Merged/closed PR counts and star
|
|
12
|
+
* - github-stats.ts: Merged/closed PR counts and star-based filtering
|
|
13
13
|
* - status-determination.ts: PR status classification logic
|
|
14
14
|
*/
|
|
15
15
|
import { getOctokit } from './github.js';
|
|
@@ -21,8 +21,7 @@ import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode }
|
|
|
21
21
|
import { paginateAll } from './pagination.js';
|
|
22
22
|
import { debug, warn, timed } from './logger.js';
|
|
23
23
|
import { getHttpCache, cachedRequest } from './http-cache.js';
|
|
24
|
-
|
|
25
|
-
import { classifyFailingChecks, analyzeCheckRuns, analyzeCombinedStatus, mergeStatuses } from './ci-analysis.js';
|
|
24
|
+
import { classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
26
25
|
import { determineReviewDecision, getLatestChangesRequestedDate, checkUnrespondedComments, } from './review-analysis.js';
|
|
27
26
|
import { analyzeChecklist } from './checklist-analysis.js';
|
|
28
27
|
import { extractMaintainerActionHints } from './maintainer-analysis.js';
|
|
@@ -30,9 +29,16 @@ import { computeDisplayLabel } from './display-utils.js';
|
|
|
30
29
|
import { fetchUserMergedPRCounts as fetchUserMergedPRCountsImpl, fetchUserClosedPRCounts as fetchUserClosedPRCountsImpl, fetchRecentlyClosedPRs as fetchRecentlyClosedPRsImpl, fetchRecentlyMergedPRs as fetchRecentlyMergedPRsImpl, } from './github-stats.js';
|
|
31
30
|
// Re-export so existing consumers can still import from pr-monitor
|
|
32
31
|
export { computeDisplayLabel } from './display-utils.js';
|
|
33
|
-
export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
|
|
32
|
+
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
34
33
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
35
34
|
export { determineStatus } from './status-determination.js';
|
|
35
|
+
/**
|
|
36
|
+
* Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
|
|
37
|
+
* Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
|
|
38
|
+
*/
|
|
39
|
+
export function hasMergeConflict(mergeable, mergeableState) {
|
|
40
|
+
return mergeable === false || mergeableState === 'dirty';
|
|
41
|
+
}
|
|
36
42
|
const MODULE = 'pr-monitor';
|
|
37
43
|
const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
|
|
38
44
|
export class PRMonitor {
|
|
@@ -167,14 +173,14 @@ export class PRMonitor {
|
|
|
167
173
|
// Determine review decision (delegated to review-analysis module)
|
|
168
174
|
const reviewDecision = determineReviewDecision(reviews);
|
|
169
175
|
// Check for merge conflict
|
|
170
|
-
const
|
|
176
|
+
const mergeConflict = hasMergeConflict(ghPR.mergeable, ghPR.mergeable_state);
|
|
171
177
|
// Check if there's an unresponded maintainer comment (delegated to review-analysis module)
|
|
172
178
|
const { hasUnrespondedComment, lastMaintainerComment } = checkUnrespondedComments(comments, reviews, reviewComments, config.githubUsername);
|
|
173
179
|
// Fetch CI status and (conditionally) latest commit date in parallel
|
|
174
180
|
// We need the commit date when hasUnrespondedComment is true (to distinguish
|
|
175
181
|
// "needs_response" from "waiting_on_maintainer") OR when reviewDecision is "changes_requested"
|
|
176
182
|
// (to detect needs_changes: review requested changes but no new commits pushed)
|
|
177
|
-
const ciPromise = this.
|
|
183
|
+
const ciPromise = getCIStatus(this.octokit, owner, repo, ghPR.head.sha);
|
|
178
184
|
const needCommitDate = hasUnrespondedComment || reviewDecision === 'changes_requested';
|
|
179
185
|
const commitInfoPromise = needCommitDate
|
|
180
186
|
? this.octokit.repos
|
|
@@ -220,9 +226,9 @@ export class PRMonitor {
|
|
|
220
226
|
const classifiedChecks = classifyFailingChecks(failingCheckNames, failingCheckConclusions);
|
|
221
227
|
// Determine status
|
|
222
228
|
const hasActionableCIFailure = ciStatus === 'failing' && classifiedChecks.some((c) => c.category === 'actionable');
|
|
223
|
-
const { status, actionReason, waitReason, stalenessTier } = determineStatus({
|
|
229
|
+
const { status, actionReason, waitReason, stalenessTier, actionReasons } = determineStatus({
|
|
224
230
|
ciStatus,
|
|
225
|
-
hasMergeConflict,
|
|
231
|
+
hasMergeConflict: mergeConflict,
|
|
226
232
|
hasUnrespondedComment,
|
|
227
233
|
hasIncompleteChecklist,
|
|
228
234
|
reviewDecision,
|
|
@@ -246,13 +252,14 @@ export class PRMonitor {
|
|
|
246
252
|
actionReason,
|
|
247
253
|
waitReason,
|
|
248
254
|
stalenessTier,
|
|
255
|
+
actionReasons,
|
|
249
256
|
createdAt: ghPR.created_at,
|
|
250
257
|
updatedAt: ghPR.updated_at,
|
|
251
258
|
daysSinceActivity,
|
|
252
259
|
ciStatus,
|
|
253
260
|
failingCheckNames,
|
|
254
261
|
classifiedChecks,
|
|
255
|
-
hasMergeConflict,
|
|
262
|
+
hasMergeConflict: mergeConflict,
|
|
256
263
|
reviewDecision,
|
|
257
264
|
hasUnrespondedComment,
|
|
258
265
|
lastMaintainerComment,
|
|
@@ -278,78 +285,6 @@ export class PRMonitor {
|
|
|
278
285
|
pr.displayDescription = displayDescription;
|
|
279
286
|
return pr;
|
|
280
287
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Check if PR has merge conflict
|
|
283
|
-
*/
|
|
284
|
-
hasMergeConflict(mergeable, mergeableState) {
|
|
285
|
-
return mergeable === false || mergeableState === 'dirty';
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Get CI status from combined status API and check runs.
|
|
289
|
-
* Returns status and names of failing checks for diagnostics.
|
|
290
|
-
* Delegates analysis to ci-analysis module.
|
|
291
|
-
*/
|
|
292
|
-
async getCIStatus(owner, repo, sha) {
|
|
293
|
-
if (!sha)
|
|
294
|
-
return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
295
|
-
try {
|
|
296
|
-
// Fetch both combined status and check runs in parallel
|
|
297
|
-
const [statusResponse, checksResponse] = await Promise.all([
|
|
298
|
-
this.octokit.repos.getCombinedStatusForRef({ owner, repo, ref: sha }),
|
|
299
|
-
// 404 is expected for repos without check runs configured; log other errors for debugging
|
|
300
|
-
this.octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
|
|
301
|
-
const status = getHttpStatusCode(err);
|
|
302
|
-
// Rate limit errors must propagate — matches listReviewComments pattern (#481)
|
|
303
|
-
if (status === 429)
|
|
304
|
-
throw err;
|
|
305
|
-
if (status === 403) {
|
|
306
|
-
const msg = errorMessage(err).toLowerCase();
|
|
307
|
-
if (msg.includes('rate limit') || msg.includes('abuse detection'))
|
|
308
|
-
throw err;
|
|
309
|
-
}
|
|
310
|
-
if (status === 404) {
|
|
311
|
-
debug('pr-monitor', `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
warn('pr-monitor', `Non-404 error fetching check runs for ${owner}/${repo}@${sha.slice(0, 7)}: ${status ?? err}`);
|
|
315
|
-
}
|
|
316
|
-
return null;
|
|
317
|
-
}),
|
|
318
|
-
]);
|
|
319
|
-
const combinedStatus = statusResponse.data;
|
|
320
|
-
const allCheckRuns = checksResponse?.data?.check_runs || [];
|
|
321
|
-
// Deduplicate check runs by name, keeping only the most recent run per unique name.
|
|
322
|
-
// GitHub returns all historical runs (including re-runs), so without deduplication
|
|
323
|
-
// a superseded failure will incorrectly flag the PR as failing even after a re-run passes.
|
|
324
|
-
const latestCheckRunsByName = new Map();
|
|
325
|
-
for (const check of allCheckRuns) {
|
|
326
|
-
const existing = latestCheckRunsByName.get(check.name);
|
|
327
|
-
if (!existing || new Date(check.started_at ?? 0) > new Date(existing.started_at ?? 0)) {
|
|
328
|
-
latestCheckRunsByName.set(check.name, check);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
const checkRuns = [...latestCheckRunsByName.values()];
|
|
332
|
-
// Delegate analysis to ci-analysis module
|
|
333
|
-
const checkRunAnalysis = analyzeCheckRuns(checkRuns);
|
|
334
|
-
const combinedAnalysis = analyzeCombinedStatus(combinedStatus);
|
|
335
|
-
return mergeStatuses(checkRunAnalysis, combinedAnalysis, checkRuns.length);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
const statusCode = getHttpStatusCode(error);
|
|
339
|
-
if (statusCode === 401 || statusCode === 403 || statusCode === 429) {
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
else if (statusCode === 404) {
|
|
343
|
-
// Repo might not have CI configured, this is normal
|
|
344
|
-
debug('pr-monitor', `CI check 404 for ${owner}/${repo} (no CI configured)`);
|
|
345
|
-
return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage(error)}`);
|
|
349
|
-
}
|
|
350
|
-
return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
288
|
/**
|
|
354
289
|
* Fetch merged PR counts and latest merge dates per repository for the configured user.
|
|
355
290
|
* Delegates to github-stats module.
|
|
@@ -367,13 +302,13 @@ export class PRMonitor {
|
|
|
367
302
|
return fetchUserClosedPRCountsImpl(this.octokit, config.githubUsername, starFilter);
|
|
368
303
|
}
|
|
369
304
|
/**
|
|
370
|
-
* Fetch
|
|
371
|
-
*
|
|
305
|
+
* Fetch metadata (star count and primary language) for a list of repositories.
|
|
306
|
+
* Both fields come from the same `repos.get()` call — zero additional API cost.
|
|
372
307
|
*/
|
|
373
|
-
async
|
|
308
|
+
async fetchRepoMetadata(repos) {
|
|
374
309
|
if (repos.length === 0)
|
|
375
310
|
return new Map();
|
|
376
|
-
debug(MODULE, `Fetching
|
|
311
|
+
debug(MODULE, `Fetching repo metadata for ${repos.length} repos...`);
|
|
377
312
|
const results = new Map();
|
|
378
313
|
const cache = getHttpCache();
|
|
379
314
|
// Deduplicate repos to avoid fetching the same repo twice
|
|
@@ -394,17 +329,18 @@ export class PRMonitor {
|
|
|
394
329
|
repo: name,
|
|
395
330
|
headers,
|
|
396
331
|
}));
|
|
397
|
-
|
|
332
|
+
const metadata = { stars: data.stargazers_count, language: data.language ?? null };
|
|
333
|
+
return { repo, metadata };
|
|
398
334
|
}));
|
|
399
335
|
let chunkFailures = 0;
|
|
400
336
|
for (let j = 0; j < settled.length; j++) {
|
|
401
337
|
const result = settled[j];
|
|
402
338
|
if (result.status === 'fulfilled') {
|
|
403
|
-
results.set(result.value.repo, result.value.
|
|
339
|
+
results.set(result.value.repo, result.value.metadata);
|
|
404
340
|
}
|
|
405
341
|
else {
|
|
406
342
|
chunkFailures++;
|
|
407
|
-
warn(MODULE, `Failed to fetch
|
|
343
|
+
warn(MODULE, `Failed to fetch metadata for ${chunk[j]}: ${errorMessage(result.reason)}`);
|
|
408
344
|
}
|
|
409
345
|
}
|
|
410
346
|
// If entire chunk failed, likely a systemic issue (rate limit, auth, outage) — abort remaining
|
|
@@ -416,7 +352,7 @@ export class PRMonitor {
|
|
|
416
352
|
break;
|
|
417
353
|
}
|
|
418
354
|
}
|
|
419
|
-
debug(MODULE, `Fetched
|
|
355
|
+
debug(MODULE, `Fetched repo metadata for ${results.size}/${repos.length} repos`);
|
|
420
356
|
return results;
|
|
421
357
|
}
|
|
422
358
|
/**
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Health — project health checks and contribution guidelines fetching.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from issue-vetting.ts (#621) to isolate repo-level checks
|
|
5
|
+
* from issue-level eligibility logic.
|
|
6
|
+
*/
|
|
7
|
+
import { Octokit } from '@octokit/rest';
|
|
8
|
+
import { type ContributionGuidelines, type ProjectHealth } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Check the health of a GitHub project: recent commits, CI status, star/fork counts.
|
|
11
|
+
* Results are cached for HEALTH_CACHE_TTL_MS (4 hours).
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkProjectHealth(octokit: Octokit, owner: string, repo: string): Promise<ProjectHealth>;
|
|
14
|
+
/**
|
|
15
|
+
* Fetch and parse CONTRIBUTING.md (or variants) from a GitHub repo.
|
|
16
|
+
* Probes multiple paths in parallel: CONTRIBUTING.md, .github/CONTRIBUTING.md,
|
|
17
|
+
* docs/CONTRIBUTING.md, contributing.md. Results are cached for CACHE_TTL_MS.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchContributionGuidelines(octokit: Octokit, owner: string, repo: string): Promise<ContributionGuidelines | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Parse the raw content of a CONTRIBUTING.md file to extract structured guidelines:
|
|
22
|
+
* branch naming, commit format, test framework, linter, formatter, CLA requirement.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseContributionGuidelines(content: string): ContributionGuidelines;
|