gemxray 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a6d939f389ed77e696ee646dbde1ab0bb7a8ceedc557f842d28c18c98db4ce5
4
- data.tar.gz: 34b0b557679c2fa045833d3c2058e5b31277a01f06d60a0ebcc178a7b244b695
3
+ metadata.gz: 32f1f088a0926c5f77d7246c4312cf41d797e629a54e9962a01c182910bbd6fb
4
+ data.tar.gz: 34309d8c477b7044430f191cc8819f21325c014da311597a33f6a4220079b783
5
5
  SHA512:
6
- metadata.gz: e1acb06ca5efd7b530d81293f4560c02948301073abb51611a2bd34d6fa473329436bf07e354e0d22923a53b19183738375a1a82c85effedf181d4f2f6ba2ffe
7
- data.tar.gz: bb4421e1c58449dd6143afa8520a60bd38b4f5011aa5451b65f17737cffa8c7ecd8d10bc36be84cc12fb1993b489aea55a8600d4f045dd34d025acb972b0527e
6
+ metadata.gz: a15b1a9f0021545e3d03576c65be972a366214ae8bfe4b0bb55ea4de918dd4fabbbdfa73fc931a9c50e1e1bb143addfc6cb5c4156d5b914806610aeeeca4844f
7
+ data.tar.gz: b2375769bbd5abc10503d4f2619f0ca35022cd9b72339cf3a267881fabc077843e5ef47a120eb3ab651a79dd8aabc2407e49f71d54d8530134b12badb015664f
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.0 (2026-04-06)
6
+
7
+ - Add `--fail-on` option and `ci_fail_on` config field to control the minimum severity that causes `scan --ci` to exit with status 1. Previously, any finding triggered a failure; now only findings at or above the specified level do. The default is `warning`.
8
+
5
9
  ## 0.1.0 (2026-03-27)
6
10
 
7
11
  - Initial release.
data/README.md CHANGED
@@ -1,14 +1,36 @@
1
1
  # GemXray
2
2
 
3
- `gemxray` is a CLI that highlights gems you can likely remove from a Ruby project's `Gemfile`.
4
-
5
- It combines three analyzers:
6
-
7
- 1. `unused`: no `require`, constant reference, gemspec dependency, or Rails autoload signal was found.
8
- 2. `redundant`: another top-level gem already brings the gem in through `Gemfile.lock`.
9
- 3. `version-redundant`: the gem is already covered by your Ruby or Rails version.
10
-
11
- If you run `gemxray` without a command, it defaults to `scan`.
3
+ [![Gem Version](https://badge.fury.io/rb/gemxray.svg)](https://badge.fury.io/rb/gemxray)
4
+ [![CI](https://github.com/ydah/gemxray/actions/workflows/main.yml/badge.svg)](https://github.com/ydah/gemxray/actions/workflows/main.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A CLI that highlights gems you can likely remove from a Ruby project's `Gemfile`.
8
+
9
+ GemXray combines three analyzers to find removal candidates:
10
+
11
+ | Analyzer | What it detects |
12
+ | --- | --- |
13
+ | `unused` | No `require`, constant reference, gemspec dependency, or Rails autoload signal was found. |
14
+ | `redundant` | Another top-level gem already brings the gem in through `Gemfile.lock`. |
15
+ | `version` | The gem is already covered by your Ruby or Rails version (default/bundled gem). |
16
+
17
+ ## Table of Contents
18
+
19
+ - [Installation](#installation)
20
+ - [Quick Start](#quick-start)
21
+ - [Commands](#commands)
22
+ - [Shared analysis options](#shared-analysis-options)
23
+ - [`scan`](#scan)
24
+ - [`clean`](#clean)
25
+ - [`pr`](#pr)
26
+ - [`init`](#init)
27
+ - [`version`](#version)
28
+ - [`help`](#help)
29
+ - [Severity](#severity)
30
+ - [Configuration](#configuration)
31
+ - [Development](#development)
32
+ - [Contributing](#contributing)
33
+ - [License](#license)
12
34
 
13
35
  ## Installation
14
36
 
@@ -43,7 +65,7 @@ bundle exec gemxray scan
43
65
  Use structured output for CI or scripts:
44
66
 
45
67
  ```bash
46
- bundle exec gemxray scan --format json --ci
68
+ bundle exec gemxray scan --format json --ci --fail-on danger
47
69
  bundle exec gemxray scan --only unused,version --severity warning
48
70
  ```
49
71
 
@@ -70,27 +92,160 @@ bundle exec gemxray scan --gemfile path/to/Gemfile
70
92
 
71
93
  ## Commands
72
94
 
95
+ If you run `gemxray` without a command, it behaves as `gemxray scan`.
96
+
73
97
  | Command | Purpose | Useful options |
74
98
  | --- | --- | --- |
75
- | `scan` | Analyze the Gemfile and print findings. | `--format`, `--only`, `--severity`, `--ci`, `--gemfile`, `--config` |
99
+ | `scan` | Analyze the Gemfile and print findings. | `--format`, `--only`, `--severity`, `--ci`, `--fail-on`, `--gemfile`, `--config` |
76
100
  | `clean` | Remove selected gems from `Gemfile`. | `--dry-run`, `--auto-fix`, `--comment`, `--[no-]bundle` |
77
101
  | `pr` | Create a branch, commit the cleanup, and open a GitHub PR. | `--per-gem`, `--[no-]bundle`, `--comment` |
78
102
  | `init` | Write a starter `.gemxray.yml`. | `--force` |
79
103
  | `version` | Print the installed gemxray version. | none |
104
+ | `help` | Print top-level help. | none |
105
+
106
+ ### Shared analysis options
107
+
108
+ `scan`, `clean`, and `pr` all build the same report first, so the options below change which findings are available to print, remove, or turn into pull requests.
109
+
110
+ - `--gemfile PATH`
111
+ Selects the target Gemfile. This also changes the project root used for file edits, `bundle install`, and git operations, because the project root is derived from the Gemfile directory.
112
+ - `--config PATH`
113
+ Loads a specific `.gemxray.yml` instead of the default file in the current working directory.
114
+ - `--only unused,redundant,version`
115
+ Restricts analysis to the listed analyzers. This accepts a comma-separated list. For example, `--only unused` means `clean` and `pr` only act on unused-gem findings.
116
+ - `--severity info|warning|danger`
117
+ Filters the report to findings at or above the selected severity. This happens before command-specific behavior, so hidden findings are also excluded from `clean`, `pr`, and `scan --ci`.
118
+ - `--format terminal|json|yaml`
119
+ Controls output format for `scan`. `clean` and `pr` currently accept the option because they share the same parser, but they do not render the report, so `--format` has no visible effect on those commands today.
120
+ - `--ci`
121
+ Only changes `scan`. When enabled, `scan` exits with status `1` if any reported finding matches `--fail-on` or `ci_fail_on` from config. `clean` and `pr` currently accept the flag but do not use it.
122
+ - `--fail-on info|warning|danger`
123
+ Only changes `scan`, and only matters together with `--ci`. It sets the minimum reported severity that should return exit code `1`. `clean` and `pr` currently accept the flag but do not use it.
124
+ - `-h`, `--help`
125
+ Prints help for the current command and exits with status `0`.
126
+
127
+ ### `scan`
128
+
129
+ `scan` analyzes the target project, formats the resulting report, prints it to standard output, and exits without changing any files.
130
+
131
+ Behavior:
132
+
133
+ - It runs the selected analyzers, merges findings per gem, applies severity overrides, filters the report by `--severity`, and sorts results by severity and gem name.
134
+ - With `--format terminal`, it prints a human-readable tree. With `json` or `yaml`, it prints machine-readable output including summary counts.
135
+ - Without `--ci`, a successful scan exits with status `0` even if findings exist.
136
+ - With `--ci`, the exit status becomes `1` when any reported finding reaches `--fail-on` or `ci_fail_on`.
137
+
138
+ ```bash
139
+ bundle exec gemxray scan
140
+ bundle exec gemxray scan --format json --ci --fail-on danger
141
+ bundle exec gemxray scan --only unused --severity warning
142
+ ```
143
+
144
+ ### `clean`
145
+
146
+ `clean` runs the same analysis pipeline as `scan`, then edits the Gemfile based on the reported results.
147
+
148
+ Behavior:
149
+
150
+ - Without `--auto-fix`, it prompts once per reported result: `Remove <gem> (<severity>)? [y/N]:`.
151
+ - Only `y` and `yes` remove the gem. Any other answer skips it.
152
+ - It edits the full detected source range, so multiline gem declarations are removed as a unit.
153
+ - It writes a backup file at `Gemfile.bak` before saving changes.
154
+ - If nothing is selected, it prints `No removable gems were selected.` and exits with status `0`.
155
+
156
+ Command-specific options:
157
+
158
+ - `--auto-fix` -- Skips prompts and removes every reported `danger` finding automatically. `warning` and `info` findings are never auto-removed.
159
+ - `--dry-run` -- Does not write the Gemfile. Instead, it prints the selected candidates and a preview hunk showing the lines that would be removed or replaced.
160
+ - `--comment` -- Replaces each removed gem entry with a comment such as `# Removed by gemxray: ...` instead of deleting the lines outright.
161
+ - `--bundle`, `--no-bundle` -- After a real edit, `--bundle` runs `bundle install` in the target project. It is skipped automatically during `--dry-run` and when no gems were actually removed.
162
+
163
+ ```bash
164
+ bundle exec gemxray clean
165
+ bundle exec gemxray clean --auto-fix --severity danger
166
+ bundle exec gemxray clean --dry-run --comment
167
+ ```
168
+
169
+ ### `pr`
170
+
171
+ `pr` runs the same analysis pipeline as `scan`, edits the Gemfile, commits the changes on a new branch, pushes the branch, and opens a GitHub pull request.
172
+
173
+ Behavior:
174
+
175
+ - It fails if the report is empty after filters are applied.
176
+ - It requires the target project to be inside a git repository with a clean worktree before it starts.
177
+ - It switches to `github.base_branch`, creates a cleanup branch, edits the Gemfile, commits the change, optionally refreshes `Gemfile.lock`, pushes the branch, and opens a PR.
178
+ - It tries `gh pr create` first. If `gh` is unavailable, it falls back to the GitHub API when `GH_TOKEN` or `GITHUB_TOKEN` is set.
179
+ - The PR body includes removed gems, detection reasons, and a short checklist.
180
+
181
+ Command-specific options:
182
+
183
+ - `--per-gem` -- Creates one branch and one pull request per reported gem instead of grouping everything into a single cleanup PR.
184
+ - `--comment` -- Leaves comments in the Gemfile instead of deleting lines, using the same replacement behavior as `clean --comment`.
185
+ - `--bundle`, `--no-bundle` -- Controls whether `pr` runs `bundle install` before committing. The default is `--bundle` (from `github.bundle_install: true`).
186
+
187
+ ```bash
188
+ bundle exec gemxray pr
189
+ bundle exec gemxray pr --per-gem --no-bundle
190
+ bundle exec gemxray pr --only unused --severity danger
191
+ ```
192
+
193
+ ### `init`
194
+
195
+ `init` writes a starter `.gemxray.yml` into the current working directory.
196
+
197
+ - It does not read `--config`; it always writes `.gemxray.yml` in the directory where you run the command.
198
+ - If the file already exists, the command fails unless you pass `--force`.
199
+
200
+ ```bash
201
+ bundle exec gemxray init
202
+ bundle exec gemxray init --force
203
+ ```
204
+
205
+ ### `version`
206
+
207
+ Prints the installed gemxray version and exits with status `0`.
208
+
209
+ ```bash
210
+ bundle exec gemxray version
211
+ ```
212
+
213
+ ### `help`
214
+
215
+ Prints the top-level command summary and exits with status `0`.
216
+
217
+ ```bash
218
+ bundle exec gemxray help
219
+ bundle exec gemxray --help
220
+ bundle exec gemxray scan --help
221
+ ```
80
222
 
81
223
  ## Severity
82
224
 
83
- - `danger`: high-confidence removal candidate. `clean --auto-fix` only removes `danger` findings.
84
- - `warning`: likely removable, but worth a quick review.
85
- - `info`: informative hint, often tied to pinned versions or lower-confidence redundancy.
225
+ | Level | Meaning | Auto-fix target |
226
+ | --- | --- | --- |
227
+ | `danger` | High-confidence removal candidate. | Yes (`clean --auto-fix` removes these) |
228
+ | `warning` | Likely removable, but worth a quick review. | No |
229
+ | `info` | Informative hint (pinned versions, lower-confidence redundancy). | No |
86
230
 
87
231
  ## Configuration
88
232
 
89
233
  `gemxray` reads `.gemxray.yml` from the working directory unless you pass `--config PATH`.
90
234
 
235
+ The effective config is built in this order:
236
+
237
+ 1. Built-in defaults
238
+ 2. `.gemxray.yml`
239
+ 3. CLI options for the current run
240
+
241
+ Later scalar values override earlier ones. Array values (`scan_dirs`, `whitelist`, `github.labels`, `github.reviewers`) are merged and deduplicated.
242
+
91
243
  ```yaml
92
244
  version: 1
93
245
 
246
+ ci: false
247
+ ci_fail_on: warning
248
+
94
249
  whitelist:
95
250
  - bootsnap
96
251
  - tzinfo-data
@@ -115,30 +270,45 @@ github:
115
270
  bundle_install: true
116
271
  ```
117
272
 
118
- Config fields:
119
-
120
- - `whitelist`: gems to skip entirely.
121
- - `scan_dirs`: extra directories to scan in addition to the defaults: `app`, `lib`, `config`, `db`, `script`, `bin`, `exe`, `spec`, `test`, and `tasks`.
122
- - `redundant_depth`: maximum dependency depth for redundant gem detection.
123
- - `overrides.<gem>.severity`: override a finding severity with `ignore`, `info`, `warning`, or `danger`.
124
- - `github.*`: defaults used by `pr`.
273
+ ### Top-level fields
125
274
 
126
- ## Notes
127
-
128
- - `clean` writes `Gemfile.bak` before editing the file.
129
- - `clean` removes the full source range for multiline gem declarations.
130
- - `clean --bundle` runs `bundle install` after editing.
131
- - `pr` runs `bundle install` before committing by default. Use `pr --no-bundle` to skip it.
132
- - `pr` requires a clean git worktree before it creates branches or commits.
133
- - `pr` switches to `github.base_branch` before creating the cleanup branch.
134
- - If `gh` is unavailable, `pr` falls back to the GitHub API when `GH_TOKEN` or `GITHUB_TOKEN` is set.
135
- - Ruby default and bundled gem checks use cached stdgems data when available and bundled offline data otherwise.
136
- - Rails version hints come from the bundled `data/rails_changes.yml` dataset.
275
+ | Field | Default | Description |
276
+ | --- | --- | --- |
277
+ | `version` | `1` | Schema marker for future compatibility. Currently accepted but does not change behavior. |
278
+ | `gemfile_path` | `Gemfile` | Path to the target Gemfile. Expanded from the current working directory. |
279
+ | `format` | `terminal` | Output format for `scan`. Accepted: `terminal`, `json`, `yaml`. |
280
+ | `only` | all | Restricts analysis to listed analyzers: `unused`, `redundant`, `version`. |
281
+ | `severity` | `info` | Minimum severity kept in the report. Also limits what `clean`, `pr`, and `scan --ci` can act on. |
282
+ | `ci` | `false` | Enables CI-style exit codes for `scan`. |
283
+ | `ci_fail_on` | `warning` | Minimum severity that makes `scan --ci` exit with status `1`. |
284
+ | `auto_fix` | `false` | When `true`, `clean` removes `danger` findings without prompting. |
285
+ | `dry_run` | `false` | When `true`, `clean` previews changes without writing the Gemfile. |
286
+ | `comment` | `false` | When `true`, gem entries are replaced with comments instead of being deleted. |
287
+ | `bundle_install` | `false` | When `true`, `clean` runs `bundle install` after editing. Does not affect `pr`. |
288
+ | `whitelist` | `[]` | Gem names to skip completely. |
289
+ | `scan_dirs` | `[]` | Extra directories added to the built-in scan roots (`app`, `lib`, `config`, `db`, `script`, `bin`, `exe`, `spec`, `test`, `tasks`). |
290
+ | `redundant_depth` | `2` | Maximum dependency depth for the `redundant` analyzer in `Gemfile.lock`. |
291
+ | `overrides` | `{}` | Per-gem overrides keyed by gem name. |
292
+
293
+ ### Override fields
294
+
295
+ `overrides.<gem>.severity` accepts `ignore`, `info`, `warning`, or `danger`.
296
+
297
+ - `ignore` skips the gem before analysis (no finding is produced).
298
+ - `info`, `warning`, `danger` force the final reported severity after analyzers run.
299
+
300
+ ### GitHub fields
301
+
302
+ | Field | Default | Description |
303
+ | --- | --- | --- |
304
+ | `github.base_branch` | `main` | Base branch that `pr` checks out before creating the cleanup branch. |
305
+ | `github.labels` | `["dependencies", "cleanup"]` | Labels applied to created PRs. Custom labels are added to defaults (arrays are merged). |
306
+ | `github.reviewers` | `[]` | Reviewers requested on created PRs. |
307
+ | `github.per_gem` | `false` | When `true`, `pr` creates one branch and one PR per gem. |
308
+ | `github.bundle_install` | `true` | Controls whether `pr` runs `bundle install` before committing. |
137
309
 
138
310
  ## Development
139
311
 
140
- Install dependencies and run the test suite:
141
-
142
312
  ```bash
143
313
  bundle install
144
314
  bundle exec rspec
@@ -149,3 +319,11 @@ Run the executable locally:
149
319
  ```bash
150
320
  ruby exe/gemxray scan --format terminal
151
321
  ```
322
+
323
+ ## Contributing
324
+
325
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/ydah/gemxray).
326
+
327
+ ## License
328
+
329
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/lib/gemxray/cli.rb CHANGED
@@ -62,7 +62,7 @@ module GemXray
62
62
  config = Config.load(parse_scan_options(argv))
63
63
  report = Scanner.new(config).run
64
64
  out.puts(formatter_for(config.format).render(report))
65
- config.ci? && report.results.any? ? 1 : 0
65
+ config.ci_failure?(report.results) ? 1 : 0
66
66
  end
67
67
 
68
68
  def run_clean(argv)
@@ -187,7 +187,10 @@ module GemXray
187
187
  parser.on("--severity LEVEL", %w[info warning danger], "minimum severity to report") do |value|
188
188
  options[:severity] = value
189
189
  end
190
- parser.on("--ci", "exit with status 1 when issues are found") { options[:ci] = true }
190
+ parser.on("--fail-on LEVEL", %w[info warning danger], "minimum reported severity that makes --ci exit 1") do |value|
191
+ options[:ci_fail_on] = value
192
+ end
193
+ parser.on("--ci", "exit with status 1 when findings at or above fail-on level are found") { options[:ci] = true }
191
194
  parser.on("--config PATH", "path to .gemxray.yml") { |value| options[:config_path] = value }
192
195
  parser.on("-h", "--help", "show help") do
193
196
  out.puts(parser)
@@ -14,6 +14,7 @@ module GemXray
14
14
  auto_fix: false,
15
15
  dry_run: false,
16
16
  ci: false,
17
+ ci_fail_on: "warning",
17
18
  comment: false,
18
19
  bundle_install: false,
19
20
  whitelist: [],
@@ -32,6 +33,9 @@ module GemXray
32
33
  TEMPLATE = <<~YAML.freeze
33
34
  version: 1
34
35
 
36
+ ci: false
37
+ ci_fail_on: warning
38
+
35
39
  whitelist:
36
40
  - bootsnap
37
41
  - tzinfo-data
@@ -55,7 +59,7 @@ module GemXray
55
59
  YAML
56
60
 
57
61
  attr_reader :config_path, :gemfile_path, :format, :only, :severity_threshold, :whitelist,
58
- :scan_dirs, :overrides, :redundant_depth, :github
62
+ :scan_dirs, :overrides, :redundant_depth, :github, :ci_fail_threshold
59
63
 
60
64
  def self.load(options = {})
61
65
  raw_options = symbolize_keys(options)
@@ -103,6 +107,7 @@ module GemXray
103
107
  @format = options.fetch(:format).to_s
104
108
  @only = normalize_only(options[:only])
105
109
  @severity_threshold = normalize_severity(options.fetch(:severity))
110
+ @ci_fail_threshold = normalize_severity(options.fetch(:ci_fail_on))
106
111
  @whitelist = Array(options[:whitelist]).map(&:to_s).uniq
107
112
  @scan_dirs = (DEFAULT_SCAN_DIRS + Array(options[:scan_dirs]).map(&:to_s)).uniq
108
113
  @overrides = options.fetch(:overrides, {})
@@ -135,6 +140,12 @@ module GemXray
135
140
  @ci
136
141
  end
137
142
 
143
+ def ci_failure?(results)
144
+ return false unless ci?
145
+
146
+ Array(results).any? { |result| severity_matches_threshold?(result.severity, ci_fail_threshold) }
147
+ end
148
+
138
149
  def comment?
139
150
  @comment
140
151
  end
@@ -165,7 +176,7 @@ module GemXray
165
176
  end
166
177
 
167
178
  def severity_in_scope?(severity)
168
- SEVERITY_ORDER.fetch(severity) <= SEVERITY_ORDER.fetch(severity_threshold)
179
+ severity_matches_threshold?(severity, severity_threshold)
169
180
  end
170
181
 
171
182
  def github_base_branch
@@ -211,5 +222,9 @@ module GemXray
211
222
  def truthy?(value)
212
223
  value == true || value.to_s == "true"
213
224
  end
225
+
226
+ def severity_matches_threshold?(severity, threshold)
227
+ SEVERITY_ORDER.fetch(severity) <= SEVERITY_ORDER.fetch(threshold)
228
+ end
214
229
  end
215
230
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemXray
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemxray
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada