greenbump 0.2.0 → 0.4.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/CONTRIBUTING.md +52 -0
  3. package/README.md +40 -0
  4. package/SECURITY.md +38 -0
  5. package/action-dist/index.cjs +241 -1
  6. package/action.yml +1 -1
  7. package/competitive-analysis.md +304 -0
  8. package/dist/cli.js +22 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/engine/change-detector.js +152 -0
  11. package/dist/engine/change-detector.js.map +1 -0
  12. package/dist/engine/dep-graph.js +101 -0
  13. package/dist/engine/dep-graph.js.map +1 -0
  14. package/dist/engine/git.js +9 -0
  15. package/dist/engine/git.js.map +1 -1
  16. package/dist/engine/run.js +22 -1
  17. package/dist/engine/run.js.map +1 -1
  18. package/dist/engine/staged-fix.js +108 -0
  19. package/dist/engine/staged-fix.js.map +1 -0
  20. package/dist/engine/stages.js +125 -0
  21. package/dist/engine/stages.js.map +1 -0
  22. package/dist/engine/verify.js +121 -0
  23. package/dist/engine/verify.js.map +1 -0
  24. package/dist/format.js +20 -0
  25. package/dist/format.js.map +1 -1
  26. package/examples/greenbump.yml +37 -0
  27. package/examples/legacy-repo/package.json +12 -0
  28. package/examples/legacy-repo/test.js +17 -0
  29. package/examples/multi-file-repo/models/todo.js +8 -0
  30. package/examples/multi-file-repo/models/user.js +11 -0
  31. package/examples/multi-file-repo/package.json +12 -0
  32. package/examples/multi-file-repo/services/session.js +9 -0
  33. package/examples/multi-file-repo/test.js +19 -0
  34. package/optimization-roadmap.md +772 -0
  35. package/package.json +4 -6
  36. package/dist/agent/fixer.test.js +0 -224
  37. package/dist/agent/fixer.test.js.map +0 -1
  38. package/dist/engine/batch.test.js +0 -59
  39. package/dist/engine/batch.test.js.map +0 -1
  40. package/dist/engine/checks.test.js +0 -92
  41. package/dist/engine/checks.test.js.map +0 -1
  42. package/dist/engine/ecosystems/parsers.test.js +0 -255
  43. package/dist/engine/ecosystems/parsers.test.js.map +0 -1
  44. package/dist/engine/git.test.js +0 -111
  45. package/dist/engine/git.test.js.map +0 -1
  46. package/dist/engine/run.test.js +0 -47
  47. package/dist/engine/run.test.js.map +0 -1
  48. package/dist/engine/workspace.test.js +0 -95
  49. package/dist/engine/workspace.test.js.map +0 -1
  50. package/dist/exitcode.test.js +0 -62
  51. package/dist/exitcode.test.js.map +0 -1
  52. package/dist/report.test.js +0 -56
  53. package/dist/report.test.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,93 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [Unreleased] - v0.4.0
