gem_changelog_diff 0.9.0 → 1.0.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: 4c1e8032c69ad427e1469a9620c6df00d3d401e83f939e6fd8c99335fb709fc8
4
- data.tar.gz: 9d536f58e4148db45d1de19107cfc28270ab5ce92b892375cf6eae01e3ea5613
3
+ metadata.gz: d30e61b6a53988f812beaba85ab06f90dfbe3968a2b9c6fe036813c81e233ad8
4
+ data.tar.gz: e163dd6a353a7abf62a2fc02a841bf19854e8f5a427f194572e169a5f63384b8
5
5
  SHA512:
6
- metadata.gz: 8e3c3547a707fa41cf491a35f66288f1fbe8210ee99d7742bcc918a5d9721298d9c75bdbe42bd68a77c5de535f1fe0899fc1beea269291adef2b80e27a9ded9a
7
- data.tar.gz: 594f28bb073332585345ae5569c2c1298c4ca705df61ecd81e92f5cfd3c279110f13a5c34c18d543a20c75b7133d84cc011e5756116f331499644991a774ba73
6
+ metadata.gz: 5083b02d3caf6f4fdcc1f6695ff4f1189789069b44c34fb70ee8246ab5e909024ef2d342805077d0032cd298f7d5a57ec827961c1935063ab9b78015dc8e5dec
7
+ data.tar.gz: d5f330c0b92aeb969281928b976672a1d9c0cf9a886f1df8e2f37a84be03635426ce86e40709f22adaf7a59e0f2e0c4b3d18790decdf48275d99838ef0d813e6
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --markup markdown
2
+ --output-dir doc
3
+ lib/**/*.rb
4
+ -
5
+ README.md
6
+ CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.0] - 2026-06-18
11
+
12
+ ### Added
13
+
14
+ - YARD documentation on all public classes and methods
15
+ - `CONTRIBUTING.md` with development setup, architecture overview, and PR requirements
16
+ - `SECURITY.md` with vulnerability reporting instructions
17
+ - API stability guarantee: semver contract begins at 1.0.0
18
+ - `.yardopts` configuration and `yard` development dependency
19
+ - `documentation_uri` in gemspec metadata
20
+ - `gh auth token` as automatic token source for developers with GitHub CLI installed
21
+
10
22
  ## [0.9.0] - 2026-06-18
11
23
 
12
24
  ### Added
@@ -131,7 +143,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
131
143
  - Plain text formatter for changelog output
132
144
  - Full end-to-end pipeline: detect → lookup → fetch → format
133
145
 
134
- [Unreleased]: https://github.com/eclectic-coding/gem_changelog_diff/compare/v0.9.0...HEAD
146
+ [Unreleased]: https://github.com/eclectic-coding/gem_changelog_diff/compare/v1.0.0...HEAD
147
+ [1.0.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v1.0.0
135
148
  [0.9.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v0.9.0
136
149
  [0.8.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v0.8.0
137
150
  [0.7.0]: https://github.com/eclectic-coding/gem_changelog_diff/releases/tag/v0.7.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,57 @@
1
+ # Contributing
2
+
3
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eclectic-coding/gem_changelog_diff.
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/eclectic-coding/gem_changelog_diff.git
9
+ cd gem_changelog_diff
10
+ bin/setup
11
+ ```
12
+
13
+ ## Running Tests
14
+
15
+ ```bash
16
+ bundle exec rake # Run all checks (rubocop, bundler-audit, rspec)
17
+ bundle exec rake spec # Run tests only
18
+ bundle exec rspec spec/path_spec.rb # Run a single file
19
+ bundle exec rspec spec/path_spec.rb:42 # Run a single example
20
+ ```
21
+
22
+ Integration tests use VCR cassettes committed to the repo. To re-record:
23
+
24
+ ```bash
25
+ GITHUB_TOKEN=ghp_... bundle exec rspec spec/integration/
26
+ ```
27
+
28
+ ## Linting
29
+
30
+ ```bash
31
+ bundle exec rubocop # Check style
32
+ bundle exec rubocop -a # Auto-correct
33
+ ```
34
+
35
+ ## Architecture
36
+
37
+ The pipeline flows through these stages:
38
+
39
+ 1. **Detection** -- `Detector` runs `bundle outdated` (or `LockfileParser` reads `Gemfile.lock`) to find outdated gems
40
+ 2. **Resolution** -- `RubygemsClient` queries the RubyGems API, `UriResolver` extracts the GitHub slug
41
+ 3. **Fetching** -- `GithubClient` fetches releases from the GitHub API; `ChangelogParser` parses `CHANGELOG.md` as a fallback. `SourceResolver` orchestrates both
42
+ 4. **Concurrency** -- `ConcurrentFetcher` runs fetches in a thread pool
43
+ 5. **Formatting** -- `Formatters::Text`, `Json`, or `Markdown` render the output
44
+ 6. **CLI** -- `CLI` (Thor) ties everything together with flags, config, and exit codes
45
+
46
+ ## Branch Workflow
47
+
48
+ All feature work lives on `feature/<version>-<scope>` branches. Every branch produces two commits:
49
+
50
+ 1. **Feature commit** -- implementation + specs. Run `bundle exec rake` and fix all failures before committing.
51
+ 2. **Docs commit** -- update `CHANGELOG.md`, remove shipped items from `ROADMAP.md`, update `README.md` if needed.
52
+
53
+ ## Pull Request Requirements
54
+
55
+ - All CI checks must pass (lint, security audit, tests on Ruby 3.3, 3.4, and 4.0)
56
+ - 100% line coverage required
57
+ - RuboCop must report zero offenses
data/README.md CHANGED
@@ -25,8 +25,10 @@ CLI that shows you the changelog diff for each gem before you `bundle update`, p
25
25
  - [Concurrency](#concurrency)
26
26
  - [Exit Codes](#exit-codes)
27
27
  - [Configuration File](#configuration-file)
28
+ - [Stability](#stability)
28
29
  - [Development](#development)
29
30
  - [Contributing](#contributing)
31
+ - [Security](#security)
30
32
  - [License](#license)
31
33
 
32
34
  ## Installation
@@ -73,7 +75,11 @@ export GITHUB_TOKEN=ghp_your_token
73
75
  gem_changelog_diff
74
76
  ```
75
77
 
76
- Token resolution priority: `--token` flag → `GITHUB_TOKEN` env → Rails credentials → config file.
78
+ Token resolution priority: `--token` flag → `GITHUB_TOKEN` env → Rails credentials → `gh auth token` → config file.
79
+
80
+ #### GitHub CLI
81
+
82
+ If you have the [GitHub CLI](https://cli.github.com/) installed and authenticated (`gh auth login`), the token is picked up automatically — no configuration needed.
77
83
 
78
84
  #### Rails Credentials
79
85
 
@@ -249,6 +255,14 @@ CLI flags always take priority over config file values.
249
255
 
250
256
  [Back to top](#gemchangelogdiff)
251
257
 
258
+ ## Stability
259
+
260
+ Starting with version 1.0.0, this gem follows [Semantic Versioning](https://semver.org/). The public API is frozen — breaking changes require a major version bump.
261
+
262
+ A future Bundler plugin (`bundler-changelog-diff`) may provide deeper integration, but the standalone CLI will remain the primary interface.
263
+
264
+ [Back to top](#gemchangelogdiff)
265
+
252
266
  ## Development
253
267
 
254
268
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -259,7 +273,13 @@ To install this gem onto your local machine, run `bundle exec rake install`.
259
273
 
260
274
  ## Contributing
261
275
 
262
- Bug reports and pull requests are welcome on GitHub at https://github.com/eclectic-coding/gem_changelog_diff.
276
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eclectic-coding/gem_changelog_diff. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
277
+
278
+ [Back to top](#gemchangelogdiff)
279
+
280
+ ## Security
281
+
282
+ To report a security vulnerability, see [SECURITY.md](SECURITY.md).
263
283
 
264
284
  [Back to top](#gemchangelogdiff)
265
285
 
data/ROADMAP.md CHANGED
@@ -2,16 +2,3 @@
2
2
 
3
3
  Feature roadmap for gem_changelog_diff. Each section is auto-pruned by `bin/release` when that version ships.
4
4
 
5
- ## 1.0.0 -- Stable Release
6
-
7
- Public API is frozen. Semantic versioning contract begins.
8
-
9
- - API stability guarantee: breaking changes require a major version bump
10
- - YARD documentation on all public classes and methods
11
- - `CONTRIBUTING.md` with development setup, testing, and architecture overview
12
- - `SECURITY.md` with vulnerability reporting instructions
13
- - Document future Bundler plugin possibility (`bundler-changelog-diff`)
14
-
15
- **Dependencies:** `yard` (development)
16
-
17
- ---
data/SECURITY.md ADDED
@@ -0,0 +1,33 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | 1.x | Yes |
8
+ | < 1.0 | No |
9
+
10
+ ## Reporting a Vulnerability
11
+
12
+ If you discover a security vulnerability in gem_changelog_diff, please report it responsibly.
13
+
14
+ **Email:** chuck@eclecticcoding.com
15
+
16
+ Please include:
17
+
18
+ - A description of the vulnerability
19
+ - Steps to reproduce the issue
20
+ - The potential impact
21
+
22
+ **Expected response time:** You should receive an acknowledgment within 48 hours. A fix or mitigation plan will be communicated within 7 days.
23
+
24
+ ## Scope
25
+
26
+ Security issues relevant to this gem include:
27
+
28
+ - Command injection via gem names or user-supplied arguments
29
+ - Credential leakage (GitHub tokens in logs, cached responses, or error messages)
30
+ - Unsafe deserialization of cached data or API responses
31
+ - Path traversal in file operations (cache, config, output)
32
+
33
+ Issues related to the GitHub API, RubyGems API, or upstream dependencies should be reported to their respective maintainers.
@@ -6,6 +6,7 @@ require "json"
6
6
  require "net/http"
7
7
 
8
8
  module GemChangelogDiff
9
+ # Disk-based HTTP response cache with ETag revalidation.
9
10
  class Cache
10
11
  DEFAULT_DIR = File.join(Dir.home, ".cache", "gem_changelog_diff")
11
12
  DEFAULT_TTL = 86_400
@@ -16,6 +17,10 @@ module GemChangelogDiff
16
17
  @enabled = enabled
17
18
  end
18
19
 
20
+ # Fetches a response, returning cached data when available.
21
+ # @param uri [URI::Generic] the request URI
22
+ # @param headers [Hash<String, String>] additional HTTP headers
23
+ # @return [Net::HTTPResponse, CachedResponse]
19
24
  def get(uri, headers: {})
20
25
  return fetch_from_network(uri, headers) unless @enabled
21
26
 
@@ -31,6 +36,8 @@ module GemChangelogDiff
31
36
  end
32
37
  end
33
38
 
39
+ # Deletes all cached data.
40
+ # @return [void]
34
41
  def clear
35
42
  FileUtils.rm_rf(@cache_dir)
36
43
  end
@@ -104,7 +111,10 @@ module GemChangelogDiff
104
111
  end
105
112
  end
106
113
 
114
+ # Lightweight stand-in for Net::HTTPResponse built from cached data.
107
115
  class CachedResponse
116
+ # @return [String] the response body
117
+ # @return [String] the HTTP status code
108
118
  attr_reader :body, :code
109
119
 
110
120
  def initialize(body, code)
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
 
6
6
  module GemChangelogDiff
7
+ # Parses CHANGELOG.md files from GitHub repos as a fallback source.
7
8
  class ChangelogParser
8
9
  CONTENTS_URL = "https://api.github.com/repos/%<repo>s/contents/%<path>s"
9
10
  FILENAMES = %w[CHANGELOG.md CHANGES.md History.md NEWS.md].freeze
@@ -13,6 +14,12 @@ module GemChangelogDiff
13
14
  @cache = cache
14
15
  end
15
16
 
17
+ # Parses changelog entries between two versions from a repo's changelog file.
18
+ # @param repo [String] GitHub "owner/repo" slug
19
+ # @param current_version [String] currently locked version (exclusive)
20
+ # @param newest_version [String] target version (inclusive)
21
+ # @return [Array<Hash>] release hashes with :tag_name, :name, :published_at, :body
22
+ # @raise [NetworkError] on HTTP connection failures
16
23
  def entries_between(repo, current_version, newest_version)
17
24
  content = fetch_changelog(repo)
18
25
  return [] unless content
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
3
4
  require "thor"
4
5
 
5
6
  module GemChangelogDiff
7
+ # Thor-based command-line interface.
6
8
  class CLI < Thor
7
9
  def self.exit_on_failure?
8
10
  true
@@ -29,6 +31,7 @@ module GemChangelogDiff
29
31
  class_option :timeout, type: :numeric, desc: "Per-request timeout in seconds (default: 10)"
30
32
 
31
33
  desc "check [GEM...]", "Show changelog diffs for outdated gems"
34
+ # @param gem_names [Array<String>] optional gem names to filter
32
35
  def check(*gem_names)
33
36
  setup_environment
34
37
  gems = filter_gems(detect_gems, gem_names)
@@ -42,6 +45,9 @@ module GemChangelogDiff
42
45
  end
43
46
 
44
47
  desc "show GEM FROM_VERSION TO_VERSION", "Show changelog between two versions of a gem"
48
+ # @param gem_name [String] the gem to look up
49
+ # @param from_version [String] current version (exclusive)
50
+ # @param to_version [String] target version (inclusive)
45
51
  def show(gem_name, from_version, to_version)
46
52
  setup_environment
47
53
  gem = OutdatedGem.new(name: gem_name, current_version: from_version, newest_version: to_version)
@@ -97,6 +103,7 @@ module GemChangelogDiff
97
103
  def configure_token
98
104
  token = options[:token] || ENV.fetch("GITHUB_TOKEN", nil)
99
105
  token ||= rails_credentials_token
106
+ token ||= gh_cli_token
100
107
  token ||= GemChangelogDiff.configuration.github_token
101
108
  GemChangelogDiff.configuration.github_token = token if token
102
109
  end
@@ -106,6 +113,14 @@ module GemChangelogDiff
106
113
  GemChangelogDiff.configuration.request_timeout = timeout
107
114
  end
108
115
 
116
+ def gh_cli_token
117
+ output, status = Open3.capture2("gh", "auth", "token")
118
+ token = output.strip
119
+ token if status.success? && !token.empty?
120
+ rescue Errno::ENOENT
121
+ nil
122
+ end
123
+
109
124
  def rails_credentials_token
110
125
  return unless defined?(Rails) && Rails.application.respond_to?(:credentials)
111
126
 
@@ -3,11 +3,17 @@
3
3
  require "timeout"
4
4
 
5
5
  module GemChangelogDiff
6
+ # Thread pool for fetching multiple gems in parallel.
6
7
  class ConcurrentFetcher
7
8
  def initialize(concurrency: 4)
8
9
  @concurrency = concurrency
9
10
  end
10
11
 
12
+ # Processes items concurrently, returning results in order.
13
+ # @param items [Array] items to process
14
+ # @yield [item] block called for each item
15
+ # @return [Array] results in the same order as items
16
+ # @raise [NetworkError] if the total timeout is exceeded
11
17
  def fetch_all(items, &)
12
18
  return items.map(&) if @concurrency <= 1
13
19
 
@@ -3,6 +3,7 @@
3
3
  require "yaml"
4
4
 
5
5
  module GemChangelogDiff
6
+ # Loads and merges YAML config from user and project locations.
6
7
  class ConfigLoader
7
8
  USER_CONFIG_DIR = File.join(Dir.home, ".config", "gem_changelog_diff")
8
9
  USER_CONFIG_PATH = File.join(USER_CONFIG_DIR, "config.yml")
@@ -12,6 +13,8 @@ module GemChangelogDiff
12
13
  @project_dir = project_dir
13
14
  end
14
15
 
16
+ # Loads config from user and project files, with project taking priority.
17
+ # @return [Hash<Symbol, Object>]
15
18
  def load
16
19
  user_config = load_file(USER_CONFIG_PATH)
17
20
  project_config = load_file(project_config_path)
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # Holds runtime settings for the gem (token, cache, format, timeouts).
4
5
  class Configuration
6
+ # @return [String, nil] GitHub personal access token
7
+ # @return [Boolean] whether disk caching is enabled
8
+ # @return [Integer] cache time-to-live in seconds
9
+ # @return [String] default output format ("text", "json", "markdown")
10
+ # @return [Integer] number of concurrent fetch threads
11
+ # @return [Array<String>] gem names to skip
12
+ # @return [Boolean] whether to disable colored output
13
+ # @return [Integer] per-request HTTP timeout in seconds
14
+ # @return [Integer] total operation timeout in seconds
5
15
  attr_accessor :github_token, :cache_enabled, :cache_ttl,
6
16
  :default_format, :concurrency, :ignore_gems, :no_color,
7
17
  :request_timeout, :total_timeout
@@ -20,6 +30,9 @@ module GemChangelogDiff
20
30
  @total_timeout = 120
21
31
  end
22
32
 
33
+ # Applies a hash of settings, ignoring unknown keys and nil values.
34
+ # @param hash [Hash<Symbol, Object>] configuration key-value pairs
35
+ # @return [void]
23
36
  def apply(hash)
24
37
  hash.each do |key, value|
25
38
  public_send(:"#{key}=", value) if VALID_KEYS.include?(key) && !value.nil?
@@ -27,14 +40,21 @@ module GemChangelogDiff
27
40
  end
28
41
  end
29
42
 
43
+ # Returns the global configuration instance.
44
+ # @return [Configuration]
30
45
  def self.configuration
31
46
  @configuration ||= Configuration.new
32
47
  end
33
48
 
49
+ # Yields the global configuration for modification.
50
+ # @yieldparam config [Configuration]
51
+ # @return [void]
34
52
  def self.configure
35
53
  yield(configuration)
36
54
  end
37
55
 
56
+ # Resets the global configuration to defaults.
57
+ # @return [Configuration]
38
58
  def self.reset_configuration!
39
59
  @configuration = Configuration.new
40
60
  end
@@ -3,9 +3,13 @@
3
3
  require "open3"
4
4
 
5
5
  module GemChangelogDiff
6
+ # Detects outdated gems by parsing `bundle outdated --parseable` output.
6
7
  class Detector
7
8
  PARSEABLE_REGEX = /\A(\S+)\s+\(newest\s+([^,]+),\s+installed\s+([^,)]+)/
8
9
 
10
+ # Runs bundle outdated and returns the list of outdated gems.
11
+ # @return [Array<OutdatedGem>]
12
+ # @raise [Error] if bundle outdated fails
9
13
  def detect
10
14
  output = run_bundle_outdated
11
15
  parse(output)
@@ -1,8 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # Raised when a gem's source repository cannot be found on GitHub.
4
5
  class RepoNotFoundError < Error; end
6
+
7
+ # Raised when the GitHub API returns an unexpected error response.
5
8
  class GitHubAPIError < Error; end
9
+
10
+ # Raised when the GitHub API rate limit is exceeded.
6
11
  class RateLimitError < GitHubAPIError; end
12
+
13
+ # Raised on HTTP connection failures (timeouts, DNS, SSL).
7
14
  class NetworkError < Error; end
8
15
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # Process exit code constants for CLI commands.
4
5
  module ExitCode
5
6
  SUCCESS = 0
6
7
  ERROR = 1
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # @deprecated Use {Formatters::Text} directly or {Formatters.build}.
4
5
  Formatter = Formatters::Text
5
6
  end
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # Output formatters for rendering gem reports.
4
5
  module Formatters
6
+ # Builds a formatter instance for the given format name.
7
+ # @param format [String] "text", "json", or "markdown"
8
+ # @param color [Boolean] whether to enable ANSI colors
9
+ # @return [Text, Json, Markdown]
10
+ # @raise [ArgumentError] for unknown formats
5
11
  def self.build(format:, color: false)
6
12
  case format
7
13
  when "text" then Text.new(color: color)
@@ -11,11 +17,15 @@ module GemChangelogDiff
11
17
  end
12
18
  end
13
19
 
20
+ # Abstract base class for output formatters.
14
21
  class Base
15
22
  def initialize(color: false)
16
23
  @color = color
17
24
  end
18
25
 
26
+ # Formats gem reports into a string.
27
+ # @param _gem_reports [Array<Hash>] list of gem report hashes
28
+ # @return [String]
19
29
  def format(_gem_reports)
20
30
  raise NotImplementedError, "#{self.class}#format must be implemented"
21
31
  end
@@ -4,6 +4,7 @@ require "json"
4
4
 
5
5
  module GemChangelogDiff
6
6
  module Formatters
7
+ # JSON formatter for machine-readable output.
7
8
  class Json < Base
8
9
  def format(gem_reports)
9
10
  counts = summary_counts(gem_reports)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module GemChangelogDiff
4
4
  module Formatters
5
+ # Markdown formatter for PR descriptions and documentation.
5
6
  class Markdown < Base
6
7
  def format(gem_reports)
7
8
  sections = gem_reports.map { |report| format_gem(report) }
@@ -4,6 +4,7 @@ require "tty-color"
4
4
 
5
5
  module GemChangelogDiff
6
6
  module Formatters
7
+ # Plain text formatter with optional ANSI color output.
7
8
  class Text < Base
8
9
  BOLD = "\e[1m"
9
10
  CYAN = "\e[36m"
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
 
6
6
  module GemChangelogDiff
7
+ # Fetches release notes from the GitHub Releases API with pagination.
7
8
  class GithubClient
8
9
  RELEASES_URL = "https://api.github.com/repos/%<repo>s/releases"
9
10
  RATE_LIMIT_WARNING_THRESHOLD = 10
@@ -13,6 +14,11 @@ module GemChangelogDiff
13
14
  @cache = cache
14
15
  end
15
16
 
17
+ # Returns releases between two versions, sorted newest first.
18
+ # @param repo [String] GitHub "owner/repo" slug
19
+ # @param current_version [String] currently locked version (exclusive)
20
+ # @param newest_version [String] target version (inclusive)
21
+ # @return [Array<Hash>] release hashes with :tag_name, :name, :published_at, :body
16
22
  def releases_between(repo, current_version, newest_version)
17
23
  gem_name = repo.split("/").last
18
24
  @active_matcher = TagMatcher.new(gem_name: gem_name)
@@ -3,11 +3,14 @@
3
3
  require "tty-prompt"
4
4
 
5
5
  module GemChangelogDiff
6
+ # Presents a multi-select prompt for choosing which gems to check.
6
7
  class Interactive
7
8
  def initialize(gems:)
8
9
  @gems = gems
9
10
  end
10
11
 
12
+ # Displays the selection prompt and returns chosen gems.
13
+ # @return [Array<OutdatedGem>]
11
14
  def select
12
15
  prompt = TTY::Prompt.new
13
16
  prompt.multi_select("Select gems to check:",
@@ -3,11 +3,16 @@
3
3
  require "bundler"
4
4
 
5
5
  module GemChangelogDiff
6
+ # Detects outdated gems by comparing Gemfile.lock specs to RubyGems.
6
7
  class LockfileParser
7
8
  def initialize(rubygems_client: RubygemsClient.new)
8
9
  @rubygems_client = rubygems_client
9
10
  end
10
11
 
12
+ # Parses the lockfile and returns gems with newer versions available.
13
+ # @param lockfile_path [String] path to Gemfile.lock
14
+ # @return [Array<OutdatedGem>]
15
+ # @raise [Error] if the lockfile is not found
11
16
  def detect(lockfile_path: "Gemfile.lock")
12
17
  content = File.read(lockfile_path)
13
18
  parser = Bundler::LockfileParser.new(content)
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # Immutable value object representing a gem with an available update.
5
+ # @!attribute [r] name
6
+ # @return [String] the gem name
7
+ # @!attribute [r] current_version
8
+ # @return [String] the currently locked version
9
+ # @!attribute [r] newest_version
10
+ # @return [String] the latest available version
4
11
  OutdatedGem = Data.define(:name, :current_version, :newest_version)
5
12
  end
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
 
6
6
  module GemChangelogDiff
7
+ # Queries the RubyGems.org API for gem metadata and source repository URLs.
7
8
  class RubygemsClient
8
9
  RUBYGEMS_API = "https://rubygems.org/api/v1/gems/%<name>s.json"
9
10
 
@@ -12,6 +13,9 @@ module GemChangelogDiff
12
13
  @uri_resolver = uri_resolver
13
14
  end
14
15
 
16
+ # Looks up the GitHub repository slug for a gem.
17
+ # @param gem_name [String]
18
+ # @return [String, nil] "owner/repo" slug, or nil if not found
15
19
  def repo_url(gem_name)
16
20
  data = fetch_gem_data(gem_name)
17
21
  return nil unless data
@@ -19,6 +23,9 @@ module GemChangelogDiff
19
23
  @uri_resolver.resolve(data)
20
24
  end
21
25
 
26
+ # Returns the latest version string for a gem from RubyGems.
27
+ # @param gem_name [String]
28
+ # @return [String, nil]
22
29
  def latest_version(gem_name)
23
30
  data = fetch_gem_data(gem_name)
24
31
  data&.dig("version")
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # Fetches release notes, trying GitHub Releases first then changelog files.
4
5
  class SourceResolver
5
6
  def initialize(github_client: GithubClient.new, changelog_parser: ChangelogParser.new)
6
7
  @github_client = github_client
7
8
  @changelog_parser = changelog_parser
8
9
  end
9
10
 
11
+ # Returns release entries between two versions for a given repo.
12
+ # @param repo [String] GitHub "owner/repo" slug
13
+ # @param current_version [String] currently locked version (exclusive)
14
+ # @param newest_version [String] target version (inclusive)
15
+ # @return [Array<Hash>] release hashes with :tag_name, :name, :published_at, :body
10
16
  def resolve(repo, current_version, newest_version)
11
17
  releases = @github_client.releases_between(repo, current_version, newest_version)
12
18
  return releases unless releases.empty?
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
+ # Extracts version strings from various Git tag formats.
4
5
  class TagMatcher
5
6
  STANDARD_PATTERN = /\A(?:release-)?v?(\d+\..+)\z/
6
7
 
@@ -8,6 +9,9 @@ module GemChangelogDiff
8
9
  @gem_name = gem_name
9
10
  end
10
11
 
12
+ # Parses a version string from a tag name.
13
+ # @param tag [String, nil] the Git tag name
14
+ # @return [String, nil] extracted version, or nil if unparseable
11
15
  def extract_version(tag)
12
16
  return nil if tag.nil? || tag.strip.empty?
13
17
 
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "uri"
5
5
 
6
6
  module GemChangelogDiff
7
+ # Resolves a gem's RubyGems metadata to a GitHub owner/repo slug.
7
8
  class UriResolver
8
9
  GITHUB_REGEX = %r{github\.com/([^/]+)/([^/]+)}
9
10
  NON_GITHUB_HOSTS = {
@@ -15,6 +16,10 @@ module GemChangelogDiff
15
16
  URI_FIELDS = %w[source_code_uri homepage_uri bug_tracker_uri].freeze
16
17
  MAX_REDIRECTS = 3
17
18
 
19
+ # Extracts a GitHub slug from gem metadata, following redirects.
20
+ # @param gem_data [Hash<String, Object>] RubyGems API response data
21
+ # @return [String, nil] "owner/repo" slug, or nil if not on GitHub
22
+ # @raise [RepoNotFoundError] if hosted on a non-GitHub platform
18
23
  def resolve(gem_data)
19
24
  uris = extract_uris(gem_data)
20
25
  return nil if uris.empty?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GemChangelogDiff
4
- VERSION = "0.9.0"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -4,7 +4,9 @@ require_relative "gem_changelog_diff/version"
4
4
  require_relative "gem_changelog_diff/configuration"
5
5
  require_relative "gem_changelog_diff/config_loader"
6
6
 
7
+ # CLI that shows changelog diffs for outdated gems before you bundle update.
7
8
  module GemChangelogDiff
9
+ # Base error class for all gem_changelog_diff errors.
8
10
  class Error < StandardError; end
9
11
  end
10
12
 
@@ -317,6 +317,7 @@ module GemChangelogDiff
317
317
  def dry_run_output: (Array[OutdatedGem] gems) -> void
318
318
  def format_dry_run: (Array[OutdatedGem] gems) -> String
319
319
  def configure_timeout: () -> void
320
+ def gh_cli_token: () -> String?
320
321
  def rails_credentials_token: () -> String?
321
322
  def apply_interactive: (Array[OutdatedGem] gems) -> Array[OutdatedGem]
322
323
  def output_results: (Array[OutdatedGem] gems) -> Array[gem_report]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gem_changelog_diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -76,12 +76,15 @@ extra_rdoc_files: []
76
76
  files:
77
77
  - ".github/workflows/main.yml"
78
78
  - ".github/workflows/publish.yml"
79
+ - ".yardopts"
79
80
  - CHANGELOG.md
80
81
  - CLAUDE.md
82
+ - CONTRIBUTING.md
81
83
  - LICENSE.txt
82
84
  - README.md
83
85
  - ROADMAP.md
84
86
  - Rakefile
87
+ - SECURITY.md
85
88
  - codecov.yml
86
89
  - exe/gem_changelog_diff
87
90
  - lib/gem_changelog_diff.rb
@@ -117,6 +120,7 @@ metadata:
117
120
  homepage_uri: https://github.com/eclectic-coding/gem_changelog_diff
118
121
  source_code_uri: https://github.com/eclectic-coding/gem_changelog_diff
119
122
  changelog_uri: https://github.com/eclectic-coding/gem_changelog_diff/blob/main/CHANGELOG.md
123
+ documentation_uri: https://rubydoc.info/gems/gem_changelog_diff
120
124
  rubygems_mfa_required: 'true'
121
125
  rdoc_options: []
122
126
  require_paths: