bundler-alive 0.1.0 → 0.1.3

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: 95401e4f8058b42f643043c89f012ff2c684ad2844ccd8ae8b8f9740d7262b2b
4
- data.tar.gz: 5de7bffe8f34c12365916c5835c0321880303c69fde54ea588013548e794f9de
3
+ metadata.gz: cf64468693c247f87a2ffbce58960dd82d1313614d1433d0d032210f080e8fd4
4
+ data.tar.gz: a3d9f48ce1e8a932ae954897448acf7ab5cc82123992bd82b72ab33b7a8902b0
5
5
  SHA512:
6
- metadata.gz: cf87c1bb0ffa1e5cabb3739024e97e90cdacd94a1540e30454bd16d36b56e6f2a51904b75c3c843decc1a07ec504925d317632dc4113f2a6c385942317ddb670
7
- data.tar.gz: ce0786a1ea7ce4418de1640178b76536f852a8b0376327cc0a5d86a26f46b0dea520504c6a2650ce2406c7f800510046d0f6eb37ddcb5cf3abc064f6b965a6c4
6
+ metadata.gz: 0c4d5747e3b1a133bcc8e1d757fb0ca790b280c667b7f2aa865c95363db66a878f2b0383d436a3b83cb43c19b28d06815c226d7fde09411d626ecdbc0508836f
7
+ data.tar.gz: e4ad9d70fb50258517e8cfbde02aea45435575e0e41435a26e9c5e35141b6df5e71bbebd85f11837afc6fdaa2b76c89508191234417214471f64c18b43d5f8d9
data/.rspec CHANGED
@@ -2,3 +2,4 @@
2
2
  --color
3
3
  --warnings
4
4
  --require spec_helper
5
+ --backtrace
data/Gemfile CHANGED
@@ -17,7 +17,11 @@ group :development do
17
17
  end
18
18
 
19
19
  group :test do
20
- gem "simplecov"
20
+ # Workaround for cc-test-reporter with SimpleCov 0.18.
21
+ # Stop upgrading SimpleCov until the following issue will be resolved.
22
+ # https://github.com/codeclimate/test-reporter/issues/418
23
+ gem "factory_bot"
24
+ gem "simplecov", "~> 0.10", "< 0.18"
21
25
  gem "vcr"
22
26
  gem "webmock"