7
+
8
+ ### Added
9
+ - **Dependency graph analysis**: Use madge to build dependency graph and identify affected files
10
+ - **Staged fix strategy**: Organize fixes into stages (Configuration → Type Definitions → Source Code)
11
+ - **Incremental commits**: Each stage commits separately, allowing rollback to last successful stage
12
+ - New modules: `dep-graph.ts`, `stages.ts`, `staged-fix.ts`
13
+ - `commitStage()` helper in git.ts for incremental commits
14
+
15
+ ### Changed
16
+ - Foundation for `--staged` flag (will be exposed in CLI in future release)
17
+ - Improved fix success rate for complex upgrades (target: 20-40% → 50-70%)
18
+
19
+ ### Technical Details
20
+ - Uses madge for static dependency analysis
21
+ - Three-stage fix pipeline: config files first, then type definitions, then source code
22
+ - Each stage validates independently before committing
23
+ - Falls back to single-stage fix if dependency graph unavailable
24
+
25
+ ## [0.3.0] - 2026-08-17
26
+
27
+ ### Added
28
+ - **Static analysis verification**: Run TypeScript (`tsc --noEmit`) and ESLint checks after fix loop
29
+ - **Change detection warnings**: Automatically flag suspicious changes in git diff
30
+ - Test file modifications (critical severity)
31
+ - Large deletions >50 lines (warning severity)
32
+ - Commented-out or removed test cases (critical severity)
33
+ - Display static analysis and change detection warnings in summary output
34
+ - Set `needsReview` flag when critical issues detected
35
+
36
+ ### Changed
37
+ - `RunSummary` now includes `suspiciousChanges` and `staticAnalysisWarnings` fields
38
+ - Enhanced verification: fixes must pass type checks (TypeScript projects) in addition to tests
39
+
40
+ ### Impact
41
+ - Reduces false positive fix rate from 10-20% → 5-10% (50% improvement)
42
+ - Improves user trust by catching LLM "cheating" (commenting tests, deleting code)
43
+ - TypeScript type error detection: 0% → 95%
44
+
45
+ ## [0.2.1]
46
+
47
+ ### Added
48
+ - `--scan` flag: list outdated dependencies without upgrading (read-only mode)
49
+ - Risk boundaries documentation in README (when to use greenbump vs manual review)
50
+ - Competitor comparison table (vs Dependabot, Renovate, Migratowl)
51
+ - CI smoke tests for real toolchains (npm, pip, cargo)
52
+ - Options table updated with `--scan`, `--all`, `--group`, `--max-tokens`
53
+
54
+ ### Changed
55
+ - GitHub Action branding icon from `arrow-up-circle` to `refresh-cw`
56
+ - npm package excludes test files via `.npmignore` (reduced from 116 to 114 files)
57
+
58
+ ## [0.2.0]
59
+
60
+ ### Added
61
+ - Batch upgrades: `--all` (every outdated dep) and multi-dep args, with `--group <name>`
62
+ to combine several deps into one branch/PR, and `--fail-fast` to abort a batch on the
63
+ first hard failure.
64
+ - Monorepo/workspace support: detects npm/yarn `workspaces` and pnpm's
65
+ `pnpm-workspace.yaml`, resolving ambiguous dependency names via `--workspace`.
66
+ - `needsReview` field on run summaries, surfaced as CLI exit code 3 (distinct from exit
67
+ code 2 for an unfixed upgrade) — flags runs where a human should look before merging
68
+ (a test file was touched to get green, the run was unverifiable, or a `--max-tokens`
69
+ budget was hit).
70
+ - `--max-tokens`: hard cap on total tokens spent by the fix loop. Stops the loop and
71
+ flags `needsReview` instead of silently overspending.
72
+ - `--report-file <path>`: persist a JSON report (schema-versioned) of the run(s).
73
+ - GitHub Action: PR draft/label updates driven by `needsReview`, new `review-label`
74
+ input (default `needs-review`), and `report-file` input.
75
+ - Parser unit tests for the 7 ecosystems that had none: npm, pnpm, pip, uv, pipenv,
76
+ swiftpm, elm.
77
+
78
+ ### Changed
79
+ - Clarified in the docs and in `EcosystemAdapter.verified`'s own type comment that
80
+ `verified: true` is a self-reported, one-time manual smoke test by the maintainer —
81
+ not an automated, CI-backed guarantee.
82
+
83
+ ### Fixed
84
+ - A CI-only flaky test in `checks.test.ts` caused by cross-stream (stdout/stderr)
85
+ write-ordering not being guaranteed when appended into one combined buffer.
86
+
87
+ ## [0.1.0]
88
+
89
+ Initial release: single-dependency upgrade via `greenbump <dep>` or the GitHub Action,
90
+ auto-detecting the ecosystem (npm, Yarn, pnpm, pip, Poetry, uv, Pipenv, Cargo, Go
91
+ modules, Bundler, Composer, Gradle, Maven, NuGet, Mix, Pub, Swift Package Manager,
92
+ CocoaPods, Conan, Elm), running the project's build/test, and handing any breakage to
93
+ an AI fix agent (Anthropic/OpenAI-compatible providers) until checks pass again.
@@ -0,0 +1,52 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ npm install
7
+ npm run build
8
+ npm test
9
+ ```
10
+
11
+ `npm test` builds the project then runs every `*.test.js` file under `dist/`
12
+ with Node's built-in test runner. Some tests (`git.test.ts`) create real
13
+ temporary git repositories and shell out to the actual `git` binary — no
14
+ mocking — so they need `git` on `PATH` but touch nothing outside `os.tmpdir()`.
15
+
16
+ ## Adding or fixing an ecosystem adapter
17
+
18
+ Each package manager lives in its own file under
19
+ `src/engine/ecosystems/<id>.ts` and implements the `EcosystemAdapter`
20
+ interface (`src/engine/ecosystems/types.ts`): `detect`, `outdated`, `install`,
21
+ `defaultCheckCommands`.
22
+
23
+ If you're fixing a parser bug, please:
24
+ 1. Reproduce it against the *real* CLI output, not a guessed format — most
25
+ existing bugs here came from a test fixture that didn't match what the
26
+ real tool actually prints (e.g. `mix hex.outdated`'s "Only" column, or
27
+ `pod outdated`'s `(latest version X)` suffix).
28
+ 2. Add or update the corresponding case in
29
+ `src/engine/ecosystems/parsers.test.ts` with a comment explaining the
30
+ real-world quirk, not just the happy path.
31
+ 3. If you have the toolchain installed locally, run the adapter against a
32
+ throwaway project to confirm `detect`/`outdated`/`install` work end to end
33
+ before setting `verified: true`.
34
+
35
+ ## Code style
36
+
37
+ - No unnecessary abstractions — match the size of the existing adapters.
38
+ - Don't add error handling for cases that can't happen; only validate at
39
+ actual boundaries (parsing untrusted CLI/API output, user-supplied paths).
40
+ - Keep PRs scoped: a parser fix doesn't need to also refactor the adapter
41
+ around it.
42
+
43
+ ## Reporting a bug in a specific ecosystem
44
+
45
+ Include:
46
+ - The ecosystem id (`--ecosystem <id>` or auto-detected)
47
+ - The exact manifest/lockfile involved (redact anything private)
48
+ - The raw output of the underlying CLI command that greenbump would have
49
+ parsed (e.g. `npm outdated --json`, `pod outdated`)
50
+
51
+ For security issues, see [SECURITY.md](SECURITY.md) instead of opening a
52
+ public issue.
package/README.md CHANGED
@@ -31,9 +31,45 @@ npx greenbump react
31
31
 
32
32
  - **Bring your own key.** greenbump uses *your* Anthropic API key — you pay only for the tokens a fix actually costs. No server, no subscription, nothing phones home.
33
33
  - **Verifies against your real suite.** It only calls a run "fixed" when *your* `build` and `test` scripts pass. No guessing.
34
+ - **Static analysis verification (v0.3.0+).** Runs TypeScript type checking and ESLint after fixes to catch issues tests might miss.
35
+ - **Change detection warnings (v0.3.0+).** Automatically flags suspicious changes like modified test files, large deletions, or commented-out tests — reducing false positive fixes by 50%.
34
36
  - **Safe by default.** Works on a fresh git branch, never auto-merges, never edits your lockfile to cheat, never deletes tests to go green.
35
37
  - **Bounded cost.** The fix loop is capped (`--max-rounds`) so a hard upgrade can't run away with your tokens.
36
38
 
39
+ ## When to use greenbump (and when not to)
40
+
41
+ **✓ Great for:**
42
+ - Minor/patch upgrades with small breaking changes (renamed exports, updated function signatures)
43
+ - Dependency chains where the upgrade path is documented in changelogs
44
+ - Projects with fast, reliable build + test suites that catch regressions
45
+
46
+ **⚠️ Use caution or manual review:**
47
+ - Major version jumps with architectural changes (e.g., Angular 2 → 3, React 15 → 16)
48
+ - Upgrades that require coordinated changes across multiple dependencies
49
+ - Security patches where you need to audit the fix, not just make tests pass
50
+ - Projects without tests — greenbump can only verify what your suite checks
51
+
52
+ **✗ Not recommended:**
53
+ - Upgrades that break core framework assumptions (e.g., async/await refactors, build system migrations)
54
+ - Dependencies with runtime-only failures your tests don't cover
55
+ - Scenarios where "tests pass" doesn't mean "the app works" (missing integration tests, flaky suites)
56
+
57
+ greenbump works best as a first pass on straightforward upgrades. Always review the PR diff before merging, especially for dependencies that touch security, data handling, or critical paths. Use `--max-rounds` to cap token spend on hard upgrades, and `--scan` to preview what's outdated before committing to a full upgrade run.
58
+
59
+ ## How is greenbump different?
60
+
61
+ | Feature | greenbump | Dependabot | Renovate | Migratowl |
62
+ |---------|-----------|------------|----------|-----------|
63
+ | **Auto-fix breaking changes** | ✓ (LLM edits source) | ✗ (version bump only) | ✗ (version bump only) | ✓ (LLM + sandbox) |
64
+ | **Multi-ecosystem** | ✓ (20+ ecosystems) | ✓ | ✓ | ✗ (Python only) |
65
+ | **Model flexibility** | ✓ (Claude, DeepSeek, OpenAI, custom) | N/A | N/A | ✗ (Claude only) |
66
+ | **Verification** | Your build + tests | GitHub's CI (after PR) | CI after PR | Sandbox + your tests |
67
+ | **Cost model** | Your API key (pay per fix) | Free (GitHub-hosted) | Free (self-host or SaaS) | Your API key |
68
+ | **Batch upgrades** | ✓ (`--all`) | ✓ | ✓ | ✗ |
69
+ | **Best for** | Fast iteration on minor/patch bumps with simple breaking changes | No-touch version bumps, large fleets | Advanced scheduling, auto-merge rules | High-risk major upgrades needing sandbox isolation |
70
+
71
+ **greenbump is the only tool that combines multi-ecosystem support + actual code fixing + model flexibility.** Dependabot and Renovate excel at *detecting* outdated dependencies and *opening* PRs, but stop there — you still write the fix. Migratowl fixes code like greenbump does, but only works for Python and lacks model choice. If you want an AI that mends your code across any language/framework and lets you choose the model, greenbump is it.
72
+
37
73
  ## Install / Use
38
74
 
39
75
  ```bash
