podrpt 0.1.0 → 1.0.1
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/.DS_Store +0 -0
- data/CHANGELOG.md +2 -2
- data/README.md +36 -10
- data/lib/podrpt/cli.rb +142 -0
- data/lib/podrpt/configuration.rb +81 -0
- data/lib/podrpt/lockfile_analyzer.rb +32 -0
- data/lib/podrpt/models.rb +14 -0
- data/lib/podrpt/report_generator.rb +35 -0
- data/lib/podrpt/slack_notifier.rb +39 -0
- data/lib/podrpt/version.rb +1 -1
- data/lib/podrpt/version_comparer.rb +18 -0
- data/lib/podrpt/version_fetcher.rb +29 -0
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b806f0000eb01912678830776a5ef3a61582d552bd0d648eb5b3cfdb3896394
|
|
4
|
+
data.tar.gz: e590ece5be99ae5d5c63df07de580009e149f03ccc06c2d838a8fe687aa6216c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bb4f467def5f841df6e848eed1f1eca29d7968763bbe65c144529c96dd77eb55e67f54e761811b6adade103cf8ab07b928d8a89901290b14bf747c15794c305
|
|
7
|
+
data.tar.gz: 1056100004bb690b1db1c591738b0c02e0e439061435dea82ccf182ff7e23d60b5da2e59af8eff04b43802c43ca54b90c22b58e09d95795037610e127a122835
|
data/.DS_Store
ADDED
|
Binary file
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -4,38 +4,55 @@ Podrpt is a command-line tool written in Ruby to analyze and report outdated Coc
|
|
|
4
4
|
|
|
5
5
|
It leverages the native CocoaPods API for high-performance analysis, avoiding slow external process calls for version fetching.
|
|
6
6
|
|
|
7
|
-
Features
|
|
8
|
-
- Fast Analysis
|
|
7
|
+
**Features**
|
|
8
|
+
- **Fast Analysis**: Uses the native CocoaPods gem API instead of shelling out to pod search.
|
|
9
9
|
|
|
10
|
-
- Outdated Pod Detection
|
|
10
|
+
- **Outdated Pod Detection**: Compares versions in your **Podfile.lock** against the latest public releases.
|
|
11
11
|
|
|
12
|
-
- Risk Assessment
|
|
12
|
+
- **Risk Assessment**: Assign custom risk scores and owner teams to dependencies via a PodsRisk.yaml file.
|
|
13
13
|
|
|
14
|
-
- Dependency Filtering
|
|
14
|
+
- **Dependency Filtering**: Use an PodsAllowlist.yaml file to filter out transitive dependencies and focus only on the pods you directly manage.
|
|
15
15
|
|
|
16
|
-
- Slack Notifications
|
|
16
|
+
- **Slack Notifications**: Delivers a clean, formatted report directly to a Slack channel, perfect for CI/CD pipelines.
|
|
17
17
|
|
|
18
|
-
- CI/CD Focused
|
|
18
|
+
- **CI/CD Focused**: Designed to run in automated environments without leaving behind unnecessary file artifacts.
|
|
19
19
|
|
|
20
20
|
- Interactive Setup: A simple init command to generate all necessary configuration files.-
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
23
23
|
|
|
24
24
|
Add this line to your application's Gemfile:
|
|
25
|
-
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'podrpt', '~> 1.0.0'
|
|
27
|
+
```
|
|
26
28
|
|
|
27
29
|
And then execute:
|
|
30
|
+
```sh
|
|
28
31
|
bundle install
|
|
32
|
+
```
|
|
29
33
|
|
|
30
34
|
## Setup
|
|
31
35
|
After installing the gem, you need to generate the configuration files in your project's root directory. Navigate to your iOS project's root folder and run the interactive setup command:
|
|
32
36
|
|
|
37
|
+
And then execute:
|
|
38
|
+
```sh
|
|
33
39
|
bundle exec podrpt init
|
|
40
|
+
```
|
|
34
41
|
|
|
35
42
|
This command will:
|
|
36
43
|
|
|
37
|
-
1 Create a sample PodsRisk.yaml file for defining risk scores.
|
|
38
|
-
|
|
44
|
+
1 Create a sample **PodsRisk.yaml** file for defining risk scores.
|
|
45
|
+
```yaml
|
|
46
|
+
default:
|
|
47
|
+
risk: 500
|
|
48
|
+
owners: []
|
|
49
|
+
pods:
|
|
50
|
+
XPTO:
|
|
51
|
+
risk: 100
|
|
52
|
+
owners: ['core-team']
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
2 Create a sample **PodsAllowlist.yaml** file for filtering dependencies.
|
|
39
56
|
3 Prompt you for your Slack Incoming Webhook URL and save it securely in a .podrpt.yml file (which should be added to your .gitignore).
|
|
40
57
|
|
|
41
58
|
After running init, customize the generated .yaml files to fit your project's needs.
|
|
@@ -43,7 +60,16 @@ After running init, customize the generated .yaml files to fit your project's ne
|
|
|
43
60
|
## Usage
|
|
44
61
|
To run an analysis and send a report to Slack, simply execute the run command:
|
|
45
62
|
|
|
63
|
+
And then execute:
|
|
64
|
+
```sh
|
|
46
65
|
bundle exec podrpt run
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If you want to see what will be sent and what URL is configured, use this command:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
bundle exec podrpt run --dry-run
|
|
72
|
+
```
|
|
47
73
|
|
|
48
74
|
The report will only include outdated pods by default.
|
|
49
75
|
|
data/lib/podrpt/cli.rb
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# lib/podrpt/cli.rb
|
|
2
|
+
module Podrpt
|
|
3
|
+
class CLI
|
|
4
|
+
def self.start(args)
|
|
5
|
+
command = args.shift || 'run'
|
|
6
|
+
case command
|
|
7
|
+
when 'run'
|
|
8
|
+
run_reporter(args)
|
|
9
|
+
when 'init'
|
|
10
|
+
initialize_configuration
|
|
11
|
+
when '--version', '-v'
|
|
12
|
+
puts Podrpt::VERSION
|
|
13
|
+
else
|
|
14
|
+
puts "Comando desconhecido: '#{command}'. Use 'run' ou 'init'."
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def self.initialize_configuration
|
|
22
|
+
puts "🚀 Iniciando a configuração do Podrpt..."
|
|
23
|
+
|
|
24
|
+
pod_names_to_configure = []
|
|
25
|
+
project_dir = Dir.pwd
|
|
26
|
+
lockfile_path = File.join(project_dir, 'Podfile.lock')
|
|
27
|
+
|
|
28
|
+
if File.exist?(lockfile_path)
|
|
29
|
+
puts "📄 `Podfile.lock` encontrado. Analisando pods para pré-popular os arquivos..."
|
|
30
|
+
begin
|
|
31
|
+
# Analisa o lockfile para obter a lista completa de pods externos
|
|
32
|
+
analyzer = Podrpt::LockfileAnalyzer.new(project_dir)
|
|
33
|
+
all_pods_versions = analyzer.pod_versions
|
|
34
|
+
classified_pods = analyzer.classify_pods
|
|
35
|
+
|
|
36
|
+
# Filtramos apenas para pods que não são de desenvolvimento local
|
|
37
|
+
external_pods_filter = classified_pods[:spec_repo].dup
|
|
38
|
+
external_pods_filter -= classified_pods[:dev_path]
|
|
39
|
+
|
|
40
|
+
pods_to_configure = all_pods_versions.slice(*external_pods_filter.to_a)
|
|
41
|
+
pod_names_to_configure = pods_to_configure.keys
|
|
42
|
+
|
|
43
|
+
rescue => e
|
|
44
|
+
puts "⚠️ Erro ao analisar o `Podfile.lock`: #{e.message}. Os arquivos serão criados com exemplos."
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
puts "⚠️ `Podfile.lock` não encontrado. Os arquivos serão criados com exemplos."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Cria os arquivos de configuração, passando a lista de pods encontrados
|
|
51
|
+
Podrpt::Configuration.create_allowlist_file(pod_names: pod_names_to_configure)
|
|
52
|
+
Podrpt::Configuration.create_risk_file(pod_names: pod_names_to_configure)
|
|
53
|
+
|
|
54
|
+
puts "\nAgora, por favor, informe a URL do seu Incoming Webhook do Slack:"
|
|
55
|
+
print "> "
|
|
56
|
+
url = $stdin.gets.chomp
|
|
57
|
+
Podrpt::Configuration.save_slack_url(url)
|
|
58
|
+
puts "\nConfiguração concluída! Edite os arquivos .yaml conforme necessário e execute 'podrpt run'."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# O método `run_reporter` e seus auxiliares permanecem os mesmos da última versão funcional
|
|
62
|
+
def self.run_reporter(args)
|
|
63
|
+
options = parse_run_options(args)
|
|
64
|
+
analyzer = Podrpt::LockfileAnalyzer.new(options.project_dir)
|
|
65
|
+
all_pods_versions = analyzer.pod_versions
|
|
66
|
+
classified_pods = analyzer.classify_pods
|
|
67
|
+
|
|
68
|
+
initial_filter = classified_pods[:spec_repo].dup
|
|
69
|
+
initial_filter -= classified_pods[:dev_path]
|
|
70
|
+
current_pods = all_pods_versions.slice(*initial_filter.to_a)
|
|
71
|
+
|
|
72
|
+
allowlist_config = load_allowlist(File.join(options.project_dir, options.allowlist_yaml))
|
|
73
|
+
pods_for_report = apply_allowlist_filter(current_pods, allowlist_config)
|
|
74
|
+
puts "[podrpt] Totais lock: #{all_pods_versions.size} | Pré-allowlist: #{current_pods.size} | Relatório final: #{pods_for_report.size}"
|
|
75
|
+
options.total_pods_count = pods_for_report.size
|
|
76
|
+
|
|
77
|
+
risk_config = load_risk_config(File.join(options.project_dir, options.risk_yaml))
|
|
78
|
+
if options.sync_risk_yaml
|
|
79
|
+
sync_risk_yaml(File.join(options.project_dir, options.risk_yaml), pods_for_report, risk_config)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
version_fetcher = Podrpt::VersionFetcher.new(options)
|
|
83
|
+
latest_versions = version_fetcher.fetch_latest_versions_in_bulk(pods_for_report.keys)
|
|
84
|
+
|
|
85
|
+
final_analysis = []
|
|
86
|
+
pods_for_report.sort_by { |name, _| name.downcase }.each do |name, version|
|
|
87
|
+
pod_risk_info = risk_config['pods'][name] || risk_config['default']
|
|
88
|
+
analysis = Podrpt::PodAnalysis.new(name: name, current_version: version, latest_version: latest_versions[name], risk: pod_risk_info['risk'], owners: pod_risk_info['owners'] || [])
|
|
89
|
+
if options.only_outdated && !is_outdated(analysis.current_version, analysis.latest_version)
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
final_analysis << analysis
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
reporter = Podrpt::ReportGenerator.new(final_analysis, options)
|
|
96
|
+
report_text = reporter.build_report_text
|
|
97
|
+
|
|
98
|
+
Podrpt::SlackNotifier.notify(options.slack_webhook_url, report_text)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.parse_run_options(args)
|
|
102
|
+
options = Podrpt::Options.new(
|
|
103
|
+
project_dir: Dir.pwd,
|
|
104
|
+
risk_yaml: 'PodsRisk.yaml',
|
|
105
|
+
allowlist_yaml: 'PodsAllowlist.yaml',
|
|
106
|
+
only_outdated: true,
|
|
107
|
+
trunk_workers: 8,
|
|
108
|
+
slack_webhook_url: ENV['SLACK_WEBHOOK_URL'] || Podrpt::Configuration.load_slack_url
|
|
109
|
+
)
|
|
110
|
+
OptionParser.new do |opts|
|
|
111
|
+
opts.banner = "Usage: podrpt run [options]"
|
|
112
|
+
opts.on("--project-dir DIR", "Diretório do projeto") { |v| options.project_dir = v }
|
|
113
|
+
opts.on("--slack-webhook-url URL", "URL do Webhook (sobrescreve config)") { |v| options.slack_webhook_url = v }
|
|
114
|
+
opts.on("--show-all", "Mostra todos os pods") { |v| options.only_outdated = false }
|
|
115
|
+
opts.on("--sync-risk-yaml", "Sincroniza PodsRisk.yaml") { |v| options.sync_risk_yaml = v }
|
|
116
|
+
end.parse!(args)
|
|
117
|
+
|
|
118
|
+
unless options.slack_webhook_url
|
|
119
|
+
puts "❌ ERRO: URL do Slack não configurada. Rode 'podrpt init'."
|
|
120
|
+
exit 1
|
|
121
|
+
end
|
|
122
|
+
options
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.load_allowlist(path); return {} unless File.exist?(path); config = YAML.load_file(path); config&.key?('allowlist') ? config['allowlist'] : {}; rescue; {}; end
|
|
126
|
+
def self.apply_allowlist_filter(all_pods, allowlist_config)
|
|
127
|
+
return all_pods if allowlist_config.nil? || allowlist_config.empty?
|
|
128
|
+
allowed_sets = allowlist_config.transform_values(&:to_set); vendor_prefixes = allowed_sets.keys
|
|
129
|
+
all_pods.select { |pod_name, _| matched_prefix = vendor_prefixes.find { |p| pod_name.start_with?(p) }; matched_prefix ? allowed_sets[matched_prefix].include?(pod_name) : true }
|
|
130
|
+
end
|
|
131
|
+
def self.load_risk_config(path)
|
|
132
|
+
return { 'default' => { 'risk' => 500, 'owners' => [] }, 'pods' => {} } unless File.exist?(path)
|
|
133
|
+
config = YAML.load_file(path) || {}; config['default'] ||= { 'risk' => 500, 'owners' => [] }; config['pods'] ||= {}; config
|
|
134
|
+
end
|
|
135
|
+
def self.sync_risk_yaml(path, pods, config)
|
|
136
|
+
pods.keys.sort_by(&:downcase).each { |name| config['pods'][name] ||= config['default'].dup }
|
|
137
|
+
File.write(path, config.to_yaml)
|
|
138
|
+
puts "[podrpt] PodsRisk.yaml sincronizado com #{config['pods'].size} pods."
|
|
139
|
+
end
|
|
140
|
+
def self.is_outdated(current, latest); latest && !latest.empty? && Podrpt::VersionComparer.compare(latest, current) > 0; end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# lib/podrpt/configuration.rb
|
|
2
|
+
require 'yaml'
|
|
3
|
+
|
|
4
|
+
module Podrpt
|
|
5
|
+
class Configuration
|
|
6
|
+
CONFIG_FILE = '.podrpt.yml'.freeze
|
|
7
|
+
RISK_FILE = 'PodsRisk.yaml'.freeze
|
|
8
|
+
ALLOWLIST_FILE = 'PodsAllowlist.yaml'.freeze
|
|
9
|
+
|
|
10
|
+
def self.save_slack_url(url)
|
|
11
|
+
config = File.exist?(CONFIG_FILE) ? YAML.load_file(CONFIG_FILE) || {} : {}
|
|
12
|
+
config['slack_webhook_url'] = url
|
|
13
|
+
File.write(CONFIG_FILE, config.to_yaml)
|
|
14
|
+
puts "✅ URL do Slack salva em #{CONFIG_FILE}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.load_slack_url
|
|
18
|
+
return nil unless File.exist?(CONFIG_FILE)
|
|
19
|
+
YAML.load_file(CONFIG_FILE)['slack_webhook_url']
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.create_risk_file(pod_names: [])
|
|
23
|
+
return if File.exist?(RISK_FILE)
|
|
24
|
+
|
|
25
|
+
bands_config = { 'green_max' => 400, 'yellow_max' => 700 }
|
|
26
|
+
default_config = { 'owners' => [], 'risk' => 500 }
|
|
27
|
+
|
|
28
|
+
pods_hash = {}
|
|
29
|
+
if pod_names.empty?
|
|
30
|
+
puts "⚠️ Nenhum pod encontrado para pré-popular. Criando arquivo de risco com um exemplo."
|
|
31
|
+
pods_hash['Firebase'] = { 'owners' => ['core-team'], 'risk' => 100 }
|
|
32
|
+
else
|
|
33
|
+
pod_names.sort_by(&:downcase).each do |name|
|
|
34
|
+
pods_hash[name] = YAML.load(default_config.to_yaml)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
final_structure = {
|
|
39
|
+
'bands' => bands_config,
|
|
40
|
+
'default' => default_config,
|
|
41
|
+
'pods' => pods_hash
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
File.write(RISK_FILE, final_structure.to_yaml)
|
|
45
|
+
puts "✅ Arquivo '#{RISK_FILE}' criado e pré-populado com #{pods_hash.count} pods."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.create_allowlist_file(pod_names: [])
|
|
49
|
+
return if File.exist?(ALLOWLIST_FILE)
|
|
50
|
+
|
|
51
|
+
header = <<~YAML
|
|
52
|
+
# The allowlist is used to filter transitive dependencies (sub-dependencies)
|
|
53
|
+
# and focus only on the pods you manage directly in your Podfile.
|
|
54
|
+
#
|
|
55
|
+
# For each "group" (e.g., Firebase), only the pods listed here will appear in the report.
|
|
56
|
+
# Pods that don't belong to any group (e.g., Alamofire) will appear by default.
|
|
57
|
+
#
|
|
58
|
+
# Uncomment and adjust the examples below, or create your own groups.
|
|
59
|
+
YAML
|
|
60
|
+
|
|
61
|
+
example_content = {
|
|
62
|
+
'allowlist' => {
|
|
63
|
+
'Firebase' => ['FirebaseAnalytics', 'FirebaseCrashlytics']
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
project_pods_comment = pod_names.sort_by(&:downcase).map { |name| "# - #{name}" }.join("\n")
|
|
68
|
+
|
|
69
|
+
final_content = header + example_content.to_yaml
|
|
70
|
+
unless pod_names.empty?
|
|
71
|
+
final_content += "\n# --- Pods Encontrados no seu Projeto (descomente para usar) ---\n"
|
|
72
|
+
final_content += "# allowlist:\n"
|
|
73
|
+
final_content += "# MeuGrupo:\n"
|
|
74
|
+
final_content += project_pods_comment
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
File.write(ALLOWLIST_FILE, final_content)
|
|
78
|
+
puts "✅ Arquivo de exemplo '#{ALLOWLIST_FILE}' criado."
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Podrpt
|
|
2
|
+
class LockfileAnalyzer
|
|
3
|
+
def initialize(project_dir)
|
|
4
|
+
lockfile_path = File.join(project_dir, 'Podfile.lock')
|
|
5
|
+
raise "ERRO: #{lockfile_path} not found." unless File.exist?(lockfile_path)
|
|
6
|
+
@lockfile = Pod::Lockfile.from_file(Pathname.new(lockfile_path))
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def pod_versions
|
|
10
|
+
pod_versions_map = {}
|
|
11
|
+
@lockfile.pod_names.each do |pod_name|
|
|
12
|
+
root_name = Pod::Specification.root_name(pod_name)
|
|
13
|
+
pod_versions_map[root_name] ||= @lockfile.version(pod_name).to_s
|
|
14
|
+
end
|
|
15
|
+
pod_versions_map
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def classify_pods
|
|
19
|
+
lockfile_hash = @lockfile.to_hash
|
|
20
|
+
all_pods = (@lockfile.pod_names || []).map { |n| Pod::Specification.root_name(n) }.to_set
|
|
21
|
+
git_pods = Set.new
|
|
22
|
+
dev_pods = Set.new
|
|
23
|
+
(lockfile_hash['EXTERNAL SOURCES'] || {}).each do |name, details|
|
|
24
|
+
root_name = Pod::Specification.root_name(name)
|
|
25
|
+
git_pods.add(root_name) if details.key?(:git)
|
|
26
|
+
dev_pods.add(root_name) if details.key?(:path)
|
|
27
|
+
end
|
|
28
|
+
spec_repo_pods = all_pods - git_pods - dev_pods
|
|
29
|
+
{ spec_repo: spec_repo_pods, git_source: git_pods, dev_path: dev_pods }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# lib/podrpt/models.rb
|
|
2
|
+
|
|
3
|
+
module Podrpt
|
|
4
|
+
Options = Struct.new(
|
|
5
|
+
:project_dir, :risk_yaml, :allowlist_yaml, :trunk_workers,
|
|
6
|
+
:only_outdated, :sync_risk_yaml, :total_pods_count, :slack_webhook_url,
|
|
7
|
+
:dry_run,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
)
|
|
10
|
+
PodAnalysis = Struct.new(
|
|
11
|
+
:name, :current_version, :latest_version,
|
|
12
|
+
:risk, :owners, keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Podrpt
|
|
2
|
+
class ReportGenerator
|
|
3
|
+
def initialize(pods_analysis, options)
|
|
4
|
+
@pods = pods_analysis
|
|
5
|
+
@options = options
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def build_report_text
|
|
9
|
+
outdated_count = @pods.count { |p| is_outdated?(p.current_version, p.latest_version) }
|
|
10
|
+
total_pods_in_report = @options.total_pods_count
|
|
11
|
+
header = "_Generated: #{Time.now.utc.iso8601}_\n" \
|
|
12
|
+
"_Outdated: #{outdated_count}/#{total_pods_in_report}_"
|
|
13
|
+
|
|
14
|
+
h_pod, h_ver, h_risk, h_owners = "Pod", "Versions", "Risk", "Owners"
|
|
15
|
+
pod_names = @pods.map(&:name); versions = @pods.map { |p| versions_cell(p) }; risks = @pods.map { |p| risk_cell(p) }; owners = @pods.map { |p| p.owners.empty? ? "—" : p.owners.join(', ') }
|
|
16
|
+
w_pod = [h_pod.length, pod_names.map(&:length).max || 0].max; w_ver = [h_ver.length, versions.map(&:length).max || 0].max; w_risk = [h_risk.length, risks.map(&:length).max || 0].max; w_owners = [h_owners.length, owners.map(&:length).max || 0].max
|
|
17
|
+
|
|
18
|
+
row_formatter = ->(c1, c2, c3, c4) { "| #{c1.ljust(w_pod)} | #{c2.ljust(w_ver)} | #{c3.rjust(w_risk)} | #{c4.ljust(w_owners)} |" }
|
|
19
|
+
|
|
20
|
+
lines = [header, ""]; lines << row_formatter.call(h_pod, h_ver, h_risk, h_owners)
|
|
21
|
+
sep_pod = ":-" + ("-" * (w_pod - 1)); sep_ver = ":-" + ("-" * (w_ver - 1)); sep_risk = ("-" * (w_risk - 1)) + ":"; sep_owners = ":-" + ("-" * (w_owners - 1))
|
|
22
|
+
lines << "|#{sep_pod}|#{sep_ver}|#{sep_risk}|#{sep_owners}|"
|
|
23
|
+
@pods.each_with_index { |_, i| lines << row_formatter.call(pod_names[i], versions[i], risks[i], owners[i]) }
|
|
24
|
+
|
|
25
|
+
lines.join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def is_outdated?(current, latest); latest && !latest.empty? && Podrpt::VersionComparer.compare(latest, current) > 0; end
|
|
31
|
+
def versions_cell(pod); current, latest = pod.current_version, pod.latest_version; return "#{current} (latest unknown)" if latest.nil? || latest.empty?; is_outdated?(current, latest) ? "#{current} -> #{latest}" : "#{current} (latest)"; end
|
|
32
|
+
def risk_cell(pod); "#{pod.risk} #{risk_emoji(pod.risk)}"; end
|
|
33
|
+
def risk_emoji(risk_value); return "🟢" if risk_value.nil?; case risk_value; when ...401 then "🟢"; when 401..700 then "🟡"; else "🔴"; end; end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# lib/podrpt/slack_notifier.rb
|
|
2
|
+
|
|
3
|
+
module Podrpt
|
|
4
|
+
class SlackNotifier
|
|
5
|
+
def self.notify(webhook_url, report_text, dry_run: false)
|
|
6
|
+
if dry_run
|
|
7
|
+
puts "\n--- SLACK NOTIFICATION DRY RUN ---"
|
|
8
|
+
puts "Target URL: #{webhook_url || 'Nenhuma URL fornecida'}"
|
|
9
|
+
puts "--- Payload ---"
|
|
10
|
+
puts report_text
|
|
11
|
+
puts "----------------------------------"
|
|
12
|
+
puts "Dry run completed. No notification was sent."
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
unless webhook_url && !webhook_url.empty?
|
|
17
|
+
puts "ERRO: Slack URL not provided. Logging out."
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
puts "Sending report to Slack..."
|
|
22
|
+
headers = { 'Content-Type' => 'application/json' }
|
|
23
|
+
payload = { text: "```\n#{report_text}\n```" }.to_json
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
response = HTTParty.post(webhook_url, body: payload, headers: headers)
|
|
27
|
+
if response.success?
|
|
28
|
+
puts "Report sent successfully!"
|
|
29
|
+
else
|
|
30
|
+
puts "ERROR sending to Slack. Status: #{response.code}, Response: #{response.body}"
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
rescue => e
|
|
34
|
+
puts "Connection ERROR when sending to Slack: #{e.message}"
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/podrpt/version.rb
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Podrpt
|
|
2
|
+
module VersionComparer
|
|
3
|
+
def self.tokenize(version); (version || '').to_s.scan(/[A-Za-z]+|\d+/).map { |t| t.match?(/\d+/) ? t.to_i : t.downcase }; end
|
|
4
|
+
def self.compare(a, b)
|
|
5
|
+
ta, tb = tokenize(a), tokenize(b)
|
|
6
|
+
[ta.length, tb.length].max.times do |i|
|
|
7
|
+
va, vb = ta[i] || 0, tb[i] || 0
|
|
8
|
+
if va.is_a?(vb.class)
|
|
9
|
+
next if va == vb
|
|
10
|
+
return va > vb ? 1 : -1
|
|
11
|
+
else
|
|
12
|
+
return va.is_a?(Integer) ? 1 : -1
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
0
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Podrpt
|
|
2
|
+
class VersionFetcher
|
|
3
|
+
def initialize(options)
|
|
4
|
+
@options = options
|
|
5
|
+
@sources_manager = Pod::Config.instance.sources_manager
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def fetch_latest_versions_in_bulk(pod_names)
|
|
9
|
+
return {} if pod_names.empty?
|
|
10
|
+
puts "Discovering the latest version for #{pod_names.length} pods..."
|
|
11
|
+
results = Concurrent::Map.new
|
|
12
|
+
pool = Concurrent::ThreadPoolExecutor.new(max_threads: @options.trunk_workers)
|
|
13
|
+
pod_names.each { |name| pool.post { results[name] = find_latest_version(name) } }
|
|
14
|
+
pool.shutdown
|
|
15
|
+
pool.wait_for_termination
|
|
16
|
+
Hash[results.each_pair.to_a]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def find_latest_version(pod_name)
|
|
22
|
+
set = @sources_manager.search(Pod::Dependency.new(pod_name))
|
|
23
|
+
set&.highest_version.to_s
|
|
24
|
+
rescue => e
|
|
25
|
+
warn " WARNING: Failed to fetch version for #{pod_name}: #{e.message}"
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: podrpt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Alves
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-09-
|
|
11
|
+
date: 2025-09-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cocoapods
|
|
@@ -74,6 +74,7 @@ executables:
|
|
|
74
74
|
extensions: []
|
|
75
75
|
extra_rdoc_files: []
|
|
76
76
|
files:
|
|
77
|
+
- ".DS_Store"
|
|
77
78
|
- CHANGELOG.md
|
|
78
79
|
- CODE_OF_CONDUCT.md
|
|
79
80
|
- Gemfile
|
|
@@ -82,7 +83,15 @@ files:
|
|
|
82
83
|
- Rakefile
|
|
83
84
|
- bin/podrpt
|
|
84
85
|
- lib/podrpt.rb
|
|
86
|
+
- lib/podrpt/cli.rb
|
|
87
|
+
- lib/podrpt/configuration.rb
|
|
88
|
+
- lib/podrpt/lockfile_analyzer.rb
|
|
89
|
+
- lib/podrpt/models.rb
|
|
90
|
+
- lib/podrpt/report_generator.rb
|
|
91
|
+
- lib/podrpt/slack_notifier.rb
|
|
85
92
|
- lib/podrpt/version.rb
|
|
93
|
+
- lib/podrpt/version_comparer.rb
|
|
94
|
+
- lib/podrpt/version_fetcher.rb
|
|
86
95
|
- sig/podrpt.rbs
|
|
87
96
|
homepage: https://github.com/swiftdrew/podrpt
|
|
88
97
|
licenses:
|