dep-brain 1.6.0 → 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 CHANGED
@@ -2,6 +2,14 @@
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
+
5
13
  ## 1.6.0
6
14
 
7
15
  - Added Slack and Discord webhook notification summaries.
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.6.0` adds Slack and Discord notification summaries while keeping analysis output contract `1.6`.
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
 
@@ -29,6 +29,7 @@ Current release `1.6.0` adds Slack and Discord notification summaries while keep
29
29
  - Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
30
30
  - Output upgrade-advice reports via `--advise`
31
31
  - Send Slack and Discord webhook summaries for CI runs
32
+ - Create or update GitHub PR comments for pull request checks
32
33
  - Gate CI with score and finding policies
33
34
  - Compare new findings against a baseline report
34
35
 
@@ -38,7 +39,7 @@ The long-term goal is not just to list problems, but to answer:
38
39
  - Can I remove it safely?
39
40
  - What should I fix first?
40
41
 
41
- ## 1.6 Highlights
42
+ ## 1.7 Highlights
42
43
 
43
44
  - Duplicate dependency detection with lockfile instance tracking
44
45
  - Unused dependency detection with runtime vs dev-tool heuristics
@@ -57,6 +58,7 @@ The long-term goal is not just to list problems, but to answer:
57
58
  - Static HTML dashboard via `--dashboard`
58
59
  - Upgrade advisor output via `--advise`
59
60
  - Slack and Discord notification summaries via `--notify`
61
+ - GitHub PR comments via `--pr-comment`
60
62
  - Ranked top issues via `--top`
61
63
  - Baseline mode via `--baseline`
62
64
  - Focused analysis via `--focus`
@@ -88,6 +90,8 @@ npx dep-brain analyze --dashboard
88
90
  npx dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
89
91
  npx dep-brain analyze --notify
90
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
91
95
  npx dep-brain analyze --focus duplicates
92
96
  npx dep-brain analyze --ci
93
97
  npx dep-brain analyze --baseline depbrain-baseline.json
@@ -173,7 +177,7 @@ Suggestions:
173
177
  dep-brain analyze --json
174
178
  ```
175
179
 
176
- Output includes `outputVersion` for schema stability. `dep-brain@1.6.0` writes contract version `1.6`.
180
+ Output includes `outputVersion` for schema stability. `dep-brain@1.7.0` writes contract version `1.6`.
177
181
 
178
182
  Validate against:
179
183
 
@@ -219,6 +223,15 @@ DEPBRAIN_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... dep-brain anal
219
223
 
220
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.
221
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
+
222
235
  ## Plugins
223
236
 
224
237
  ```json
@@ -360,6 +373,7 @@ dep-brain analyze --min-score 85 --fail-on-risks
360
373
  dep-brain analyze --config depbrain.config.json
361
374
  dep-brain analyze --baseline depbrain-baseline.json --fail-on-unused
362
375
  dep-brain analyze --ci --notify
376
+ dep-brain analyze --ci --pr-comment
363
377
  ```
364
378
 
365
379
  ## Config Debugging
@@ -406,7 +420,7 @@ src/
406
420
 
407
421
  ## Product Direction
408
422
 
409
- `dep-brain` is in `v1.6.0` production CLI stage, with current focus on actionable dependency decisions and CI workflow integration.
423
+ `dep-brain` is in `v1.7.0` production CLI stage, with current focus on actionable dependency decisions and PR workflow integration.
410
424
 
411
425
  Recent releases added:
412
426
 
@@ -415,6 +429,7 @@ Recent releases added:
415
429
  - baseline, focus, and CI workflows
416
430
  - structured upgrade advice with release-note links
417
431
  - Slack and Discord notification summaries
432
+ - idempotent GitHub PR comments
418
433
 
419
434
  Project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
420
435
 
package/action.yml CHANGED
@@ -59,6 +59,18 @@ inputs:
59
59
  description: Notification trigger. Use always, failure, or never.
60
60
  required: false
61
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: ""
62
74
 
63
75
  runs:
64
76
  using: composite
@@ -80,6 +92,9 @@ runs:
80
92
  INPUT_FAIL_ON_RISKS: ${{ inputs.fail-on-risks }}
81
93
  INPUT_NOTIFY: ${{ inputs.notify }}
82
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 }}
83
98
  run: |
84
99
  set -euo pipefail
85
100
 
@@ -141,6 +156,10 @@ runs:
141
156
  args+=("--notify" "--notify-on" "$INPUT_NOTIFY_ON")
142
157
  fi
143
158
 
159
+ if [ "$INPUT_PR_COMMENT" = "true" ]; then
160
+ args+=("--pr-comment" "--comment-on" "$INPUT_COMMENT_ON")
161
+ fi
162
+
144
163
  node "$GITHUB_ACTION_PATH/dist/cli.js" "${args[@]}"
145
164
 
146
165
  branding:
package/dist/cli.js CHANGED
@@ -3,8 +3,10 @@ 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";
8
10
  import { sendConfiguredNotifications } from "./utils/notifications.js";
9
11
  import { defaultConfig } from "./utils/config.js";
10
12
  import { promises as fs } from "node:fs";
@@ -175,6 +177,28 @@ async function main() {
175
177
  }
176
178
  }
177
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
+ }
178
202
  if (!result.policy.passed) {
179
203
  process.exitCode = 1;
180
204
  }
@@ -246,7 +270,7 @@ function printHelp() {
246
270
  console.log("Dependency Brain");
247
271
  console.log("");
248
272
  console.log("Usage:");
249
- console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--notify] [--notify-on kind] [--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]");
250
274
  console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--advise] [--dashboard] [--out path]");
251
275
  console.log(" dep-brain config [path] [--config path]");
252
276
  console.log(" dep-brain init [--out depbrain.config.json]");
@@ -263,6 +287,8 @@ function printHelp() {
263
287
  console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
264
288
  console.log(" --notify Send Slack or Discord webhook summaries");
265
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");
266
292
  console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
267
293
  console.log(" --ci Apply low-noise CI defaults");
268
294
  console.log(" --config <path> Path to depbrain.config.json");
@@ -396,3 +422,15 @@ function compareAdviceRisk(left, right) {
396
422
  const rank = { high: 3, medium: 2, low: 1 };
397
423
  return rank[left] - rank[right];
398
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/index.d.ts CHANGED
@@ -6,5 +6,8 @@ export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/pl
6
6
  export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
7
7
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
8
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";
9
11
  export type { NotificationChannel, NotificationResult, NotificationSendInput, NotificationSender } from "./utils/notifications.js";
12
+ export type { GitHubPrCommentInput, GitHubPrCommentResult, GitHubRequest, PrCommentTrigger } from "./utils/github.js";
10
13
  export type { WorkspacePackage } from "./utils/workspaces.js";
package/dist/index.js CHANGED
@@ -2,3 +2,5 @@ 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
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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "CLI and library for explainable dependency intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",