still_active 0.5.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 +4 -4
- data/.github/dependabot.yml +21 -0
- data/.github/workflows/codeql-analysis.yml +5 -36
- data/.github/workflows/publish.yml +19 -0
- data/.github/workflows/rspec.yml +3 -3
- data/.github/workflows/rubocop-analysis.yml +6 -15
- data/.gitignore +6 -0
- data/.rspec +0 -2
- data/.rubocop.yml +7 -3
- data/CHANGELOG.md +51 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +192 -154
- data/README.md +67 -28
- data/fixtures/debug_versions.json +38 -0
- data/fixtures/still_active_version.json +9 -0
- data/fixtures/vcr_cassettes/deps_dev_project.yml +46 -0
- data/fixtures/vcr_cassettes/deps_dev_version.yml +56 -0
- data/fixtures/vcr_cassettes/gems.yml +2028 -963
- data/lib/helpers/activity_helper.rb +31 -0
- data/lib/helpers/ansi_helper.rb +25 -0
- data/lib/helpers/emoji_helper.rb +7 -16
- data/lib/helpers/http_helper.rb +31 -0
- data/lib/helpers/markdown_helper.rb +66 -55
- data/lib/helpers/terminal_helper.rb +118 -0
- data/lib/helpers/version_helper.rb +21 -19
- data/lib/still_active/cli.rb +46 -19
- data/lib/still_active/config.rb +10 -2
- data/lib/still_active/core_ext.rb +13 -0
- data/lib/still_active/deps_dev_client.rb +57 -0
- data/lib/still_active/gitlab_client.rb +30 -0
- data/lib/still_active/options.rb +19 -5
- data/lib/still_active/repository.rb +7 -14
- data/lib/still_active/version.rb +1 -1
- data/lib/still_active/workflow.rb +32 -24
- data/still_active.gemspec +11 -17
- metadata +30 -45
- data/lib/still_active/gemfile.rb +0 -14
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../still_active/core_ext"
|
|
4
|
+
|
|
5
|
+
module StillActive
|
|
6
|
+
module ActivityHelper
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
using StillActive::CoreExt
|
|
10
|
+
|
|
11
|
+
# Returns :ok, :stale, :critical, or :unknown
|
|
12
|
+
def activity_level(gem_data)
|
|
13
|
+
most_recent = [
|
|
14
|
+
gem_data[:last_commit_date],
|
|
15
|
+
gem_data[:latest_version_release_date],
|
|
16
|
+
gem_data[:latest_pre_release_version_release_date],
|
|
17
|
+
].compact.max
|
|
18
|
+
|
|
19
|
+
return :unknown if most_recent.nil?
|
|
20
|
+
|
|
21
|
+
config = StillActive.config
|
|
22
|
+
if most_recent >= config.no_warning_range_end.years.ago
|
|
23
|
+
:ok
|
|
24
|
+
elsif most_recent >= config.warning_range_end.years.ago
|
|
25
|
+
:stale
|
|
26
|
+
else
|
|
27
|
+
:critical
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StillActive
|
|
4
|
+
module AnsiHelper
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
RESET = "\e[0m"
|
|
8
|
+
ANSI_PATTERN = /\e\[[0-9;]*m/
|
|
9
|
+
|
|
10
|
+
def green(text) = "\e[32m#{text}#{RESET}"
|
|
11
|
+
def yellow(text) = "\e[33m#{text}#{RESET}"
|
|
12
|
+
def red(text) = "\e[31m#{text}#{RESET}"
|
|
13
|
+
def dim(text) = "\e[2m#{text}#{RESET}"
|
|
14
|
+
def bold(text) = "\e[1m#{text}#{RESET}"
|
|
15
|
+
|
|
16
|
+
def visible_length(text)
|
|
17
|
+
text.gsub(ANSI_PATTERN, "").length
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def pad(text, width)
|
|
21
|
+
padding = width - visible_length(text)
|
|
22
|
+
padding > 0 ? "#{text}#{" " * padding}" : text
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/helpers/emoji_helper.rb
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "active_support/core_ext/integer/time"
|
|
3
|
+
require_relative "activity_helper"
|
|
5
4
|
|
|
6
5
|
module StillActive
|
|
7
6
|
module EmojiHelper
|
|
8
7
|
extend self
|
|
9
|
-
def inactive_gem_emoji(result_hash)
|
|
10
|
-
most_recent_activity = [
|
|
11
|
-
result_hash.dig(:last_commit_date),
|
|
12
|
-
result_hash.dig(:latest_version_release_date),
|
|
13
|
-
result_hash.dig(:latest_pre_release_version_release_date),
|
|
14
|
-
].compact.sort.last
|
|
15
|
-
return StillActive.config.unsure_emoji if most_recent_activity.nil?
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
when
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
StillActive.config.critical_warning_emoji
|
|
9
|
+
def inactive_gem_emoji(result_hash)
|
|
10
|
+
case ActivityHelper.activity_level(result_hash)
|
|
11
|
+
when :ok then ""
|
|
12
|
+
when :stale then StillActive.config.warning_emoji
|
|
13
|
+
when :critical then StillActive.config.critical_warning_emoji
|
|
14
|
+
when :unknown then StillActive.config.unsure_emoji
|
|
24
15
|
end
|
|
25
16
|
end
|
|
26
17
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module StillActive
|
|
7
|
+
module HttpHelper
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def get_json(base_uri, path, headers: {}, params: {})
|
|
11
|
+
uri = base_uri.dup
|
|
12
|
+
uri.path = path
|
|
13
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
14
|
+
|
|
15
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
16
|
+
http.use_ssl = true
|
|
17
|
+
http.open_timeout = 10
|
|
18
|
+
http.read_timeout = 10
|
|
19
|
+
|
|
20
|
+
request = Net::HTTP::Get.new(uri)
|
|
21
|
+
headers.each { |key, value| request[key] = value }
|
|
22
|
+
|
|
23
|
+
response = http.request(request)
|
|
24
|
+
return unless response.is_a?(Net::HTTPSuccess)
|
|
25
|
+
|
|
26
|
+
JSON.parse(response.body)
|
|
27
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED, JSON::ParserError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -3,78 +3,89 @@
|
|
|
3
3
|
module StillActive
|
|
4
4
|
module MarkdownHelper
|
|
5
5
|
extend self
|
|
6
|
+
|
|
6
7
|
def markdown_table_header_line
|
|
7
|
-
"|
|
|
8
|
-
"|
|
|
8
|
+
"| activity | up to date? | OpenSSF | vulns | name | version used | latest version | latest pre-release | last commit |\n" \
|
|
9
|
+
"| -------- | ----------- | ------- | ----- | ---- | ------------ | -------------- | ------------------ | ----------- |"
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def markdown_table_body_line(gem_name:, data:)
|
|
12
13
|
repository_url = data[:repository_url]
|
|
13
14
|
ruby_gems_url = data[:ruby_gems_url]
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if version_used && ruby_gems_url
|
|
18
|
-
"#{ruby_gems_url}/versions/#{version_used}"
|
|
19
|
-
end
|
|
20
|
-
version_used_release_date = data.dig(:version_used_release_date)
|
|
21
|
-
|
|
22
|
-
latest_version = data[:latest_version]
|
|
23
|
-
latest_version_url =
|
|
24
|
-
if latest_version && ruby_gems_url
|
|
25
|
-
"#{ruby_gems_url}/versions/#{latest_version}"
|
|
26
|
-
end
|
|
27
|
-
latest_version_release_date = data.dig(:latest_version_release_date)
|
|
28
|
-
|
|
29
|
-
latest_version_prerelease = data.dig(:latest_pre_release_version)
|
|
30
|
-
latest_version_prerelease_url =
|
|
31
|
-
if latest_version_prerelease && ruby_gems_url
|
|
32
|
-
"#{ruby_gems_url}/versions/#{latest_version_prerelease}"
|
|
33
|
-
end
|
|
34
|
-
latest_version_prerelease_date = data.dig(:latest_pre_release_version_release_date)
|
|
35
|
-
|
|
36
|
-
last_commit_date = data.dig(:last_commit_date)
|
|
37
|
-
last_commit_url = repository_url
|
|
38
|
-
|
|
39
|
-
inactive_repository_emoji = data.dig(:last_activity_warning_emoj)
|
|
40
|
-
using_latest_version_emoji = data.dig(:up_to_date_emoji)
|
|
16
|
+
inactive_repository_emoji = data[:last_activity_warning_emoji]
|
|
17
|
+
using_latest_version_emoji = data[:up_to_date_emoji]
|
|
41
18
|
|
|
42
19
|
formatted_name = markdown_url(text: gem_name, url: repository_url)
|
|
43
20
|
|
|
44
|
-
formatted_version_used =
|
|
45
|
-
|
|
21
|
+
formatted_version_used = version_with_date(
|
|
22
|
+
text: data[:version_used],
|
|
23
|
+
url: version_url(ruby_gems_url, data[:version_used]),
|
|
24
|
+
date: data[:version_used_release_date],
|
|
25
|
+
)
|
|
46
26
|
|
|
47
|
-
|
|
48
|
-
|
|
27
|
+
formatted_latest_version = version_with_date(
|
|
28
|
+
text: data[:latest_version],
|
|
29
|
+
url: version_url(ruby_gems_url, data[:latest_version]),
|
|
30
|
+
date: data[:latest_version_release_date],
|
|
31
|
+
)
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
text:
|
|
52
|
-
url:
|
|
33
|
+
formatted_latest_pre_release = version_with_date(
|
|
34
|
+
text: data[:latest_pre_release_version],
|
|
35
|
+
url: version_url(ruby_gems_url, data[:latest_pre_release_version]),
|
|
36
|
+
date: data[:latest_pre_release_version_release_date],
|
|
53
37
|
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"| #{formatted_markdown_table_line} |"
|
|
38
|
+
|
|
39
|
+
formatted_last_commit = markdown_url(text: year_month(data[:last_commit_date]), url: repository_url)
|
|
40
|
+
|
|
41
|
+
unsure = StillActive.config.unsure_emoji
|
|
42
|
+
|
|
43
|
+
cells = [
|
|
44
|
+
inactive_repository_emoji || unsure,
|
|
45
|
+
using_latest_version_emoji || unsure,
|
|
46
|
+
format_scorecard(data[:scorecard_score]),
|
|
47
|
+
format_vulns(data[:vulnerability_count]),
|
|
48
|
+
formatted_name,
|
|
49
|
+
formatted_version_used || unsure,
|
|
50
|
+
formatted_latest_version || unsure,
|
|
51
|
+
formatted_latest_pre_release || unsure,
|
|
52
|
+
formatted_last_commit || unsure,
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
"| #{cells.join(" | ")} |"
|
|
74
56
|
end
|
|
75
57
|
|
|
76
58
|
private
|
|
77
59
|
|
|
60
|
+
def version_with_date(text:, url:, date:)
|
|
61
|
+
version_part = markdown_url(text: text, url: url)
|
|
62
|
+
return if version_part.nil?
|
|
63
|
+
|
|
64
|
+
date_part = year_month(date)
|
|
65
|
+
return version_part unless date_part
|
|
66
|
+
|
|
67
|
+
"#{version_part} (#{date_part})"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def version_url(ruby_gems_url, version)
|
|
71
|
+
return if ruby_gems_url.nil? || version.nil?
|
|
72
|
+
|
|
73
|
+
"#{ruby_gems_url}/versions/#{version}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def format_scorecard(score)
|
|
77
|
+
return StillActive.config.unsure_emoji if score.nil?
|
|
78
|
+
|
|
79
|
+
"#{score}/10"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def format_vulns(count)
|
|
83
|
+
return StillActive.config.unsure_emoji if count.nil?
|
|
84
|
+
return StillActive.config.success_emoji if count.zero?
|
|
85
|
+
|
|
86
|
+
count.to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
78
89
|
def markdown_url(text:, url:)
|
|
79
90
|
return text if url.nil?
|
|
80
91
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "activity_helper"
|
|
4
|
+
require_relative "ansi_helper"
|
|
5
|
+
require_relative "version_helper"
|
|
6
|
+
|
|
7
|
+
module StillActive
|
|
8
|
+
module TerminalHelper
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
HEADERS = ["Name", "Version", "Activity", "OpenSSF", "Vulns"].freeze
|
|
12
|
+
|
|
13
|
+
def render(result)
|
|
14
|
+
rows = result.keys.sort.map { |name| build_row(name, result[name]) }
|
|
15
|
+
widths = column_widths(rows)
|
|
16
|
+
|
|
17
|
+
lines = []
|
|
18
|
+
lines << header_line(widths)
|
|
19
|
+
lines << separator_line(widths)
|
|
20
|
+
rows.each { |row| lines << row_line(row, widths) }
|
|
21
|
+
lines << ""
|
|
22
|
+
lines << summary_line(result)
|
|
23
|
+
lines.join("\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_row(name, data)
|
|
29
|
+
[
|
|
30
|
+
name,
|
|
31
|
+
format_version(data),
|
|
32
|
+
format_activity(data),
|
|
33
|
+
format_scorecard(data[:scorecard_score]),
|
|
34
|
+
format_vulns(data[:vulnerability_count]),
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def format_version(data)
|
|
39
|
+
used = data[:version_used]
|
|
40
|
+
latest = data[:latest_version]
|
|
41
|
+
return AnsiHelper.dim("-") if used.nil? && latest.nil?
|
|
42
|
+
|
|
43
|
+
if VersionHelper.up_to_date(version_used: used, latest_version: latest)
|
|
44
|
+
AnsiHelper.green("#{used} (latest)")
|
|
45
|
+
elsif latest
|
|
46
|
+
AnsiHelper.yellow("#{used} → #{latest}")
|
|
47
|
+
else
|
|
48
|
+
used.to_s
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def format_activity(data)
|
|
53
|
+
case ActivityHelper.activity_level(data)
|
|
54
|
+
when :ok then AnsiHelper.green("ok")
|
|
55
|
+
when :stale then AnsiHelper.yellow("stale")
|
|
56
|
+
when :critical then AnsiHelper.red("critical")
|
|
57
|
+
when :unknown then AnsiHelper.dim("-")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def format_scorecard(score)
|
|
62
|
+
score.nil? ? AnsiHelper.dim("-") : "#{score}/10"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def format_vulns(count)
|
|
66
|
+
return AnsiHelper.dim("-") if count.nil?
|
|
67
|
+
return AnsiHelper.green("0") if count.zero?
|
|
68
|
+
|
|
69
|
+
AnsiHelper.red(count.to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def column_widths(rows)
|
|
73
|
+
return HEADERS.map { |h| h.length + 2 } if rows.empty?
|
|
74
|
+
|
|
75
|
+
HEADERS.zip(rows.transpose).map do |header, cells|
|
|
76
|
+
widths = cells.map { AnsiHelper.visible_length(_1) }
|
|
77
|
+
[header.length, *widths].max + 2
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def header_line(widths)
|
|
82
|
+
HEADERS.zip(widths)
|
|
83
|
+
.map { |h, w| AnsiHelper.pad(AnsiHelper.bold(h), w) }
|
|
84
|
+
.join
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def separator_line(widths)
|
|
88
|
+
AnsiHelper.dim(widths.map { |w| "─" * w }.join)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def row_line(row, widths)
|
|
92
|
+
row.zip(widths)
|
|
93
|
+
.map { |cell, w| AnsiHelper.pad(cell, w) }
|
|
94
|
+
.join
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def summary_line(result)
|
|
98
|
+
total = result.size
|
|
99
|
+
level_counts = result.each_value.map { |d| ActivityHelper.activity_level(d) }.tally
|
|
100
|
+
version_counts = result.each_value
|
|
101
|
+
.map { |d| VersionHelper.up_to_date(version_used: d[:version_used], latest_version: d[:latest_version]) }
|
|
102
|
+
.tally
|
|
103
|
+
|
|
104
|
+
up_to_date = version_counts.fetch(true, 0)
|
|
105
|
+
outdated = version_counts.fetch(false, 0)
|
|
106
|
+
active = level_counts.fetch(:ok, 0)
|
|
107
|
+
stale = level_counts.fetch(:stale, 0) + level_counts.fetch(:critical, 0)
|
|
108
|
+
vulns = result.each_value.sum { |d| d[:vulnerability_count] || 0 }
|
|
109
|
+
|
|
110
|
+
parts = [
|
|
111
|
+
"#{total} gems: #{up_to_date} up to date, #{outdated} outdated",
|
|
112
|
+
"#{active} active, #{stale} stale",
|
|
113
|
+
"#{vulns} vulnerabilities",
|
|
114
|
+
]
|
|
115
|
+
parts.join(" · ")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
module StillActive
|
|
4
6
|
module VersionHelper
|
|
5
7
|
extend self
|
|
@@ -14,27 +16,16 @@ module StillActive
|
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
def up_to_date
|
|
18
|
-
return
|
|
19
|
+
def up_to_date(version_used:, latest_version: nil, latest_pre_release_version: nil)
|
|
20
|
+
return if latest_version.nil? && latest_pre_release_version.nil?
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else
|
|
23
|
-
gem_version(version_hash: version_used)
|
|
24
|
-
end
|
|
22
|
+
used = to_gem_version(version_used)
|
|
23
|
+
return if used.nil?
|
|
25
24
|
|
|
26
|
-
latest_version
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
latest_pre_release_version = if latest_pre_release_version.is_a?(String)
|
|
32
|
-
latest_pre_release_version
|
|
33
|
-
else
|
|
34
|
-
gem_version(version_hash: latest_pre_release_version)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
[latest_version, latest_pre_release_version].include?(version_used)
|
|
25
|
+
[latest_version, latest_pre_release_version]
|
|
26
|
+
.compact
|
|
27
|
+
.filter_map { |v| to_gem_version(v) }
|
|
28
|
+
.any? { |v| used >= v }
|
|
38
29
|
end
|
|
39
30
|
|
|
40
31
|
def gem_version(version_hash:)
|
|
@@ -46,5 +37,16 @@ module StillActive
|
|
|
46
37
|
|
|
47
38
|
Time.parse(release_date) unless release_date.nil?
|
|
48
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def normalize_version(version)
|
|
44
|
+
version.is_a?(String) ? version : gem_version(version_hash: version)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_gem_version(version)
|
|
48
|
+
str = normalize_version(version)
|
|
49
|
+
Gem::Version.new(str) if str
|
|
50
|
+
end
|
|
49
51
|
end
|
|
50
52
|
end
|
data/lib/still_active/cli.rb
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "options"
|
|
4
|
+
require_relative "../helpers/activity_helper"
|
|
4
5
|
require_relative "../helpers/bundler_helper"
|
|
5
6
|
require_relative "../helpers/emoji_helper"
|
|
6
7
|
require_relative "../helpers/markdown_helper"
|
|
8
|
+
require_relative "../helpers/terminal_helper"
|
|
7
9
|
require_relative "../helpers/version_helper"
|
|
8
10
|
require_relative "workflow"
|
|
9
|
-
# require "cli/ui"
|
|
10
11
|
|
|
11
12
|
module StillActive
|
|
12
13
|
class CLI
|
|
13
|
-
include VersionHelper
|
|
14
14
|
def run(args)
|
|
15
15
|
options = Options.new.parse!(args)
|
|
16
16
|
if options[:provided_gems]
|
|
@@ -20,27 +20,54 @@ module StillActive
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
result = Workflow.call
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
case resolve_format
|
|
24
25
|
when :json
|
|
25
26
|
puts result.to_json
|
|
27
|
+
when :terminal
|
|
28
|
+
puts TerminalHelper.render(result)
|
|
26
29
|
when :markdown
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
render_markdown(result)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
check_exit_status(result)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def resolve_format
|
|
39
|
+
format = StillActive.config.output_format
|
|
40
|
+
return format unless format == :auto
|
|
41
|
+
|
|
42
|
+
$stdout.tty? ? :terminal : :json
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_markdown(result)
|
|
46
|
+
puts MarkdownHelper.markdown_table_header_line
|
|
47
|
+
result.keys.sort.each do |name|
|
|
48
|
+
gem_data = result[name]
|
|
49
|
+
gem_data[:last_activity_warning_emoji] = EmojiHelper.inactive_gem_emoji(gem_data)
|
|
50
|
+
gem_data[:up_to_date_emoji] = EmojiHelper.using_latest_emoji(
|
|
51
|
+
using_last_release: VersionHelper.up_to_date(
|
|
52
|
+
version_used: gem_data[:version_used], latest_version: gem_data[:latest_version],
|
|
53
|
+
),
|
|
54
|
+
using_last_pre_release: VersionHelper.up_to_date(
|
|
55
|
+
version_used: gem_data[:version_used], latest_pre_release_version: gem_data[:latest_pre_release_version],
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
puts MarkdownHelper.markdown_table_body_line(gem_name: name, data: gem_data)
|
|
43
60
|
end
|
|
44
61
|
end
|
|
62
|
+
|
|
63
|
+
def check_exit_status(result)
|
|
64
|
+
config = StillActive.config
|
|
65
|
+
return unless config.fail_if_critical || config.fail_if_warning
|
|
66
|
+
|
|
67
|
+
levels = result.each_value.map { |gem_data| ActivityHelper.activity_level(gem_data) }
|
|
68
|
+
|
|
69
|
+
exit(1) if config.fail_if_warning && levels.intersect?([:stale, :critical])
|
|
70
|
+
exit(1) if config.fail_if_critical && levels.include?(:critical)
|
|
71
|
+
end
|
|
45
72
|
end
|
|
46
73
|
end
|
data/lib/still_active/config.rb
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler"
|
|
4
|
+
require "octokit"
|
|
4
5
|
|
|
5
6
|
module StillActive
|
|
6
7
|
class Config
|
|
7
8
|
attr_accessor :critical_warning_emoji,
|
|
9
|
+
:fail_if_critical,
|
|
10
|
+
:fail_if_warning,
|
|
8
11
|
:futurist_emoji,
|
|
9
12
|
:gemfile_path,
|
|
10
13
|
:gems,
|
|
11
14
|
:github_oauth_token,
|
|
15
|
+
:gitlab_token,
|
|
12
16
|
:output_format,
|
|
13
17
|
:parallelism,
|
|
14
18
|
:no_warning_range_end,
|
|
@@ -18,12 +22,16 @@ module StillActive
|
|
|
18
22
|
:warning_range_end
|
|
19
23
|
|
|
20
24
|
def initialize
|
|
25
|
+
@fail_if_critical = false
|
|
26
|
+
@fail_if_warning = false
|
|
21
27
|
@gemfile_path = Bundler.default_gemfile.to_s
|
|
22
28
|
@gems = []
|
|
29
|
+
@github_oauth_token = ENV["GITHUB_TOKEN"]
|
|
30
|
+
@gitlab_token = ENV["GITLAB_TOKEN"]
|
|
23
31
|
|
|
24
32
|
@parallelism = 10
|
|
25
33
|
|
|
26
|
-
@output_format = :
|
|
34
|
+
@output_format = :auto
|
|
27
35
|
|
|
28
36
|
@critical_warning_emoji = "🚩"
|
|
29
37
|
@futurist_emoji = "🔮"
|
|
@@ -37,7 +45,7 @@ module StillActive
|
|
|
37
45
|
|
|
38
46
|
def github_client
|
|
39
47
|
@github_client ||=
|
|
40
|
-
|
|
48
|
+
Octokit::Client.new(access_token: github_oauth_token)
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StillActive
|
|
4
|
+
module CoreExt
|
|
5
|
+
SECONDS_PER_YEAR = 31_556_952 # Gregorian average (365.2425 days)
|
|
6
|
+
|
|
7
|
+
refine Numeric do
|
|
8
|
+
def days = self * 86_400
|
|
9
|
+
def years = self * SECONDS_PER_YEAR
|
|
10
|
+
def ago = Time.now - self
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../helpers/http_helper"
|
|
4
|
+
|
|
5
|
+
module StillActive
|
|
6
|
+
module DepsDevClient
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
BASE_URI = URI("https://api.deps.dev/")
|
|
10
|
+
|
|
11
|
+
def version_info(gem_name:, version:)
|
|
12
|
+
return if gem_name.nil? || version.nil?
|
|
13
|
+
|
|
14
|
+
path = "/v3alpha/systems/rubygems/packages/#{encode(gem_name)}/versions/#{encode(version)}"
|
|
15
|
+
body = HttpHelper.get_json(BASE_URI, path)
|
|
16
|
+
return if body.nil?
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
advisory_keys: body.dig("advisoryKeys")&.map { |a| a["id"] } || [],
|
|
20
|
+
project_id: extract_project_id(body),
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def project_scorecard(project_id:)
|
|
25
|
+
return if project_id.nil?
|
|
26
|
+
|
|
27
|
+
path = "/v3alpha/projects/#{encode(project_id)}"
|
|
28
|
+
body = HttpHelper.get_json(BASE_URI, path)
|
|
29
|
+
return if body.nil?
|
|
30
|
+
|
|
31
|
+
scorecard = body["scorecard"]
|
|
32
|
+
return if scorecard.nil?
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
score: scorecard["overallScore"],
|
|
36
|
+
date: scorecard["date"],
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Extracts "host/owner/repo" from the SOURCE_REPO link URL.
|
|
43
|
+
# URLs may have trailing slashes or extra path segments (e.g. /tree/v1.0).
|
|
44
|
+
def extract_project_id(body)
|
|
45
|
+
url = body.dig("links")&.find { |l| l["label"] == "SOURCE_REPO" }&.dig("url")
|
|
46
|
+
return if url.nil?
|
|
47
|
+
|
|
48
|
+
path = url.delete_prefix("https://").delete_prefix("http://")
|
|
49
|
+
segments = path.split("/")
|
|
50
|
+
segments[0..2].join("/") if segments.length >= 3
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def encode(value)
|
|
54
|
+
URI.encode_www_form_component(value)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "../helpers/http_helper"
|
|
5
|
+
|
|
6
|
+
module StillActive
|
|
7
|
+
module GitlabClient
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
BASE_URI = URI("https://gitlab.com/")
|
|
11
|
+
|
|
12
|
+
def last_commit_date(owner:, name:)
|
|
13
|
+
return if owner.nil? || name.nil?
|
|
14
|
+
|
|
15
|
+
project_id = URI.encode_www_form_component("#{owner}/#{name}")
|
|
16
|
+
headers = {}
|
|
17
|
+
token = StillActive.config.gitlab_token
|
|
18
|
+
headers["PRIVATE-TOKEN"] = token if token
|
|
19
|
+
|
|
20
|
+
path = "/api/v4/projects/#{project_id}/repository/commits"
|
|
21
|
+
body = HttpHelper.get_json(BASE_URI, path, headers: headers, params: { per_page: 1 })
|
|
22
|
+
return if body.nil? || body.empty?
|
|
23
|
+
|
|
24
|
+
date = body.first["committed_date"]
|
|
25
|
+
Time.parse(date) if date
|
|
26
|
+
rescue ArgumentError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|