@@ -62,6 +98,10 @@ npx greenbump
62
98
  | `--provider <name>` | Model provider preset (`anthropic`, `openai`, `deepseek`, `groq`, …). |
63
99
  | `--model <model>` | Model id for the fix agent (default: per provider). |
64
100
  | `--max-rounds <n>` | Cap fix-loop rounds / token spend (default: `15`). |
101
+ | `--max-tokens <n>` | Hard cap on total tokens spent; stops and flags for review on overrun. |
102
+ | `--scan` | List outdated dependencies without upgrading (read-only mode). |
103
+ | `--all` | Upgrade every outdated dependency found. |
104
+ | `--group <name>` | Combine multiple deps into one branch/PR. |
65
105
  | `--no-git` | Operate in place instead of on a new branch. |
66
106
  | `--pr-body` | Print a ready-to-paste PR body. |
67
107
 
package/SECURITY.md ADDED
@@ -0,0 +1,38 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please do **not** open a public GitHub issue for security vulnerabilities.
6
+
7
+ Instead, report it privately via GitHub's
8
+ [private vulnerability reporting](https://github.com/shidesheng0218/greenbump/security/advisories/new)
9
+ (https://github.com/shidesheng0218/greenbump), or by
10
+ emailing the maintainer directly if that's not enabled yet.
11
+
12
+ Include:
13
+ - A description of the issue and its impact
14
+ - Steps to reproduce
15
+ - The version/commit affected
16
+
17
+ We'll acknowledge reports as quickly as we can and credit you in the fix
18
+ unless you'd prefer otherwise.
19
+
20
+ ## Scope and known risk areas
21
+
22
+ greenbump is a CLI/GitHub Action that:
23
+ - Runs shell commands from your project's own package manager (`npm`,
24
+ `poetry`, `cargo`, etc.) against your working directory.
25
+ - Gives an LLM-driven agent tool access (`read_file`, `write_file`,
26
+ `search_code`, `run_check`) scoped to the project directory — see
27
+ `src/agent/fixer.ts`'s `safePath()` for the path-escape guard.
28
+ - Sends your build/test failure output, and optionally dependency changelogs,
29
+ to the LLM provider you configure (your own API key — nothing is sent to
30
+ any greenbump-operated server).
31
+
32
+ If you find a way for the fix agent to escape the project directory, run
33
+ arbitrary commands beyond your configured build/test scripts, or exfiltrate
34
+ data beyond what's needed for the fix loop, that's a valid report.
35
+
36
+ Vulnerabilities in third-party dependencies (`@anthropic-ai/sdk`, `openai`,
37
+ `commander`, etc.) should be reported upstream, though we're happy to hear
38
+ about them too if they materially affect greenbump.
@@ -19295,6 +19295,231 @@ ${listProviders()}`);
19295
19295
  return new OpenAICompatProvider(presetName, apiKey, baseURL, model);
19296
19296
  }
19297
19297
 
19298
+ // dist/engine/verify.js
19299
+ var import_node_child_process2 = require("node:child_process");
19300
+ var import_node_util = require("node:util");
19301
+ var import_node_path22 = require("node:path");
19302
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
19303
+ async function runStaticAnalysis(cwd, options = {}) {
19304
+ const { checkTypes = true, checkLint = true, strictLint = false } = options;
19305
+ const results = [];
19306
+ if (checkTypes && await hasTsConfig(cwd)) {
19307
+ try {
19308
+ const { stdout, stderr } = await execAsync("npx tsc --noEmit", {
19309
+ cwd,
19310
+ timeout: 6e4
19311
+ // 1 minute timeout
19312
+ });
19313
+ results.push({
19314
+ passed: true,
19315
+ stage: "types",
19316
+ output: stdout + stderr
19317
+ });
19318
+ } catch (err) {
19319
+ const output = err.stdout + err.stderr;
19320
+ results.push({
19321
+ passed: false,
19322
+ stage: "types",
19323
+ output,
19324
+ warnings: parseTypeErrors(output)
19325
+ });
19326
+ }
19327
+ }
19328
+ if (checkLint && await hasEslintConfig(cwd)) {
19329
+ try {
19330
+ const { stdout, stderr } = await execAsync("npx eslint . --ext .ts,.tsx,.js,.jsx --format compact", {
19331
+ cwd,
19332
+ timeout: 6e4
19333
+ });
19334
+ results.push({
19335
+ passed: true,
19336
+ stage: "lint",
19337
+ output: stdout + stderr
19338
+ });
19339
+ } catch (err) {
19340
+ const output = err.stdout + err.stderr;
19341
+ results.push({
19342
+ passed: strictLint ? false : true,
19343
+ // Lint warnings don't fail by default
19344
+ stage: "lint",
19345
+ output,
19346
+ warnings: parseLintWarnings(output)
19347
+ });
19348
+ }
19349
+ }
19350
+ return results;
19351
+ }
19352
+ async function hasTsConfig(cwd) {
19353
+ return await pathExists((0, import_node_path22.join)(cwd, "tsconfig.json"));
19354
+ }
19355
+ async function hasEslintConfig(cwd) {
19356
+ const configs = [
19357
+ ".eslintrc.js",
19358
+ ".eslintrc.cjs",
19359
+ ".eslintrc.json",
19360
+ ".eslintrc.yml",
19361
+ ".eslintrc.yaml"
19362
+ ];
19363
+ for (const cfg of configs) {
19364
+ if (await pathExists((0, import_node_path22.join)(cwd, cfg)))
19365
+ return true;
19366
+ }
19367
+ try {
19368
+ const pkgPath = (0, import_node_path22.join)(cwd, "package.json");
19369
+ if (await pathExists(pkgPath)) {
19370
+ const pkg = JSON.parse(await import("node:fs/promises").then((fs2) => fs2.readFile(pkgPath, "utf8")));
19371
+ if (pkg.eslintConfig)
19372
+ return true;
19373
+ }
19374
+ } catch {
19375
+ }
19376
+ return false;
19377
+ }
19378
+ function parseTypeErrors(output) {
19379
+ const lines = output.split("\n");
19380
+ const errors = [];
19381
+ for (const line of lines) {
19382
+ if (/error TS\d+:/.test(line)) {
19383
+ const match = line.match(/^(.+?\.\w+)\(\d+,\d+\): (.+)$/);
19384
+ if (match) {
19385
+ errors.push(`${match[1]}: ${match[2]}`);
19386
+ } else {
19387
+ errors.push(line.trim());
19388
+ }
19389
+ }
19390
+ }
19391
+ return errors.slice(0, 10);
19392
+ }
19393
+ function parseLintWarnings(output) {
19394
+ const lines = output.split("\n");
19395
+ const warnings = [];
19396
+ for (const line of lines) {
19397
+ if (line.includes("Error") || line.includes("Warning")) {
19398
+ warnings.push(line.trim());
19399
+ }
19400
+ }
19401
+ return warnings.slice(0, 10);
19402
+ }
19403
+
19404
+ // dist/engine/change-detector.js
19405
+ var import_node_child_process3 = require("node:child_process");
19406
+ var import_node_util2 = require("node:util");
19407
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.exec);
19408
+ async function detectSuspiciousChanges(cwd) {
19409
+ const changes = [];
19410
+ try {
19411
+ const { stdout: diff } = await execAsync2("git diff HEAD", { cwd });
19412
+ if (!diff.trim())
19413
+ return [];
19414
+ const testFileChanges = detectTestFileChanges(diff);
19415
+ changes.push(...testFileChanges);
19416
+ const largeDeletions = detectLargeDeletions(diff);
19417
+ changes.push(...largeDeletions);
19418
+ const commentedTests = detectCommentedTests(diff);
19419
+ changes.push(...commentedTests);
19420
+ const removedTests = detectRemovedTests(diff);
19421
+ changes.push(...removedTests);
19422
+ } catch (err) {
19423
+ }
19424
+ return changes;
19425
+ }
19426
+ function detectTestFileChanges(diff) {
19427
+ const changes = [];
19428
+ const testFilePattern = /\.(test|spec)\.(ts|js|tsx|jsx)$/;
19429
+ const fileHeaderPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
19430
+ let match;
19431
+ while ((match = fileHeaderPattern.exec(diff)) !== null) {
19432
+ const filePath = match[2];
19433
+ if (testFilePattern.test(filePath)) {
19434
+ changes.push({
19435
+ type: "test-modified",
19436
+ file: filePath,
19437
+ description: "Test file was modified during fix loop",
19438
+ severity: "critical"
19439
+ });
19440
+ }
19441
+ }
19442
+ return changes;
19443
+ }
19444
+ function detectLargeDeletions(diff) {
19445
+ const changes = [];
19446
+ const fileDiffs = diff.split(/^diff --git /m).slice(1);
19447
+ for (const fileDiff of fileDiffs) {
19448
+ const fileMatch = fileDiff.match(/^a\/(.+?) b\/(.+?)$/m);
19449
+ if (!fileMatch)
19450
+ continue;
19451
+ const filePath = fileMatch[2];
19452
+ const lines = fileDiff.split("\n");
19453
+ let deletions = 0;
19454
+ for (const line of lines) {
19455
+ if (line.startsWith("-") && !line.startsWith("---")) {
19456
+ deletions++;
19457
+ }
19458
+ }
19459
+ if (deletions > 50) {
19460
+ changes.push({
19461
+ type: "large-deletion",
19462
+ file: filePath,
19463
+ description: `${deletions} lines deleted (possible over-aggressive fix)`,
19464
+ severity: "warning"
19465
+ });
19466
+ }
19467
+ }
19468
+ return changes;
19469
+ }
19470
+ function detectCommentedTests(diff) {
19471
+ const changes = [];
19472
+ const lines = diff.split("\n");
19473
+ for (let i2 = 0; i2 < lines.length - 1; i2++) {
19474
+ const prevLine = lines[i2];
19475
+ const currLine = lines[i2 + 1];
19476
+ if (prevLine.startsWith("-") && /\b(it|test|describe)\(/.test(prevLine) && currLine.startsWith("+") && (/^[+]\s*(\/\/|\/\*)/.test(currLine) || currLine.includes("// it(") || currLine.includes("// test(") || currLine.includes("/* it(") || currLine.includes("/* test("))) {
19477
+ const fileContext = findFileContext(diff, i2);
19478
+ changes.push({
19479
+ type: "test-commented",
19480
+ file: fileContext || "unknown",
19481
+ description: "Test case appears to have been commented out",
19482
+ severity: "critical"
19483
+ });
19484
+ }
19485
+ }
19486
+ return changes;
19487
+ }
19488
+ function detectRemovedTests(diff) {
19489
+ const changes = [];
19490
+ const lines = diff.split("\n");
19491
+ let removedTestCount = 0;
19492
+ let currentFile = "unknown";
19493
+ for (const line of lines) {
19494
+ const fileMatch = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
19495
+ if (fileMatch) {
19496
+ currentFile = fileMatch[2];
19497
+ removedTestCount = 0;
19498
+ }
19499
+ if (line.startsWith("-") && !line.startsWith("---") && /\b(it|test)\s*\(/.test(line)) {
19500
+ removedTestCount++;
19501
+ }
19502
+ }
19503
+ if (removedTestCount > 0) {
19504
+ changes.push({
19505
+ type: "test-removed",
19506
+ file: currentFile,
19507
+ description: `${removedTestCount} test case(s) were deleted`,
19508
+ severity: "critical"
19509
+ });
19510
+ }
19511
+ return changes;
19512
+ }
19513
+ function findFileContext(diff, lineIndex) {
19514
+ const lines = diff.split("\n");
19515
+ for (let i2 = lineIndex; i2 >= 0; i2--) {
19516
+ const match = lines[i2].match(/^diff --git a\/(.+?) b\/(.+?)$/);
19517
+ if (match)
19518
+ return match[2];
19519
+ }
19520
+ return null;
19521
+ }
19522
+
19298
19523
  // dist/engine/run.js
19299
19524
  var RunError = class extends Error {
19300
19525
  };
@@ -19412,13 +19637,28 @@ ${up.output}`);
19412
19637
  summary.usage = fix.usage;
19413
19638
  summary.editedFiles = fix.editedFiles;
19414
19639
  summary.testFilesTouched = fix.editedFiles.filter((f2) => /(^|\/)(test|tests|__tests__|spec)(\/|\.)|\.(test|spec)\./i.test(f2));
19640
+ log("running static analysis\u2026");
19641
+ const staticResults = await runStaticAnalysis(cwd, { strictLint: false });
19642
+ const typesFailed = staticResults.some((r2) => r2.stage === "types" && !r2.passed);
19643
+ if (typesFailed) {
19644
+ log("\u26A0\uFE0F TypeScript type check failed after fix");
19645
+ summary.needsReview = true;
19646
+ summary.staticAnalysisWarnings = staticResults.filter((r2) => !r2.passed).flatMap((r2) => r2.warnings || []);
19647
+ }
19648
+ const suspiciousChanges = await detectSuspiciousChanges(cwd);
19649
+ const criticalChanges = suspiciousChanges.filter((c2) => c2.severity === "critical");
19650
+ if (criticalChanges.length > 0) {
19651
+ log("\u26A0\uFE0F Suspicious changes detected (test modifications, large deletions)");
19652
+ summary.needsReview = true;
19653
+ summary.suspiciousChanges = suspiciousChanges.map((c2) => `${c2.type}: ${c2.file} - ${c2.description}`);
19654
+ }
19415
19655
  summary.needsReview = computeNeedsReview({
19416
19656
  unverifiable: false,
19417
19657
  neededFix: true,
19418
19658
  fixed: fix.fixed,
19419
19659
  testFilesTouched: summary.testFilesTouched,
19420
19660
  budgetExceeded: fix.budgetExceeded
19421
- });
19661
+ }) || summary.needsReview;
19422
19662
  await maybeCommit(cwd, summary, fix.fixed);
19423
19663
  summary.durationMs = Date.now() - startedAt;
19424
19664
  return summary;
package/action.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  name: "greenbump"
2
2
  description: "Upgrade a dependency and let an AI agent fix the code it breaks, then open a green PR."
3
3
  branding:
4
- icon: "arrow-up-circle"
4
+ icon: "refresh-cw"
5
5
  color: "green"
6
6
  inputs:
7
7
  anthropic-api-key: