dep-brain 1.5.1 → 1.7.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 +16 -0
- package/README.md +38 -4
- package/action.yml +33 -0
- package/depbrain.config.json +2 -0
- package/depbrain.config.schema.json +2 -0
- package/dist/cli.js +68 -2
- package/dist/core/analyzer.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/reporters/pr-comment.d.ts +6 -0
- package/dist/reporters/pr-comment.js +46 -0
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +9 -0
- package/dist/utils/github.d.ts +25 -0
- package/dist/utils/github.js +112 -0
- package/dist/utils/notifications.d.ts +20 -0
- package/dist/utils/notifications.js +116 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.7.0
|
|
6
|
+
|
|
7
|
+
- Added idempotent GitHub PR comments with `--pr-comment`.
|
|
8
|
+
- Added `--comment-on` trigger support for `always`, `failure`, and `new-findings`.
|
|
9
|
+
- Added PR comment markdown renderer with top issues, policy reasons, upgrade priorities, and baseline delta counts.
|
|
10
|
+
- Added GitHub Action inputs for PR comment runs.
|
|
11
|
+
- Kept analysis output contract at `1.6` because no JSON result fields changed.
|
|
12
|
+
|
|
13
|
+
## 1.6.0
|
|
14
|
+
|
|
15
|
+
- Added Slack and Discord webhook notification summaries.
|
|
16
|
+
- Added `--notify` and `--notify-on` CLI controls.
|
|
17
|
+
- Added notification config with `enabled`, `on`, `slackWebhookEnv`, and `discordWebhookEnv`.
|
|
18
|
+
- Added GitHub Action inputs for notification runs.
|
|
19
|
+
- Kept analysis output contract at `1.6` because no JSON result fields changed.
|
|
20
|
+
|
|
5
21
|
## 1.5.0
|
|
6
22
|
|
|
7
23
|
- Added structured upgrade advisor data under `outdated[].advice`.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
`dep-brain` is a CLI and library for explainable dependency intelligence in JavaScript and TypeScript projects.
|
|
8
8
|
|
|
9
|
-
Current release `1.
|
|
9
|
+
Current release `1.7.0` adds idempotent GitHub PR comments while keeping analysis output contract `1.6`.
|
|
10
10
|
|
|
11
11
|
## Vision
|
|
12
12
|
|
|
@@ -28,6 +28,8 @@ Current release `1.5.1` adds upgrade-advice output, stepped major-version guidan
|
|
|
28
28
|
- Generate a simple project health score
|
|
29
29
|
- Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
|
|
30
30
|
- Output upgrade-advice reports via `--advise`
|
|
31
|
+
- Send Slack and Discord webhook summaries for CI runs
|
|
32
|
+
- Create or update GitHub PR comments for pull request checks
|
|
31
33
|
- Gate CI with score and finding policies
|
|
32
34
|
- Compare new findings against a baseline report
|
|
33
35
|
|
|
@@ -37,7 +39,7 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
37
39
|
- Can I remove it safely?
|
|
38
40
|
- What should I fix first?
|
|
39
41
|
|
|
40
|
-
## 1.
|
|
42
|
+
## 1.7 Highlights
|
|
41
43
|
|
|
42
44
|
- Duplicate dependency detection with lockfile instance tracking
|
|
43
45
|
- Unused dependency detection with runtime vs dev-tool heuristics
|
|
@@ -55,6 +57,8 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
55
57
|
- SARIF output via `--sarif`
|
|
56
58
|
- Static HTML dashboard via `--dashboard`
|
|
57
59
|
- Upgrade advisor output via `--advise`
|
|
60
|
+
- Slack and Discord notification summaries via `--notify`
|
|
61
|
+
- GitHub PR comments via `--pr-comment`
|
|
58
62
|
- Ranked top issues via `--top`
|
|
59
63
|
- Baseline mode via `--baseline`
|
|
60
64
|
- Focused analysis via `--focus`
|
|
@@ -84,6 +88,10 @@ npx dep-brain analyze --json --out depbrain.json
|
|
|
84
88
|
npx dep-brain analyze --sarif --out depbrain.sarif
|
|
85
89
|
npx dep-brain analyze --dashboard
|
|
86
90
|
npx dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
|
|
91
|
+
npx dep-brain analyze --notify
|
|
92
|
+
npx dep-brain analyze --notify --notify-on always
|
|
93
|
+
npx dep-brain analyze --pr-comment
|
|
94
|
+
npx dep-brain analyze --pr-comment --comment-on new-findings
|
|
87
95
|
npx dep-brain analyze --focus duplicates
|
|
88
96
|
npx dep-brain analyze --ci
|
|
89
97
|
npx dep-brain analyze --baseline depbrain-baseline.json
|
|
@@ -169,7 +177,7 @@ Suggestions:
|
|
|
169
177
|
dep-brain analyze --json
|
|
170
178
|
```
|
|
171
179
|
|
|
172
|
-
Output includes `outputVersion` for schema stability. `dep-brain@1.
|
|
180
|
+
Output includes `outputVersion` for schema stability. `dep-brain@1.7.0` writes contract version `1.6`.
|
|
173
181
|
|
|
174
182
|
Validate against:
|
|
175
183
|
|
|
@@ -206,6 +214,24 @@ dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
|
|
|
206
214
|
|
|
207
215
|
Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
|
|
208
216
|
|
|
217
|
+
## Notifications
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
DEPBRAIN_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... dep-brain analyze --notify
|
|
221
|
+
DEPBRAIN_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... dep-brain analyze --notify --notify-on always
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`--notify` sends compact summaries to configured Slack and Discord webhook URLs. Default trigger is `failure`, so passing runs stay quiet unless `--notify-on always` is set.
|
|
225
|
+
|
|
226
|
+
## PR Comments
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
dep-brain analyze --ci --pr-comment
|
|
230
|
+
dep-brain analyze --ci --baseline depbrain-baseline.json --pr-comment --comment-on new-findings
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`--pr-comment` creates or updates one GitHub pull request comment using `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_EVENT_PATH`. The comment includes policy status, health score, top issues, upgrade priorities, and baseline delta counts when `--baseline` is used.
|
|
234
|
+
|
|
209
235
|
## Plugins
|
|
210
236
|
|
|
211
237
|
```json
|
|
@@ -276,6 +302,8 @@ Create a `depbrain.config.json` file in the project root:
|
|
|
276
302
|
"outputPath": "depbrain-dashboard.html"
|
|
277
303
|
},
|
|
278
304
|
"notifications": {
|
|
305
|
+
"enabled": false,
|
|
306
|
+
"on": "failure",
|
|
279
307
|
"slackWebhookEnv": "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
280
308
|
"discordWebhookEnv": "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
281
309
|
},
|
|
@@ -315,6 +343,8 @@ Supported sections:
|
|
|
315
343
|
- `risk.lowTrustWeightThreshold`
|
|
316
344
|
- `risk.mediumTrustWeightThreshold`
|
|
317
345
|
- `dashboard.outputPath`
|
|
346
|
+
- `notifications.enabled`
|
|
347
|
+
- `notifications.on`
|
|
318
348
|
- `notifications.slackWebhookEnv`
|
|
319
349
|
- `notifications.discordWebhookEnv`
|
|
320
350
|
- `scoring.duplicateWeight`
|
|
@@ -342,6 +372,8 @@ dep-brain analyze --fail-on-unused
|
|
|
342
372
|
dep-brain analyze --min-score 85 --fail-on-risks
|
|
343
373
|
dep-brain analyze --config depbrain.config.json
|
|
344
374
|
dep-brain analyze --baseline depbrain-baseline.json --fail-on-unused
|
|
375
|
+
dep-brain analyze --ci --notify
|
|
376
|
+
dep-brain analyze --ci --pr-comment
|
|
345
377
|
```
|
|
346
378
|
|
|
347
379
|
## Config Debugging
|
|
@@ -388,7 +420,7 @@ src/
|
|
|
388
420
|
|
|
389
421
|
## Product Direction
|
|
390
422
|
|
|
391
|
-
`dep-brain` is in `v1.
|
|
423
|
+
`dep-brain` is in `v1.7.0` production CLI stage, with current focus on actionable dependency decisions and PR workflow integration.
|
|
392
424
|
|
|
393
425
|
Recent releases added:
|
|
394
426
|
|
|
@@ -396,6 +428,8 @@ Recent releases added:
|
|
|
396
428
|
- dashboard and plugin support
|
|
397
429
|
- baseline, focus, and CI workflows
|
|
398
430
|
- structured upgrade advice with release-note links
|
|
431
|
+
- Slack and Discord notification summaries
|
|
432
|
+
- idempotent GitHub PR comments
|
|
399
433
|
|
|
400
434
|
Project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
|
|
401
435
|
|
package/action.yml
CHANGED
|
@@ -51,6 +51,26 @@ inputs:
|
|
|
51
51
|
description: Fail when risky dependencies are found.
|
|
52
52
|
required: false
|
|
53
53
|
default: "false"
|
|
54
|
+
notify:
|
|
55
|
+
description: Send Slack or Discord webhook summaries.
|
|
56
|
+
required: false
|
|
57
|
+
default: "false"
|
|
58
|
+
notify-on:
|
|
59
|
+
description: Notification trigger. Use always, failure, or never.
|
|
60
|
+
required: false
|
|
61
|
+
default: "failure"
|
|
62
|
+
pr-comment:
|
|
63
|
+
description: Create or update a GitHub PR comment.
|
|
64
|
+
required: false
|
|
65
|
+
default: "false"
|
|
66
|
+
comment-on:
|
|
67
|
+
description: PR comment trigger. Use always, failure, or new-findings.
|
|
68
|
+
required: false
|
|
69
|
+
default: "failure"
|
|
70
|
+
github-token:
|
|
71
|
+
description: GitHub token for PR comments.
|
|
72
|
+
required: false
|
|
73
|
+
default: ""
|
|
54
74
|
|
|
55
75
|
runs:
|
|
56
76
|
using: composite
|
|
@@ -70,6 +90,11 @@ runs:
|
|
|
70
90
|
INPUT_FAIL_ON_OUTDATED: ${{ inputs.fail-on-outdated }}
|
|
71
91
|
INPUT_FAIL_ON_DUPLICATES: ${{ inputs.fail-on-duplicates }}
|
|
72
92
|
INPUT_FAIL_ON_RISKS: ${{ inputs.fail-on-risks }}
|
|
93
|
+
INPUT_NOTIFY: ${{ inputs.notify }}
|
|
94
|
+
INPUT_NOTIFY_ON: ${{ inputs.notify-on }}
|
|
95
|
+
INPUT_PR_COMMENT: ${{ inputs.pr-comment }}
|
|
96
|
+
INPUT_COMMENT_ON: ${{ inputs.comment-on }}
|
|
97
|
+
GITHUB_TOKEN: ${{ inputs.github-token || github.token }}
|
|
73
98
|
run: |
|
|
74
99
|
set -euo pipefail
|
|
75
100
|
|
|
@@ -127,6 +152,14 @@ runs:
|
|
|
127
152
|
args+=("--fail-on-risks")
|
|
128
153
|
fi
|
|
129
154
|
|
|
155
|
+
if [ "$INPUT_NOTIFY" = "true" ]; then
|
|
156
|
+
args+=("--notify" "--notify-on" "$INPUT_NOTIFY_ON")
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if [ "$INPUT_PR_COMMENT" = "true" ]; then
|
|
160
|
+
args+=("--pr-comment" "--comment-on" "$INPUT_COMMENT_ON")
|
|
161
|
+
fi
|
|
162
|
+
|
|
130
163
|
node "$GITHUB_ACTION_PATH/dist/cli.js" "${args[@]}"
|
|
131
164
|
|
|
132
165
|
branding:
|
package/depbrain.config.json
CHANGED
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"type": "object",
|
|
69
69
|
"additionalProperties": false,
|
|
70
70
|
"properties": {
|
|
71
|
+
"enabled": { "type": "boolean" },
|
|
72
|
+
"on": { "type": "string", "enum": ["always", "failure", "never"] },
|
|
71
73
|
"slackWebhookEnv": { "type": "string" },
|
|
72
74
|
"discordWebhookEnv": { "type": "string" }
|
|
73
75
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,11 @@ import { analyzeProject } from "./core/analyzer.js";
|
|
|
3
3
|
import { renderConsoleReport } from "./reporters/console.js";
|
|
4
4
|
import { renderJsonReport } from "./reporters/json.js";
|
|
5
5
|
import { renderMarkdownReport } from "./reporters/markdown.js";
|
|
6
|
+
import { renderPrCommentReport } from "./reporters/pr-comment.js";
|
|
6
7
|
import { renderSarifReport } from "./reporters/sarif.js";
|
|
7
8
|
import { renderDashboardReport } from "./reporters/dashboard.js";
|
|
9
|
+
import { shouldPostPrComment, upsertGitHubPrComment } from "./utils/github.js";
|
|
10
|
+
import { sendConfiguredNotifications } from "./utils/notifications.js";
|
|
8
11
|
import { defaultConfig } from "./utils/config.js";
|
|
9
12
|
import { promises as fs } from "node:fs";
|
|
10
13
|
import path from "node:path";
|
|
@@ -160,6 +163,42 @@ async function main() {
|
|
|
160
163
|
if (flags.has("--dashboard")) {
|
|
161
164
|
await writeOutput(renderDashboardReport(result), optionValues.get("--dashboard-out") ?? result.config.dashboard.outputPath);
|
|
162
165
|
}
|
|
166
|
+
if (result.config.notifications.enabled) {
|
|
167
|
+
const notificationResults = await sendConfiguredNotifications(result);
|
|
168
|
+
for (const item of notificationResults) {
|
|
169
|
+
if (item.status === "sent") {
|
|
170
|
+
console.error(`Notification sent to ${item.channel}.`);
|
|
171
|
+
}
|
|
172
|
+
else if (item.status === "failed") {
|
|
173
|
+
console.error(`Notification failed for ${item.channel}: ${item.reason ?? "unknown error"}`);
|
|
174
|
+
}
|
|
175
|
+
else if (item.reason?.includes("webhook env")) {
|
|
176
|
+
console.error(`Notification skipped for ${item.channel}: ${item.reason}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (flags.has("--pr-comment")) {
|
|
181
|
+
const trigger = parsePrCommentTrigger(optionValues.get("--comment-on"));
|
|
182
|
+
const newFindingsCount = countFindings(result);
|
|
183
|
+
if (shouldPostPrComment({
|
|
184
|
+
trigger,
|
|
185
|
+
policyPassed: result.policy.passed,
|
|
186
|
+
newFindingsCount
|
|
187
|
+
})) {
|
|
188
|
+
const commentResult = await upsertGitHubPrComment({
|
|
189
|
+
body: renderPrCommentReport(result, { hasBaseline: Boolean(baseline) })
|
|
190
|
+
});
|
|
191
|
+
if (commentResult.status === "created" || commentResult.status === "updated") {
|
|
192
|
+
console.error(`PR comment ${commentResult.status}.`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.error(`PR comment skipped: ${commentResult.reason ?? "unknown reason"}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.error(`PR comment skipped: ${trigger} trigger did not match.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
163
202
|
if (!result.policy.passed) {
|
|
164
203
|
process.exitCode = 1;
|
|
165
204
|
}
|
|
@@ -183,6 +222,7 @@ void main();
|
|
|
183
222
|
function buildCliConfig(flags, optionValues) {
|
|
184
223
|
const minScore = optionValues.get("--min-score");
|
|
185
224
|
const policy = {};
|
|
225
|
+
const notifications = {};
|
|
186
226
|
if (minScore) {
|
|
187
227
|
policy.minScore = Number(minScore);
|
|
188
228
|
}
|
|
@@ -203,8 +243,18 @@ function buildCliConfig(flags, optionValues) {
|
|
|
203
243
|
policy.failOnDuplicates = true;
|
|
204
244
|
policy.failOnRisks = true;
|
|
205
245
|
}
|
|
246
|
+
if (flags.has("--notify")) {
|
|
247
|
+
notifications.enabled = true;
|
|
248
|
+
}
|
|
249
|
+
const notifyOn = optionValues.get("--notify-on");
|
|
250
|
+
if (notifyOn === "always" ||
|
|
251
|
+
notifyOn === "failure" ||
|
|
252
|
+
notifyOn === "never") {
|
|
253
|
+
notifications.on = notifyOn;
|
|
254
|
+
}
|
|
206
255
|
return {
|
|
207
|
-
policy
|
|
256
|
+
policy,
|
|
257
|
+
notifications
|
|
208
258
|
};
|
|
209
259
|
}
|
|
210
260
|
async function hasPackageJson(targetPath) {
|
|
@@ -220,7 +270,7 @@ function printHelp() {
|
|
|
220
270
|
console.log("Dependency Brain");
|
|
221
271
|
console.log("");
|
|
222
272
|
console.log("Usage:");
|
|
223
|
-
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
273
|
+
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--notify] [--pr-comment] [--comment-on kind] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
224
274
|
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--advise] [--dashboard] [--out path]");
|
|
225
275
|
console.log(" dep-brain config [path] [--config path]");
|
|
226
276
|
console.log(" dep-brain init [--out depbrain.config.json]");
|
|
@@ -235,6 +285,10 @@ function printHelp() {
|
|
|
235
285
|
console.log(" --advise Output upgrade advice for outdated dependencies");
|
|
236
286
|
console.log(" --dashboard Write an HTML dashboard");
|
|
237
287
|
console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
|
|
288
|
+
console.log(" --notify Send Slack or Discord webhook summaries");
|
|
289
|
+
console.log(" --notify-on <kind> Send notifications on always, failure, or never");
|
|
290
|
+
console.log(" --pr-comment Create or update a GitHub PR comment");
|
|
291
|
+
console.log(" --comment-on <kind> Post PR comment on always, failure, or new-findings");
|
|
238
292
|
console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
|
|
239
293
|
console.log(" --ci Apply low-noise CI defaults");
|
|
240
294
|
console.log(" --config <path> Path to depbrain.config.json");
|
|
@@ -368,3 +422,15 @@ function compareAdviceRisk(left, right) {
|
|
|
368
422
|
const rank = { high: 3, medium: 2, low: 1 };
|
|
369
423
|
return rank[left] - rank[right];
|
|
370
424
|
}
|
|
425
|
+
function parsePrCommentTrigger(value) {
|
|
426
|
+
if (value === "always" || value === "failure" || value === "new-findings") {
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
return "failure";
|
|
430
|
+
}
|
|
431
|
+
function countFindings(result) {
|
|
432
|
+
return (result.duplicates.length +
|
|
433
|
+
result.unused.length +
|
|
434
|
+
result.outdated.length +
|
|
435
|
+
result.risks.length);
|
|
436
|
+
}
|
package/dist/core/analyzer.js
CHANGED
|
@@ -154,6 +154,8 @@ function mergeConfig(base, overrides) {
|
|
|
154
154
|
outputPath: overrides.dashboard?.outputPath ?? base.dashboard.outputPath
|
|
155
155
|
},
|
|
156
156
|
notifications: {
|
|
157
|
+
enabled: overrides.notifications?.enabled ?? base.notifications.enabled,
|
|
158
|
+
on: overrides.notifications?.on ?? base.notifications.on,
|
|
157
159
|
slackWebhookEnv: overrides.notifications?.slackWebhookEnv ?? base.notifications.slackWebhookEnv,
|
|
158
160
|
discordWebhookEnv: overrides.notifications?.discordWebhookEnv ?? base.notifications.discordWebhookEnv
|
|
159
161
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,9 @@ export { PluginManager } from "./core/plugin-manager.js";
|
|
|
5
5
|
export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
|
|
6
6
|
export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
|
|
7
7
|
export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
|
|
8
|
+
export { renderNotificationMessage, sendConfiguredNotifications, shouldSendNotification } from "./utils/notifications.js";
|
|
9
|
+
export { shouldPostPrComment, upsertGitHubPrComment } from "./utils/github.js";
|
|
10
|
+
export { PR_COMMENT_MARKER, renderPrCommentReport } from "./reporters/pr-comment.js";
|
|
11
|
+
export type { NotificationChannel, NotificationResult, NotificationSendInput, NotificationSender } from "./utils/notifications.js";
|
|
12
|
+
export type { GitHubPrCommentInput, GitHubPrCommentResult, GitHubRequest, PrCommentTrigger } from "./utils/github.js";
|
|
8
13
|
export type { WorkspacePackage } from "./utils/workspaces.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
2
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
3
3
|
export { PluginManager } from "./core/plugin-manager.js";
|
|
4
|
+
export { renderNotificationMessage, sendConfiguredNotifications, shouldSendNotification } from "./utils/notifications.js";
|
|
5
|
+
export { shouldPostPrComment, upsertGitHubPrComment } from "./utils/github.js";
|
|
6
|
+
export { PR_COMMENT_MARKER, renderPrCommentReport } from "./reporters/pr-comment.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnalysisResult } from "../core/analyzer.js";
|
|
2
|
+
export declare const PR_COMMENT_MARKER = "<!-- dep-brain-report -->";
|
|
3
|
+
export interface PrCommentOptions {
|
|
4
|
+
hasBaseline?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function renderPrCommentReport(result: AnalysisResult, options?: PrCommentOptions): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const PR_COMMENT_MARKER = "<!-- dep-brain-report -->";
|
|
2
|
+
export function renderPrCommentReport(result, options = {}) {
|
|
3
|
+
const lines = [
|
|
4
|
+
PR_COMMENT_MARKER,
|
|
5
|
+
"## Dependency Brain",
|
|
6
|
+
"",
|
|
7
|
+
`**Policy:** ${result.policy.passed ? "PASS" : "FAIL"}`,
|
|
8
|
+
`**Project Health:** ${result.score}/100`,
|
|
9
|
+
`**Findings:** duplicates ${result.duplicates.length}, unused ${result.unused.length}, outdated ${result.outdated.length}, risks ${result.risks.length}`
|
|
10
|
+
];
|
|
11
|
+
if (options.hasBaseline) {
|
|
12
|
+
lines.push(`**New since baseline:** duplicates ${result.duplicates.length}, unused ${result.unused.length}, outdated ${result.outdated.length}, risks ${result.risks.length}`);
|
|
13
|
+
}
|
|
14
|
+
if (result.policy.reasons.length > 0) {
|
|
15
|
+
lines.push("");
|
|
16
|
+
lines.push("### Policy Reasons");
|
|
17
|
+
for (const reason of result.policy.reasons) {
|
|
18
|
+
lines.push(`- ${reason}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (result.topIssues.length > 0) {
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push("### Top Issues");
|
|
24
|
+
for (const item of result.topIssues.slice(0, 5)) {
|
|
25
|
+
lines.push(`- **${item.priority.toUpperCase()}** ${item.kind} \`${item.name}\`${item.package ? ` [${item.package}]` : ""}: ${item.summary}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const upgradePriorities = summarizeUpgradePriorities(result);
|
|
29
|
+
if (upgradePriorities) {
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push("### Upgrade Priorities");
|
|
32
|
+
lines.push(upgradePriorities);
|
|
33
|
+
}
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push("_Generated by dep-brain._");
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function summarizeUpgradePriorities(result) {
|
|
39
|
+
const counts = result.outdated.reduce((acc, item) => {
|
|
40
|
+
acc[item.advice.risk] += 1;
|
|
41
|
+
return acc;
|
|
42
|
+
}, { high: 0, medium: 0, low: 0 });
|
|
43
|
+
return [`high ${counts.high}`, `medium ${counts.medium}`, `low ${counts.low}`]
|
|
44
|
+
.filter((entry) => !entry.endsWith(" 0"))
|
|
45
|
+
.join(", ");
|
|
46
|
+
}
|
package/dist/utils/config.d.ts
CHANGED
package/dist/utils/config.js
CHANGED
|
@@ -38,6 +38,8 @@ export const defaultConfig = {
|
|
|
38
38
|
outputPath: "depbrain-dashboard.html"
|
|
39
39
|
},
|
|
40
40
|
notifications: {
|
|
41
|
+
enabled: false,
|
|
42
|
+
on: "failure",
|
|
41
43
|
slackWebhookEnv: "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
42
44
|
discordWebhookEnv: "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
43
45
|
},
|
|
@@ -100,6 +102,8 @@ function normalizeConfig(loaded) {
|
|
|
100
102
|
outputPath: normalizeString(loaded.dashboard?.outputPath, defaultConfig.dashboard.outputPath)
|
|
101
103
|
},
|
|
102
104
|
notifications: {
|
|
105
|
+
enabled: normalizeBoolean(loaded.notifications?.enabled, defaultConfig.notifications.enabled),
|
|
106
|
+
on: normalizeNotificationTrigger(loaded.notifications?.on, defaultConfig.notifications.on),
|
|
103
107
|
slackWebhookEnv: normalizeString(loaded.notifications?.slackWebhookEnv, defaultConfig.notifications.slackWebhookEnv),
|
|
104
108
|
discordWebhookEnv: normalizeString(loaded.notifications?.discordWebhookEnv, defaultConfig.notifications.discordWebhookEnv)
|
|
105
109
|
},
|
|
@@ -129,3 +133,8 @@ function normalizeNumber(value, fallback) {
|
|
|
129
133
|
function normalizeString(value, fallback) {
|
|
130
134
|
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
131
135
|
}
|
|
136
|
+
function normalizeNotificationTrigger(value, fallback) {
|
|
137
|
+
return value === "always" || value === "failure" || value === "never"
|
|
138
|
+
? value
|
|
139
|
+
: fallback;
|
|
140
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type PrCommentTrigger = "always" | "failure" | "new-findings";
|
|
2
|
+
export interface GitHubPrCommentInput {
|
|
3
|
+
body: string;
|
|
4
|
+
env?: Record<string, string | undefined>;
|
|
5
|
+
request?: GitHubRequest;
|
|
6
|
+
}
|
|
7
|
+
export interface GitHubPrCommentResult {
|
|
8
|
+
status: "created" | "updated" | "skipped";
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
export type GitHubRequest = (url: string, init: {
|
|
12
|
+
method: "GET" | "POST" | "PATCH";
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
body?: string;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
ok: boolean;
|
|
17
|
+
status: number;
|
|
18
|
+
json: () => Promise<unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function upsertGitHubPrComment(input: GitHubPrCommentInput): Promise<GitHubPrCommentResult>;
|
|
21
|
+
export declare function shouldPostPrComment(input: {
|
|
22
|
+
trigger: PrCommentTrigger;
|
|
23
|
+
policyPassed: boolean;
|
|
24
|
+
newFindingsCount: number;
|
|
25
|
+
}): boolean;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { PR_COMMENT_MARKER } from "../reporters/pr-comment.js";
|
|
3
|
+
export async function upsertGitHubPrComment(input) {
|
|
4
|
+
const env = input.env ?? process.env;
|
|
5
|
+
const token = env.GITHUB_TOKEN;
|
|
6
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
7
|
+
const eventPath = env.GITHUB_EVENT_PATH;
|
|
8
|
+
if (!token) {
|
|
9
|
+
return { status: "skipped", reason: "GITHUB_TOKEN is not set" };
|
|
10
|
+
}
|
|
11
|
+
if (!repository) {
|
|
12
|
+
return { status: "skipped", reason: "GITHUB_REPOSITORY is not set" };
|
|
13
|
+
}
|
|
14
|
+
if (!eventPath) {
|
|
15
|
+
return { status: "skipped", reason: "GITHUB_EVENT_PATH is not set" };
|
|
16
|
+
}
|
|
17
|
+
const pullNumber = await readPullNumber(eventPath);
|
|
18
|
+
if (!pullNumber) {
|
|
19
|
+
return { status: "skipped", reason: "pull request event not found" };
|
|
20
|
+
}
|
|
21
|
+
const request = input.request ?? defaultGitHubRequest;
|
|
22
|
+
const commentsUrl = `https://api.github.com/repos/${repository}/issues/${pullNumber}/comments`;
|
|
23
|
+
const headers = buildHeaders(token);
|
|
24
|
+
const existingResponse = await request(commentsUrl, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers
|
|
27
|
+
});
|
|
28
|
+
if (!existingResponse.ok) {
|
|
29
|
+
return {
|
|
30
|
+
status: "skipped",
|
|
31
|
+
reason: `failed to list comments: HTTP ${existingResponse.status}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const comments = normalizeComments(await existingResponse.json());
|
|
35
|
+
const existing = comments.find((comment) => typeof comment.body === "string" && comment.body.includes(PR_COMMENT_MARKER));
|
|
36
|
+
if (existing) {
|
|
37
|
+
const updateResponse = await request(`https://api.github.com/repos/${repository}/issues/comments/${existing.id}`, {
|
|
38
|
+
method: "PATCH",
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify({ body: input.body })
|
|
41
|
+
});
|
|
42
|
+
return updateResponse.ok
|
|
43
|
+
? { status: "updated" }
|
|
44
|
+
: {
|
|
45
|
+
status: "skipped",
|
|
46
|
+
reason: `failed to update comment: HTTP ${updateResponse.status}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const createResponse = await request(commentsUrl, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify({ body: input.body })
|
|
53
|
+
});
|
|
54
|
+
return createResponse.ok
|
|
55
|
+
? { status: "created" }
|
|
56
|
+
: {
|
|
57
|
+
status: "skipped",
|
|
58
|
+
reason: `failed to create comment: HTTP ${createResponse.status}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function shouldPostPrComment(input) {
|
|
62
|
+
if (input.trigger === "always") {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (input.trigger === "failure") {
|
|
66
|
+
return !input.policyPassed;
|
|
67
|
+
}
|
|
68
|
+
return input.newFindingsCount > 0;
|
|
69
|
+
}
|
|
70
|
+
async function readPullNumber(eventPath) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await fs.readFile(eventPath, "utf8");
|
|
73
|
+
const event = JSON.parse(raw);
|
|
74
|
+
const value = event.pull_request?.number ?? event.number;
|
|
75
|
+
return typeof value === "number" ? value : null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function defaultGitHubRequest(url, init) {
|
|
82
|
+
const response = await fetch(url, init);
|
|
83
|
+
return {
|
|
84
|
+
ok: response.ok,
|
|
85
|
+
status: response.status,
|
|
86
|
+
json: () => response.json()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function buildHeaders(token) {
|
|
90
|
+
return {
|
|
91
|
+
accept: "application/vnd.github+json",
|
|
92
|
+
authorization: `Bearer ${token}`,
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
"x-github-api-version": "2022-11-28"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function normalizeComments(value) {
|
|
98
|
+
if (!Array.isArray(value)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return value
|
|
102
|
+
.map((item) => {
|
|
103
|
+
if (!item || typeof item !== "object") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const comment = item;
|
|
107
|
+
return typeof comment.id === "number"
|
|
108
|
+
? { id: comment.id, body: comment.body }
|
|
109
|
+
: null;
|
|
110
|
+
})
|
|
111
|
+
.filter((item) => item !== null);
|
|
112
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnalysisResult } from "../core/analyzer.js";
|
|
2
|
+
import type { DepBrainConfig } from "./config.js";
|
|
3
|
+
export type NotificationChannel = "slack" | "discord";
|
|
4
|
+
export interface NotificationSendInput {
|
|
5
|
+
channel: NotificationChannel;
|
|
6
|
+
webhookUrl: string;
|
|
7
|
+
payload: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface NotificationResult {
|
|
10
|
+
channel: NotificationChannel;
|
|
11
|
+
status: "sent" | "skipped" | "failed";
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
export type NotificationSender = (input: NotificationSendInput) => Promise<void>;
|
|
15
|
+
export declare function sendConfiguredNotifications(result: AnalysisResult, options?: {
|
|
16
|
+
env?: Record<string, string | undefined>;
|
|
17
|
+
sender?: NotificationSender;
|
|
18
|
+
}): Promise<NotificationResult[]>;
|
|
19
|
+
export declare function shouldSendNotification(result: AnalysisResult, config: DepBrainConfig["notifications"]): boolean;
|
|
20
|
+
export declare function renderNotificationMessage(result: AnalysisResult): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export async function sendConfiguredNotifications(result, options = {}) {
|
|
2
|
+
const config = result.config.notifications;
|
|
3
|
+
if (!shouldSendNotification(result, config)) {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
channel: "slack",
|
|
7
|
+
status: "skipped",
|
|
8
|
+
reason: "notification trigger did not match analysis result"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
channel: "discord",
|
|
12
|
+
status: "skipped",
|
|
13
|
+
reason: "notification trigger did not match analysis result"
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
const env = options.env ?? process.env;
|
|
18
|
+
const sender = options.sender ?? postWebhook;
|
|
19
|
+
const message = renderNotificationMessage(result);
|
|
20
|
+
const targets = [
|
|
21
|
+
{
|
|
22
|
+
channel: "slack",
|
|
23
|
+
webhookUrl: env[config.slackWebhookEnv],
|
|
24
|
+
payload: { text: message }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
channel: "discord",
|
|
28
|
+
webhookUrl: env[config.discordWebhookEnv],
|
|
29
|
+
payload: { content: message }
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
const results = [];
|
|
33
|
+
for (const target of targets) {
|
|
34
|
+
if (!target.webhookUrl) {
|
|
35
|
+
results.push({
|
|
36
|
+
channel: target.channel,
|
|
37
|
+
status: "skipped",
|
|
38
|
+
reason: `${target.channel} webhook env is not set`
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await sender({
|
|
44
|
+
channel: target.channel,
|
|
45
|
+
webhookUrl: target.webhookUrl,
|
|
46
|
+
payload: target.payload
|
|
47
|
+
});
|
|
48
|
+
results.push({ channel: target.channel, status: "sent" });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
results.push({
|
|
52
|
+
channel: target.channel,
|
|
53
|
+
status: "failed",
|
|
54
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
export function shouldSendNotification(result, config) {
|
|
61
|
+
if (!config.enabled || config.on === "never") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return config.on === "always" || !result.policy.passed;
|
|
65
|
+
}
|
|
66
|
+
export function renderNotificationMessage(result) {
|
|
67
|
+
const status = result.policy.passed ? "PASS" : "FAIL";
|
|
68
|
+
const counts = [
|
|
69
|
+
`duplicates ${result.duplicates.length}`,
|
|
70
|
+
`unused ${result.unused.length}`,
|
|
71
|
+
`outdated ${result.outdated.length}`,
|
|
72
|
+
`risks ${result.risks.length}`
|
|
73
|
+
].join(", ");
|
|
74
|
+
const lines = [
|
|
75
|
+
`dep-brain ${status}: score ${result.score}/100`,
|
|
76
|
+
`path: ${result.rootDir}`,
|
|
77
|
+
`findings: ${counts}`
|
|
78
|
+
];
|
|
79
|
+
const upgradePriorities = buildUpgradePriorityText(result);
|
|
80
|
+
if (result.policy.reasons.length > 0) {
|
|
81
|
+
lines.push(`policy: ${result.policy.reasons.join("; ")}`);
|
|
82
|
+
}
|
|
83
|
+
if (result.topIssues.length > 0) {
|
|
84
|
+
lines.push("top issues:");
|
|
85
|
+
for (const issue of result.topIssues.slice(0, 3)) {
|
|
86
|
+
lines.push(`- ${formatTopIssue(issue)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (upgradePriorities) {
|
|
90
|
+
lines.push(`upgrades: ${upgradePriorities}`);
|
|
91
|
+
}
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
async function postWebhook(input) {
|
|
95
|
+
const response = await fetch(input.webhookUrl, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "content-type": "application/json" },
|
|
98
|
+
body: JSON.stringify(input.payload)
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`${input.channel} webhook failed with HTTP ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function formatTopIssue(issue) {
|
|
105
|
+
const owner = issue.package ? ` [${issue.package}]` : "";
|
|
106
|
+
return `[${issue.priority}] ${issue.kind} ${issue.name}${owner}: ${issue.summary}`;
|
|
107
|
+
}
|
|
108
|
+
function buildUpgradePriorityText(result) {
|
|
109
|
+
const counts = result.outdated.reduce((acc, item) => {
|
|
110
|
+
acc[item.advice.risk] += 1;
|
|
111
|
+
return acc;
|
|
112
|
+
}, { high: 0, medium: 0, low: 0 });
|
|
113
|
+
return [`high ${counts.high}`, `medium ${counts.medium}`, `low ${counts.low}`]
|
|
114
|
+
.filter((entry) => !entry.endsWith(" 0"))
|
|
115
|
+
.join(", ");
|
|
116
|
+
}
|