23
27
  end
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # bundler-alive
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/bundler-alive.svg)](https://badge.fury.io/rb/bundler-alive)
3
4
  ![bundler-alive](https://github.com/kyoshidajp/bundler-alive/actions/workflows/ci.yml/badge.svg)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/a79d53257bc5e93842f6/maintainability)](https://codeclimate.com/github/kyoshidajp/bundler-alive/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/a79d53257bc5e93842f6/test_coverage)](https://codeclimate.com/github/kyoshidajp/bundler-alive/test_coverage)
4
7
 
5
8
  `bunder-alive` checks if gems in a RubyGem's `Gemfile.lock` are active.
6
9
 
@@ -16,10 +19,17 @@ $ gem install bunlder-alive
16
19
 
17
20
  ```
18
21
  $ bundle-alive
22
+ 6 gems are in Gemfile.lock
23
+ ..W....
24
+ Get all source code repository URLs of gems are done!
25
+ .....
19
26
  Name: journey
20
27
  URL: http://github.com/rails/journey
21
28
  Status: false
22
29
 
30
+ Gem: bundle-alive is not found in RubyGems.org.
31
+
32
+ Total: 6 (Dead: 1, Alive: 4, Unknown: 1)
23
33
  Not alive gems are found!
24
34
  ```
25
35
 
@@ -29,7 +39,7 @@ Default `Gemfile.lock` location is in your current directory. You can specify it
29
39
  $ bundle-alive -G /path/to/Gemfile.lock
30
40
  ```
31
41
 
32
- In most cases, the following error is output.
42
+ In some cases, the following error is output.
33
43
 
34
44
  ```
35
45
  Too many requested! Retry later.
@@ -10,12 +10,12 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = "Are your gems alive?"
12
12
  spec.description = "bundler-alive reports gems are archived or not."
13
- spec.homepage = "https://github.com/kyoshidajp/bundler-audit"
13
+ spec.homepage = "https://github.com/kyoshidajp/bundler-alive"
14
14
  spec.required_ruby_version = ">= 2.6.0"
15
15
 
16
- spec.metadata["homepage_uri"] = "https://github.com/kyoshidajp/bundler-audit"
17
- spec.metadata["source_code_uri"] = "https://github.com/kyoshidajp/bundler-audit"
18
- spec.metadata["changelog_uri"] = "https://github.com/kyoshidajp/bundler-audit"
16
+ spec.metadata["homepage_uri"] = "https://github.com/kyoshidajp/bundler-alive"
17
+ spec.metadata["source_code_uri"] = "https://github.com/kyoshidajp/bundler-alive"
18
+ spec.metadata["changelog_uri"] = "https://github.com/kyoshidajp/bundler-alive"
19
19
 
20
20
  spec.files = Dir.chdir(__dir__) do
21
21
  `git ls-files -z`.split("\x0").reject do |f|
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+
5
+ module Bundler
6
+ module Alive
7
+ #
8
+ # Announces check progress
9
+ #
10
+ class Announcer
11
+ DOT = "."
12
+
13
+ private_constant :DOT
14
+
15
+ #
16
+ # A new instance of Reporter
17
+ #
18
+ def initialize
19
+ @output = $stdout
20
+ end
21
+
22
+ def announce
23
+ output.write DOT
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :output
29
+ end
30
+ end
31
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/alive"
4
4
  require "bundler/alive/doctor"
5
+ require "bundler/alive/reportable"
5
6
 
6
7
  require "thor"
7
8
 
@@ -17,19 +18,17 @@ module Bundler
17
18
  desc "check [DIR]", "Checks the Gemfile.lock"
18
19
  method_option :gemfile_lock, type: :string, aliases: "-G",
19
20
  default: "Gemfile.lock"
21
+ method_option :result, type: :string, aliases: "-r",
22
+ default: "result.toml"
20
23
 
21
24
  def check(_dir = Dir.pwd)
22
- doctor = check_by_doctor
25
+ extend Reportable
26
+ report = check_by_doctor
27
+ report.save_as_file(options[:result])
28
+ print_report(report)
23
29
 
24
- if doctor.rate_limit_exceeded_error
25
- puts "Too many requested! Retry later."
26
- exit 1
27
- end
28
-
29
- exit 0 if doctor.all_alive
30
-
31
- puts "Not alive gems are found!"
32
- exit 1
30
+ exit_status = report.result.all_alive? ? 0 : 1
31
+ exit exit_status
33
32
  end
34
33
 
35
34
  desc "version", "Prints the bundler-alive version"
@@ -41,15 +40,12 @@ module Bundler
41
40
 
42
41
  def check_by_doctor
43
42
  doctor = begin
44
- Doctor.new(options[:gemfile_lock])
43
+ Doctor.new(options[:gemfile_lock], options[:result])
45
44
  rescue Bundler::GemfileLockNotFound
46
45
  exit 1
47
46
  end
48
47
 
49
48
  doctor.diagnose
50
- doctor.report
51
- doctor.save_as_file
52
- doctor
53
49
  end
54
50
  end
55
51
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Bundler
7
+ module Alive
8
+ module Client
9
+ #
10
+ # RubyGems.org API Client
11
+ #
12
+ # @see https://guides.rubygems.org/rubygems-org-api/
13
+ #
14
+ class GemsApiClient
15
+ #
16
+ # Not found in rubygems.org error
17
+ #
18
+ class NotFound < StandardError
19
+ end
20
+
21
+ def initialize
22
+ @error_messages = []
23
+ end
24
+
25
+ #
26
+ # Gets gems from RubyGems.org
27
+ #
28
+ # @param [Array<String>] gem_names
29
+ #
30
+ # @return [Client::GemsApiResponse]
31
+ #
32
+ def gems_api_response(gem_names, &block)
33
+ urls = service_with_urls(gem_names, &block)
34
+ $stdout.puts <<~MESSAGE
35
+
36
+ Get all source code repository URLs of gems are done!
37
+ MESSAGE
38
+ Client::GemsApiResponse.new(
39
+ service_with_urls: urls,
40
+ error_messages: error_messages
41
+ )
42
+ end
43
+
44
+ private
45
+
46
+ attr_accessor :error_messages
47
+
48
+ def api_url(gem_name)
49
+ "https://rubygems.org/api/v1/gems/#{gem_name}.json"
50
+ end
51
+
52
+ def connection
53
+ Faraday.new do |connection|
54
+ connection.adapter :net_http
55
+ end
56
+ end
57
+
58
+ def service_with_urls(gem_names, &block)
59
+ urls = get_repository_urls(gem_names, &block)
60
+ urls.each_with_object({}) do |url, hash|
61
+ service_name = url.service_name
62
+ hash[service_name] = Array(hash[service_name]) << url
63
+ end
64
+ end
65
+
66
+ #
67
+ # Returns repository url
68
+ #
69
+ # @param [String] gem_name
70
+ #
71
+ # @return [SourceCodeRepositoryUrl]
72
+ #
73
+ def get_repository_url(gem_name)
74
+ url = api_url(gem_name)
75
+ response = connection.get(url)
76
+
77
+ raise NotFound, "Gem: #{gem_name} is not found in RubyGems.org." unless response.success?
78
+
79
+ body = JSON.parse(response.body)
80
+ raw_url = source_code_url(body: body, gem_name: gem_name)
81
+ SourceCodeRepositoryUrl.new(raw_url, gem_name)
82
+ end
83
+
84
+ def source_code_url(body:, gem_name:)
85
+ url = body["source_code_uri"]
86
+ return url if SourceCodeRepositoryUrl.support_url?(url)
87
+
88
+ url = body["homepage_uri"]
89
+ return url if SourceCodeRepositoryUrl.support_url?(url)
90
+
91
+ raise NotFound, "[#{gem_name}] source code repository is not found in RubyGems.org."
92
+ end
93
+
94
+ def get_repository_urls(gem_names)
95
+ result = gem_names.map do |gem_name|
96
+ yield if block_given?
97
+ get_repository_url(gem_name)
98
+ rescue StandardError => e
99
+ $stdout.write "W"
100
+ error_messages << e.message
101
+ end
102
+
103
+ result.find_all { |obj| obj.instance_of?(Alive::SourceCodeRepositoryUrl) }
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Alive
5
+ module Client
6
+ #
7
+ # Represents API Response of RubyGems.org
8
+ #
9
+ class GemsApiResponse
10
+ attr_reader :service_with_urls, :error_messages
11
+
12
+ #
13
+ # Creates a new StatusResult instance
14
+ #
15
+ # @param [StatusCollection|nil] :collection
16
+ # @param [Array] :error_messages
17
+ #
18
+ # @return [GemsApiResponse]
19
+ #
20
+ def initialize(service_with_urls:, error_messages:)
21
+ @service_with_urls = service_with_urls
22
+ @error_messages = error_messages
23
+
24
+ freeze
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -9,9 +9,36 @@ module Bundler
9
9
  #
10
10
  # API Client for GitHub API
11
11
  #
12
+ # @see https://docs.github.com/en/rest/search#search-repositories
13
+ #
12
14
  module GitHubApi
15
+ # Environment variable name of GitHub Access Token
13
16
  ACCESS_TOKEN_ENV_NAME = "BUNDLER_ALIVE_GITHUB_TOKEN"
14
17
 
18
+ # Separator of query condition
19
+ QUERY_CONDITION_SEPARATOR = " "
20
+
21
+ # Number of attempts to request after too many requests
22
+ RETRIES_ON_TOO_MANY_REQUESTS = 3
23
+
24
+ # Interval second when retrying request
25
+ RETRY_INTERVAL_SEC_ON_TOO_MANY_REQUESTS = 120
26
+
27
+ #
28
+ # Max number of conditional operator at once
29
+ #
30
+ # @see https://docs.github.com/en/rest/search#limitations-on-query-length
31
+ QUERY_MAX_OPERATORS_AT_ONCE = 6
32
+
33
+ private_constant :QUERY_MAX_OPERATORS_AT_ONCE
34
+
35
+ def self.extended(base)
36
+ base.instance_eval do
37
+ @rate_limit_exceeded = false
38
+ @retries_on_too_many_requests = 0
39
+ end
40
+ end
41
+
15
42
  #
16
43
  # Creates a GitHub client
17
44
  #
@@ -22,39 +49,112 @@ module Bundler
22
49
  end
23
50
 
24
51
  #
25
- # Returns repository URL is archived?
52
+ # Query repository statuses
53
+ #
54
+ # @param [Array<RepositoryUrl>] :urls
55
+ #
56
+ # @return [StatusResult]
57
+ #
58
+ # rubocop:disable Metrics/MethodLength
59
+ def query(urls:)
60
+ collection = StatusCollection.new
61
+ name_with_archived = get_name_with_statuses(urls)
62
+ urls.each do |url|
63
+ yield if block_given?
64
+
65
+ gem_name = url.gem_name
66
+ alive = !name_with_archived[gem_name]
67
+ status = Status.new(name: gem_name, repository_url: url, alive: alive, checked_at: Time.now)
68
+ collection = collection.add(gem_name, status)
69
+ end
70
+
71
+ StatusResult.new(collection: collection, error_messages: @error_messages,
72
+ rate_limit_exceeded: @rate_limit_exceeded)
73
+ end
74
+ # rubocop:enable Metrics/MethodLength
75
+
76
+ private
77
+
78
+ #
79
+ # Search status of repositories
26
80
  #
27
- # @param [SourceCodeRepositoryUrl] repository_url
81
+ # @param [Array<RepositoryUrl>] urls
28
82
  #
29
- # @raise [ArgumentError]
30
- # when repository_uri is not `SourceCodeRepositoryUrl`
83
+ # @return [Hash<String, Boolean>]
84
+ # gem name with archived or not
85
+ #
86
+ # rubocop:disable Metrics/MethodLength
87
+ def get_name_with_statuses(urls)
88
+ raise ArgumentError unless urls.instance_of?(Array)
89
+
90
+ name_with_status = {}
91
+ urls.each_slice(QUERY_MAX_OPERATORS_AT_ONCE) do |sliced_urls|
92
+ q = search_query(sliced_urls)
93
+ repositories = search_repositories_with_retry(q)
94
+ next if repositories.nil?
95
+
96
+ repositories.each do |repository|
97
+ name = repository["name"]
98
+ name_with_status[name] = repository["archived"]
99
+ end
100
+ end
101
+ name_with_status
102
+ end
103
+ # rubocop:enable Metrics/MethodLength
104
+
105
+ #
106
+ # Search query of repositories
107
+ #
108
+ # @param [Array<RepositoryUrl>] urls
109
+ #
110
+ # @return [String]
111
+ #
112
+ def search_query(urls)
113
+ urls.map do |url|
114
+ "repo:#{slug(url.url)}"
115
+ end.join(QUERY_CONDITION_SEPARATOR)
116
+ end
117
+
118
+ #
119
+ # Search repositories
120
+ #
121
+ # @param [String] query
31
122
  #
32
123
  # @raise [Octokit::TooManyRequests]
33
124
  # when too many requested to GitHub.com
34
- #
35
125
  # @raise [SourceCodeClient::SearchRepositoryError]
36
126
  # when Error without `Octokit::TooManyRequests`
37
127
  #
38
- # @return [Boolean]
128
+ # @return [Array<Sawyer::Resource>|nil]
39
129
  #
40
- # rubocop:disable Metrics/MethodLength
41
- def archived?(repository_url)
42
- unless repository_url.instance_of?(SourceCodeRepositoryUrl)
43
- raise ArgumentError, "UnSupported url: #{repository_url}"
130
+ def search_repositories(query)
131
+ result = @client.search_repositories(query)
132
+ result[:items]
133
+ rescue Octokit::TooManyRequests => e
134
+ raise e
135
+ rescue StandardError => e
136
+ @error_messages << e.message
137
+ []
138
+ end
139
+
140
+ def search_repositories_with_retry(query)
141
+ search_repositories(query)
142
+ rescue Octokit::TooManyRequests
143
+ if @retries_on_too_many_requests < RETRIES_ON_TOO_MANY_REQUESTS
144
+ @retries_on_too_many_requests += 1
145
+ sleep_with_message
146
+ retry
44
147
  end
45
148
 
46
- query = "repo:#{slug(repository_url.url)}"
149
+ @rate_limit_exceeded = true
150
+ []
151
+ end
47
152
 
48
- begin
49
- result = @client.search_repositories(query)
50
- result[:items][0][:archived]
51
- rescue Octokit::TooManyRequests => e
52
- raise SourceCodeClient::RateLimitExceededError, e.message
53
- rescue StandardError => e
54
- raise SourceCodeClient::SearchRepositoryError, e.message
55
- end
153
+ def sleep_with_message
154
+ puts "Too many requested. Sleep #{RETRY_INTERVAL_SEC_ON_TOO_MANY_REQUESTS} sec."
155
+ sleep RETRY_INTERVAL_SEC_ON_TOO_MANY_REQUESTS
156
+ puts "Retry request (#{@retries_on_too_many_requests}/#{RETRIES_ON_TOO_MANY_REQUESTS})"
56
157
  end
57
- # rubocop:enable Metrics/MethodLength
58
158
 
59
159
  #
60
160
  # Returns slug of repository URL
@@ -7,9 +7,11 @@ module Bundler
7
7
  # Represents a source code client
8
8
  #
9
9
  class SourceCodeClient
10
+ # Error of searching repository
10
11
  class SearchRepositoryError < StandardError
11
12
  end
12
13
 
14
+ # Error of rate limit exceeded
13
15
  class RateLimitExceededError < StandardError
14
16
  end
15
17
 
@@ -31,10 +33,11 @@ module Bundler
31
33
  def initialize(service_name:)
32
34
  raise ArgumentError, "Unknown service: #{service_name}" unless SERVICE_WITH_STRATEGIES.key?(service_name)
33
35
 
34
- service = SERVICE_WITH_STRATEGIES[service_name]
35
- extend service
36
+ strategy = SERVICE_WITH_STRATEGIES[service_name]
37
+ extend strategy
36
38
 
37
39
  @client = create_client
40
+ @error_messages = []
38
41
 
39
42
  super()
40
43
  end