@dependabit/monitor 0.1.1
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 +9 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/checkers/github-repo.d.ts +17 -0
- package/dist/checkers/github-repo.d.ts.map +1 -0
- package/dist/checkers/github-repo.js +115 -0
- package/dist/checkers/github-repo.js.map +1 -0
- package/dist/checkers/index.d.ts +7 -0
- package/dist/checkers/index.d.ts.map +1 -0
- package/dist/checkers/index.js +7 -0
- package/dist/checkers/index.js.map +1 -0
- package/dist/checkers/openapi.d.ts +24 -0
- package/dist/checkers/openapi.d.ts.map +1 -0
- package/dist/checkers/openapi.js +221 -0
- package/dist/checkers/openapi.js.map +1 -0
- package/dist/checkers/url-content.d.ts +16 -0
- package/dist/checkers/url-content.d.ts.map +1 -0
- package/dist/checkers/url-content.js +66 -0
- package/dist/checkers/url-content.js.map +1 -0
- package/dist/comparator.d.ts +16 -0
- package/dist/comparator.d.ts.map +1 -0
- package/dist/comparator.js +53 -0
- package/dist/comparator.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/monitor.d.ts +43 -0
- package/dist/monitor.d.ts.map +1 -0
- package/dist/monitor.js +85 -0
- package/dist/monitor.js.map +1 -0
- package/dist/normalizer.d.ts +24 -0
- package/dist/normalizer.d.ts.map +1 -0
- package/dist/normalizer.js +97 -0
- package/dist/normalizer.js.map +1 -0
- package/dist/scheduler.d.ts +64 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +132 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/severity.d.ts +22 -0
- package/dist/severity.d.ts.map +1 -0
- package/dist/severity.js +87 -0
- package/dist/severity.js.map +1 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
- package/src/checkers/github-repo.ts +150 -0
- package/src/checkers/index.ts +7 -0
- package/src/checkers/openapi.ts +310 -0
- package/src/checkers/url-content.ts +78 -0
- package/src/comparator.ts +68 -0
- package/src/index.ts +20 -0
- package/src/monitor.ts +120 -0
- package/src/normalizer.ts +122 -0
- package/src/scheduler.ts +175 -0
- package/src/severity.ts +112 -0
- package/src/types.ts +40 -0
- package/test/checkers/github-repo.test.ts +124 -0
- package/test/checkers/openapi.test.ts +352 -0
- package/test/checkers/url-content.test.ts +99 -0
- package/test/comparator.test.ts +108 -0
- package/test/monitor.test.ts +177 -0
- package/test/normalizer.test.ts +66 -0
- package/test/scheduler.test.ts +674 -0
- package/test/severity.test.ts +122 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Content Checker
|
|
3
|
+
* Monitors documentation URLs for content changes using SHA256 hashing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Checker, DependencySnapshot, ChangeDetection, AccessConfig } from '../types.js';
|
|
7
|
+
import { normalizeHTML } from '../normalizer.js';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
export class URLContentChecker implements Checker {
|
|
11
|
+
/**
|
|
12
|
+
* Fetches and hashes URL content
|
|
13
|
+
*/
|
|
14
|
+
async fetch(config: AccessConfig): Promise<DependencySnapshot> {
|
|
15
|
+
const { url } = config;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
headers: {
|
|
20
|
+
'User-Agent': 'dependabit-monitor/1.0'
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const contentType = response.headers.get('content-type') || '';
|
|
29
|
+
const content = await response.text();
|
|
30
|
+
|
|
31
|
+
let normalizedContent: string;
|
|
32
|
+
|
|
33
|
+
// Apply HTML normalization if content is HTML
|
|
34
|
+
if (
|
|
35
|
+
contentType.includes('text/html') ||
|
|
36
|
+
content.trim().startsWith('<!DOCTYPE') ||
|
|
37
|
+
content.trim().startsWith('<html')
|
|
38
|
+
) {
|
|
39
|
+
normalizedContent = normalizeHTML(content);
|
|
40
|
+
} else {
|
|
41
|
+
// For non-HTML content (markdown, plain text, etc.), just normalize whitespace
|
|
42
|
+
normalizedContent = content.replace(/\s+/g, ' ').trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Generate SHA256 hash of normalized content
|
|
46
|
+
const stateHash = crypto.createHash('sha256').update(normalizedContent).digest('hex');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
stateHash,
|
|
50
|
+
fetchedAt: new Date(),
|
|
51
|
+
metadata: {
|
|
52
|
+
contentType,
|
|
53
|
+
contentLength: content.length,
|
|
54
|
+
normalizedLength: normalizedContent.length
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Failed to fetch URL content: ${(error as Error).message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compares two snapshots to detect content changes
|
|
64
|
+
*/
|
|
65
|
+
async compare(prev: DependencySnapshot, curr: DependencySnapshot): Promise<ChangeDetection> {
|
|
66
|
+
const changes: string[] = [];
|
|
67
|
+
|
|
68
|
+
// Content changed if hashes differ
|
|
69
|
+
if (prev.stateHash !== curr.stateHash) {
|
|
70
|
+
changes.push('content');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
hasChanged: changes.length > 0,
|
|
75
|
+
changes
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Comparator
|
|
3
|
+
* Generic comparison logic for dependency state snapshots
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DependencySnapshot, ChangeDetection } from './types.js';
|
|
7
|
+
|
|
8
|
+
export class StateComparator {
|
|
9
|
+
/**
|
|
10
|
+
* Compares two dependency snapshots to detect changes
|
|
11
|
+
*/
|
|
12
|
+
compare(oldState: DependencySnapshot, newState: DependencySnapshot): ChangeDetection {
|
|
13
|
+
const changes: string[] = [];
|
|
14
|
+
|
|
15
|
+
// Check state hash
|
|
16
|
+
if (oldState.stateHash !== newState.stateHash) {
|
|
17
|
+
changes.push('stateHash');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check version
|
|
21
|
+
if (oldState.version !== newState.version) {
|
|
22
|
+
changes.push('version');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check metadata changes (shallow comparison)
|
|
26
|
+
if (this.hasMetadataChanges(oldState.metadata, newState.metadata)) {
|
|
27
|
+
changes.push('metadata');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
hasChanged: changes.length > 0,
|
|
32
|
+
changes,
|
|
33
|
+
oldVersion: oldState.version,
|
|
34
|
+
newVersion: newState.version
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks if metadata has changed (shallow comparison)
|
|
40
|
+
*/
|
|
41
|
+
private hasMetadataChanges(
|
|
42
|
+
oldMeta?: Record<string, unknown>,
|
|
43
|
+
newMeta?: Record<string, unknown>
|
|
44
|
+
): boolean {
|
|
45
|
+
if (!oldMeta && !newMeta) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!oldMeta || !newMeta) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const oldKeys = Object.keys(oldMeta);
|
|
54
|
+
const newKeys = Object.keys(newMeta);
|
|
55
|
+
|
|
56
|
+
if (oldKeys.length !== newKeys.length) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const key of oldKeys) {
|
|
61
|
+
if (oldMeta[key] !== newMeta[key]) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dependabit/monitor - Dependency change detection and monitoring
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { Monitor } from './monitor.js';
|
|
6
|
+
export type { DependencyConfig, CheckResult } from './monitor.js';
|
|
7
|
+
|
|
8
|
+
export { GitHubRepoChecker } from './checkers/github-repo.js';
|
|
9
|
+
export { URLContentChecker } from './checkers/url-content.js';
|
|
10
|
+
export { OpenAPIChecker } from './checkers/openapi.js';
|
|
11
|
+
|
|
12
|
+
export { StateComparator } from './comparator.js';
|
|
13
|
+
export { SeverityClassifier } from './severity.js';
|
|
14
|
+
export type { Severity } from './severity.js';
|
|
15
|
+
|
|
16
|
+
export { normalizeHTML, normalizeURL } from './normalizer.js';
|
|
17
|
+
|
|
18
|
+
export { Scheduler } from './scheduler.js';
|
|
19
|
+
|
|
20
|
+
export type { Checker, DependencySnapshot, ChangeDetection, AccessConfig } from './types.js';
|
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor Orchestrator
|
|
3
|
+
* Coordinates dependency checking across multiple access methods
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GitHubRepoChecker } from './checkers/github-repo.js';
|
|
7
|
+
import { URLContentChecker } from './checkers/url-content.js';
|
|
8
|
+
import { OpenAPIChecker } from './checkers/openapi.js';
|
|
9
|
+
import { SeverityClassifier } from './severity.js';
|
|
10
|
+
import type { Checker, AccessConfig, DependencySnapshot, ChangeDetection } from './types.js';
|
|
11
|
+
|
|
12
|
+
export interface DependencyConfig extends AccessConfig {
|
|
13
|
+
id: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
currentStateHash: string;
|
|
17
|
+
currentVersion?: string;
|
|
18
|
+
lastChecked?: string;
|
|
19
|
+
monitoring?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
ignoreChanges?: boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CheckResult {
|
|
26
|
+
dependency: DependencyConfig;
|
|
27
|
+
hasChanged: boolean;
|
|
28
|
+
changes?: ChangeDetection;
|
|
29
|
+
severity?: 'breaking' | 'major' | 'minor' | undefined;
|
|
30
|
+
newSnapshot?: DependencySnapshot;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Monitor {
|
|
35
|
+
private checkers: Map<string, Checker>;
|
|
36
|
+
private classifier: SeverityClassifier;
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.checkers = new Map();
|
|
40
|
+
this.checkers.set('github-api', new GitHubRepoChecker());
|
|
41
|
+
this.checkers.set('http', new URLContentChecker());
|
|
42
|
+
this.checkers.set('openapi', new OpenAPIChecker());
|
|
43
|
+
|
|
44
|
+
this.classifier = new SeverityClassifier();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks a single dependency for changes
|
|
49
|
+
*/
|
|
50
|
+
async checkDependency(dependency: DependencyConfig): Promise<CheckResult> {
|
|
51
|
+
try {
|
|
52
|
+
// Get appropriate checker for access method
|
|
53
|
+
const checker = this.checkers.get(dependency.accessMethod);
|
|
54
|
+
if (!checker) {
|
|
55
|
+
throw new Error(`Unsupported access method: ${dependency.accessMethod}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fetch current state
|
|
59
|
+
const newSnapshot = await checker.fetch(dependency);
|
|
60
|
+
|
|
61
|
+
// Create old snapshot from stored data
|
|
62
|
+
const oldSnapshot: DependencySnapshot = {
|
|
63
|
+
stateHash: dependency.currentStateHash,
|
|
64
|
+
version: dependency.currentVersion,
|
|
65
|
+
fetchedAt: dependency.lastChecked ? new Date(dependency.lastChecked) : new Date()
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Compare states
|
|
69
|
+
const changes = await checker.compare(oldSnapshot, newSnapshot);
|
|
70
|
+
|
|
71
|
+
// Classify severity if changes detected
|
|
72
|
+
let severity: 'breaking' | 'major' | 'minor' | undefined;
|
|
73
|
+
if (changes.hasChanged) {
|
|
74
|
+
severity = this.classifier.classify(changes);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
dependency,
|
|
79
|
+
hasChanged: changes.hasChanged,
|
|
80
|
+
changes,
|
|
81
|
+
severity,
|
|
82
|
+
newSnapshot
|
|
83
|
+
};
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
dependency,
|
|
87
|
+
hasChanged: false,
|
|
88
|
+
error: (error as Error).message
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Checks multiple dependencies, respecting monitoring rules
|
|
95
|
+
*/
|
|
96
|
+
async checkAll(dependencies: DependencyConfig[]): Promise<CheckResult[]> {
|
|
97
|
+
// Filter out disabled dependencies
|
|
98
|
+
const enabledDeps = dependencies.filter((dep) => {
|
|
99
|
+
if (dep.monitoring?.enabled === false) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (dep.monitoring?.ignoreChanges === true) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Check all dependencies in parallel
|
|
109
|
+
const results = await Promise.all(enabledDeps.map((dep) => this.checkDependency(dep)));
|
|
110
|
+
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Registers a custom checker for an access method
|
|
116
|
+
*/
|
|
117
|
+
registerChecker(accessMethod: string, checker: Checker): void {
|
|
118
|
+
this.checkers.set(accessMethod, checker);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Normalizer for content-based change detection
|
|
3
|
+
*
|
|
4
|
+
* Implements 6-step normalization to reduce false positives:
|
|
5
|
+
* 1. Remove <script> and <style> tags
|
|
6
|
+
* 2. Strip HTML comments
|
|
7
|
+
* 3. Normalize whitespace
|
|
8
|
+
* 4. Remove timestamp patterns
|
|
9
|
+
* 5. Remove analytics/tracking parameters
|
|
10
|
+
* 6. Preserve meaningful content
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalizes HTML content for consistent comparison
|
|
15
|
+
* @param html Raw HTML content
|
|
16
|
+
* @returns Normalized HTML string
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeHTML(html: string): string {
|
|
19
|
+
if (!html || html.trim() === '') {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let normalized = html;
|
|
24
|
+
|
|
25
|
+
// Step 1: Remove script and style tags (with content)
|
|
26
|
+
// Use a simpler, more secure approach - remove everything between opening and closing tags
|
|
27
|
+
// This handles variations like </script >, </script\n>, etc.
|
|
28
|
+
let prevLength = 0;
|
|
29
|
+
while (normalized.length !== prevLength) {
|
|
30
|
+
prevLength = normalized.length;
|
|
31
|
+
// Match <script...> then any content, then </script with any whitespace/attributes before >
|
|
32
|
+
normalized = normalized.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*[^>]*>/gi, '');
|
|
33
|
+
}
|
|
34
|
+
prevLength = 0;
|
|
35
|
+
while (normalized.length !== prevLength) {
|
|
36
|
+
prevLength = normalized.length;
|
|
37
|
+
normalized = normalized.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*[^>]*>/gi, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Step 2: Strip HTML comments - iterative to handle nested comments
|
|
41
|
+
prevLength = 0;
|
|
42
|
+
while (normalized.length !== prevLength) {
|
|
43
|
+
prevLength = normalized.length;
|
|
44
|
+
normalized = normalized.replace(/<!--[\s\S]*?-->/g, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 3: Normalize whitespace (collapse multiple spaces/newlines)
|
|
48
|
+
normalized = normalized.replace(/\s+/g, ' ');
|
|
49
|
+
normalized = normalized.trim();
|
|
50
|
+
|
|
51
|
+
// Step 4: Remove common timestamp patterns
|
|
52
|
+
// Patterns like "Updated: 2024-01-01" or "Last modified: Jan 1, 2024"
|
|
53
|
+
normalized = normalized.replace(
|
|
54
|
+
/\b(Updated|Last\s+modified|Modified|Created|Published):\s*[^<\n]+/gi,
|
|
55
|
+
''
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Remove ISO timestamps
|
|
59
|
+
normalized = normalized.replace(
|
|
60
|
+
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?/g,
|
|
61
|
+
''
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Remove common date formats
|
|
65
|
+
normalized = normalized.replace(
|
|
66
|
+
/\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},?\s+\d{4}\b/gi,
|
|
67
|
+
''
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Step 5: Remove analytics/tracking parameters from URLs
|
|
71
|
+
// UTM parameters, tracking IDs, session IDs, etc.
|
|
72
|
+
normalized = normalized.replace(
|
|
73
|
+
/[?&](utm_[^&\s"']+|fbclid|gclid|msclkid|mc_[^&\s"']+|_ga|sessionid)[^&\s"']*/gi,
|
|
74
|
+
''
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Step 6: Final cleanup - preserve meaningful content structure
|
|
78
|
+
// Remove extra spaces that may have been introduced
|
|
79
|
+
normalized = normalized.replace(/\s+/g, ' ');
|
|
80
|
+
normalized = normalized.trim();
|
|
81
|
+
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Normalizes a URL by removing tracking parameters
|
|
87
|
+
* @param url URL string
|
|
88
|
+
* @returns Normalized URL
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeURL(url: string): string {
|
|
91
|
+
try {
|
|
92
|
+
const urlObj = new URL(url);
|
|
93
|
+
|
|
94
|
+
// Remove common tracking parameters
|
|
95
|
+
const trackingParams = [
|
|
96
|
+
'utm_source',
|
|
97
|
+
'utm_medium',
|
|
98
|
+
'utm_campaign',
|
|
99
|
+
'utm_term',
|
|
100
|
+
'utm_content',
|
|
101
|
+
'fbclid',
|
|
102
|
+
'gclid',
|
|
103
|
+
'msclkid',
|
|
104
|
+
'mc_cid',
|
|
105
|
+
'mc_eid',
|
|
106
|
+
'_ga',
|
|
107
|
+
'_gac',
|
|
108
|
+
'sessionid',
|
|
109
|
+
'sid',
|
|
110
|
+
'SSID'
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
trackingParams.forEach((param) => {
|
|
114
|
+
urlObj.searchParams.delete(param);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return urlObj.toString();
|
|
118
|
+
} catch {
|
|
119
|
+
// If URL parsing fails, return original
|
|
120
|
+
return url;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DependencyEntry,
|
|
3
|
+
type DependabitConfig,
|
|
4
|
+
getEffectiveMonitoringRules
|
|
5
|
+
} from '@dependabit/manifest';
|
|
6
|
+
|
|
7
|
+
export interface SchedulerOptions {
|
|
8
|
+
currentTime?: Date;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Scheduler for per-dependency monitoring
|
|
13
|
+
*
|
|
14
|
+
* Determines which dependencies should be checked based on:
|
|
15
|
+
* - Check frequency (hourly, daily, weekly, monthly)
|
|
16
|
+
* - Last checked timestamp
|
|
17
|
+
* - Enabled/disabled status
|
|
18
|
+
* - IgnoreChanges flag
|
|
19
|
+
* - Config overrides
|
|
20
|
+
*/
|
|
21
|
+
export class Scheduler {
|
|
22
|
+
/**
|
|
23
|
+
* Check if a dependency should be checked now
|
|
24
|
+
*
|
|
25
|
+
* @param dependency Dependency entry
|
|
26
|
+
* @param config Configuration
|
|
27
|
+
* @param currentTime Current time (defaults to now)
|
|
28
|
+
* @returns true if dependency should be checked
|
|
29
|
+
*/
|
|
30
|
+
shouldCheckDependency(
|
|
31
|
+
dependency: DependencyEntry,
|
|
32
|
+
config: DependabitConfig,
|
|
33
|
+
currentTime: Date = new Date()
|
|
34
|
+
): boolean {
|
|
35
|
+
// Check dependency's own monitoring rules first
|
|
36
|
+
if (dependency.monitoring) {
|
|
37
|
+
if (!dependency.monitoring.enabled) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (dependency.monitoring.ignoreChanges) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get effective monitoring rules (applies config overrides)
|
|
47
|
+
const rules = getEffectiveMonitoringRules(config, dependency.url);
|
|
48
|
+
|
|
49
|
+
// Check if monitoring is enabled at config level
|
|
50
|
+
if (!rules.enabled) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if changes should be ignored at config level
|
|
55
|
+
if (rules.ignoreChanges) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get last checked time
|
|
60
|
+
const lastChecked = new Date(dependency.lastChecked);
|
|
61
|
+
const timeSinceCheck = currentTime.getTime() - lastChecked.getTime();
|
|
62
|
+
|
|
63
|
+
// Determine frequency to use (dependency's own monitoring rules take precedence)
|
|
64
|
+
const checkFrequency = dependency.monitoring?.checkFrequency || rules.checkFrequency;
|
|
65
|
+
|
|
66
|
+
// Determine if enough time has passed based on frequency
|
|
67
|
+
const intervalMs = this.getIntervalMs(checkFrequency);
|
|
68
|
+
|
|
69
|
+
return timeSinceCheck >= intervalMs;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Filter dependencies that should be checked
|
|
74
|
+
*
|
|
75
|
+
* @param dependencies Array of dependencies
|
|
76
|
+
* @param config Configuration
|
|
77
|
+
* @param currentTime Current time (defaults to now)
|
|
78
|
+
* @returns Filtered array of dependencies to check
|
|
79
|
+
*/
|
|
80
|
+
filterDependenciesToCheck(
|
|
81
|
+
dependencies: DependencyEntry[],
|
|
82
|
+
config: DependabitConfig,
|
|
83
|
+
currentTime: Date = new Date()
|
|
84
|
+
): DependencyEntry[] {
|
|
85
|
+
return dependencies.filter((dep) => this.shouldCheckDependency(dep, config, currentTime));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get interval in milliseconds for a check frequency
|
|
90
|
+
*
|
|
91
|
+
* @param frequency Check frequency
|
|
92
|
+
* @returns Interval in milliseconds
|
|
93
|
+
*/
|
|
94
|
+
private getIntervalMs(frequency: 'hourly' | 'daily' | 'weekly' | 'monthly'): number {
|
|
95
|
+
switch (frequency) {
|
|
96
|
+
case 'hourly':
|
|
97
|
+
return 60 * 60 * 1000; // 1 hour
|
|
98
|
+
case 'daily':
|
|
99
|
+
return 24 * 60 * 60 * 1000; // 24 hours
|
|
100
|
+
case 'weekly':
|
|
101
|
+
return 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
102
|
+
case 'monthly':
|
|
103
|
+
return 30 * 24 * 60 * 60 * 1000; // 30 days (approximate)
|
|
104
|
+
default:
|
|
105
|
+
return 24 * 60 * 60 * 1000; // Default to daily
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get next check time for a dependency
|
|
111
|
+
*
|
|
112
|
+
* @param dependency Dependency entry
|
|
113
|
+
* @param config Configuration
|
|
114
|
+
* @returns Next check time
|
|
115
|
+
*/
|
|
116
|
+
getNextCheckTime(dependency: DependencyEntry, config: DependabitConfig): Date {
|
|
117
|
+
const rules = getEffectiveMonitoringRules(config, dependency.url);
|
|
118
|
+
const lastChecked = new Date(dependency.lastChecked);
|
|
119
|
+
|
|
120
|
+
// Determine frequency to use (dependency's own monitoring rules take precedence)
|
|
121
|
+
const checkFrequency = dependency.monitoring?.checkFrequency || rules.checkFrequency;
|
|
122
|
+
const intervalMs = this.getIntervalMs(checkFrequency);
|
|
123
|
+
|
|
124
|
+
return new Date(lastChecked.getTime() + intervalMs);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get schedule summary for all dependencies
|
|
129
|
+
*
|
|
130
|
+
* @param dependencies Array of dependencies
|
|
131
|
+
* @param config Configuration
|
|
132
|
+
* @returns Schedule summary grouped by frequency
|
|
133
|
+
*/
|
|
134
|
+
getScheduleSummary(
|
|
135
|
+
dependencies: DependencyEntry[],
|
|
136
|
+
config: DependabitConfig
|
|
137
|
+
): {
|
|
138
|
+
hourly: number;
|
|
139
|
+
daily: number;
|
|
140
|
+
weekly: number;
|
|
141
|
+
monthly: number;
|
|
142
|
+
disabled: number;
|
|
143
|
+
} {
|
|
144
|
+
const summary = {
|
|
145
|
+
hourly: 0,
|
|
146
|
+
daily: 0,
|
|
147
|
+
weekly: 0,
|
|
148
|
+
monthly: 0,
|
|
149
|
+
disabled: 0
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const dep of dependencies) {
|
|
153
|
+
// Check dependency's own monitoring rules first
|
|
154
|
+
if (dep.monitoring) {
|
|
155
|
+
if (!dep.monitoring.enabled || dep.monitoring.ignoreChanges) {
|
|
156
|
+
summary.disabled++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const rules = getEffectiveMonitoringRules(config, dep.url);
|
|
162
|
+
|
|
163
|
+
if (!rules.enabled || rules.ignoreChanges) {
|
|
164
|
+
summary.disabled++;
|
|
165
|
+
} else {
|
|
166
|
+
// Use dependency-level frequency if available, otherwise use rules
|
|
167
|
+
const checkFrequency: 'hourly' | 'daily' | 'weekly' | 'monthly' =
|
|
168
|
+
dep.monitoring?.checkFrequency ?? rules.checkFrequency;
|
|
169
|
+
summary[checkFrequency]++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return summary;
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/severity.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Severity Classifier
|
|
3
|
+
* Classifies dependency changes into breaking, major, or minor severity levels
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ChangeDetection } from './types.js';
|
|
7
|
+
|
|
8
|
+
export type Severity = 'breaking' | 'major' | 'minor';
|
|
9
|
+
|
|
10
|
+
export class SeverityClassifier {
|
|
11
|
+
/**
|
|
12
|
+
* Classifies the severity of a change based on version changes and change types
|
|
13
|
+
*/
|
|
14
|
+
classify(changes: ChangeDetection): Severity {
|
|
15
|
+
if (!changes.hasChanged) {
|
|
16
|
+
return 'minor';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check for version-based classification
|
|
20
|
+
if (changes.oldVersion && changes.newVersion) {
|
|
21
|
+
const severity = this.classifyVersionChange(changes.oldVersion, changes.newVersion);
|
|
22
|
+
if (severity) {
|
|
23
|
+
return severity;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check for OpenAPI/schema changes
|
|
28
|
+
if (changes.diff && typeof changes.diff === 'object') {
|
|
29
|
+
const diff = changes.diff as {
|
|
30
|
+
removedEndpoints?: string[];
|
|
31
|
+
schemaChanges?: string[];
|
|
32
|
+
addedEndpoints?: string[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Breaking: Removed endpoints or incompatible schema changes
|
|
36
|
+
if (diff.removedEndpoints && diff.removedEndpoints.length > 0) {
|
|
37
|
+
return 'breaking';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (diff.schemaChanges && diff.schemaChanges.length > 0) {
|
|
41
|
+
return 'breaking';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Major: New endpoints or features
|
|
45
|
+
if (diff.addedEndpoints && diff.addedEndpoints.length > 0) {
|
|
46
|
+
return 'major';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Default to minor for content changes
|
|
51
|
+
return 'minor';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Classifies severity based on semantic versioning
|
|
56
|
+
* @returns Severity level or undefined if version format not recognized
|
|
57
|
+
*/
|
|
58
|
+
private classifyVersionChange(oldVersion: string, newVersion: string): Severity | undefined {
|
|
59
|
+
// Parse semver-like versions
|
|
60
|
+
const oldParts = this.parseVersion(oldVersion);
|
|
61
|
+
const newParts = this.parseVersion(newVersion);
|
|
62
|
+
|
|
63
|
+
if (!oldParts || !newParts) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [oldMajor, oldMinor, oldPatch] = oldParts;
|
|
68
|
+
const [newMajor, newMinor, newPatch] = newParts;
|
|
69
|
+
|
|
70
|
+
// Breaking: Major version increase
|
|
71
|
+
if (newMajor > oldMajor) {
|
|
72
|
+
return 'breaking';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Major: Minor version increase
|
|
76
|
+
if (newMajor === oldMajor && newMinor > oldMinor) {
|
|
77
|
+
return 'major';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Minor: Patch version increase or same version
|
|
81
|
+
if (newMajor === oldMajor && newMinor === oldMinor && newPatch >= oldPatch) {
|
|
82
|
+
return 'minor';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Default for other cases
|
|
86
|
+
return 'major';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parses a version string into [major, minor, patch]
|
|
91
|
+
*/
|
|
92
|
+
private parseVersion(version: string): [number, number, number] | null {
|
|
93
|
+
if (!version) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Remove 'v' prefix if present
|
|
98
|
+
const cleaned = version.replace(/^v/, '');
|
|
99
|
+
|
|
100
|
+
// Match semver pattern
|
|
101
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
102
|
+
if (!match) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
parseInt(match[1] || '0', 10),
|
|
108
|
+
parseInt(match[2] || '0', 10),
|
|
109
|
+
parseInt(match[3] || '0', 10)
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
}
|