podrpt 0.1.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/.DS_Store +0 -0
 - data/CHANGELOG.md +2 -2
 - data/lib/podrpt/cli.rb +120 -0
 - data/lib/podrpt/configuration.rb +38 -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
 - data/podrpt-0.1.0.gem +0 -0
 - metadata +11 -1
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 716eda643906d4b60d7d4e7baba1e004440c84ae01452a16b118800ca48a22a6
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: b2e8b2fd267002cf11476c6b9c35916801ba9c04b87ded24699fead6eb79c4db
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 8341dd94f68516d6c296ab9b2ba2139f18f52443bcd64c78a50953a286c0b2995f2623483cb050508561ea04a6eef779456a2d2c1b282cfa549e5e12217c7a4b
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 3ee27c63e944aa812e46d9c27cb3ff2390787bfd70ad5d1972d97c8e54da947ae4c2844cbc2ccb865c87b793059b0d77b1a5d16e7441f5549aa7c54f9731dd71
         
     | 
    
        data/.DS_Store
    ADDED
    
    | 
         Binary file 
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    
    
        data/lib/podrpt/cli.rb
    ADDED
    
    | 
         @@ -0,0 +1,120 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Podrpt
         
     | 
| 
      
 2 
     | 
    
         
            +
              class CLI
         
     | 
| 
      
 3 
     | 
    
         
            +
                def self.start(args)
         
     | 
| 
      
 4 
     | 
    
         
            +
                  command = args.shift || 'run'
         
     | 
| 
      
 5 
     | 
    
         
            +
                  case command
         
     | 
| 
      
 6 
     | 
    
         
            +
                  when 'run'
         
     | 
| 
      
 7 
     | 
    
         
            +
                    run_reporter(args)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  when 'init'
         
     | 
| 
      
 9 
     | 
    
         
            +
                    initialize_configuration
         
     | 
| 
      
 10 
     | 
    
         
            +
                  when '--version', '-v'
         
     | 
| 
      
 11 
     | 
    
         
            +
                    puts Podrpt::VERSION
         
     | 
| 
      
 12 
     | 
    
         
            +
                  else
         
     | 
| 
      
 13 
     | 
    
         
            +
                    puts "Unknown command: '#{command}'. Use 'run' or 'init'."
         
     | 
| 
      
 14 
     | 
    
         
            +
                    exit 1
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
                end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                private
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                def self.initialize_configuration
         
     | 
| 
      
 21 
     | 
    
         
            +
                  puts "🚀 Starting podrpt configuration..."
         
     | 
| 
      
 22 
     | 
    
         
            +
                  Podrpt::Configuration.create_risk_file
         
     | 
| 
      
 23 
     | 
    
         
            +
                  Podrpt::Configuration.create_allowlist_file
         
     | 
| 
      
 24 
     | 
    
         
            +
                  
         
     | 
| 
      
 25 
     | 
    
         
            +
                  puts "\nNow, please enter the URL where the Webhook will be sent to Slack:"
         
     | 
| 
      
 26 
     | 
    
         
            +
                  print "> "
         
     | 
| 
      
 27 
     | 
    
         
            +
                  url = $stdin.gets.chomp
         
     | 
| 
      
 28 
     | 
    
         
            +
                  if url.empty?
         
     | 
| 
      
 29 
     | 
    
         
            +
                    puts "❌ An error occurred and this step was skipped, talk to the gem admin"
         
     | 
| 
      
 30 
     | 
    
         
            +
                  else
         
     | 
| 
      
 31 
     | 
    
         
            +
                    Podrpt::Configuration.save_slack_url(url)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
                  puts "\nSetup complete! Edit the .yaml files and run 'podrpt run'."
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def self.run_reporter(args)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  options = parse_run_options(args)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  
         
     | 
| 
      
 39 
     | 
    
         
            +
                  analyzer = Podrpt::LockfileAnalyzer.new(options.project_dir)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  all_pods_versions = analyzer.pod_versions
         
     | 
| 
      
 41 
     | 
    
         
            +
                  classified_pods = analyzer.classify_pods
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  initial_filter = classified_pods[:spec_repo].dup
         
     | 
| 
      
 44 
     | 
    
         
            +
                  initial_filter -= classified_pods[:dev_path]
         
     | 
| 
      
 45 
     | 
    
         
            +
                  current_pods = all_pods_versions.slice(*initial_filter.to_a)
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                  allowlist_config = load_allowlist(File.join(options.project_dir, options.allowlist_yaml))
         
     | 
| 
      
 48 
     | 
    
         
            +
                  pods_for_report = apply_allowlist_filter(current_pods, allowlist_config)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  puts "[podrpt] Lock totals: #{all_pods_versions.size} | Pre-allowlist: #{current_pods.size} | Final report: #{pods_for_report.size}"
         
     | 
| 
      
 50 
     | 
    
         
            +
                  options.total_pods_count = pods_for_report.size
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  risk_config = load_risk_config(File.join(options.project_dir, options.risk_yaml))
         
     | 
| 
      
 53 
     | 
    
         
            +
                  if options.sync_risk_yaml
         
     | 
| 
      
 54 
     | 
    
         
            +
                    sync_risk_yaml(File.join(options.project_dir, options.risk_yaml), pods_for_report, risk_config)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  version_fetcher = Podrpt::VersionFetcher.new(options)
         
     | 
| 
      
 58 
     | 
    
         
            +
                  latest_versions = version_fetcher.fetch_latest_versions_in_bulk(pods_for_report.keys)
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                  final_analysis = []
         
     | 
| 
      
 61 
     | 
    
         
            +
                  pods_for_report.sort_by { |name, _| name.downcase }.each do |name, version|
         
     | 
| 
      
 62 
     | 
    
         
            +
                    pod_risk_info = risk_config['pods'][name] || risk_config['default']
         
     | 
| 
      
 63 
     | 
    
         
            +
                    analysis = Podrpt::PodAnalysis.new(name: name, current_version: version, latest_version: latest_versions[name], risk: pod_risk_info['risk'], owners: pod_risk_info['owners'] || [])
         
     | 
| 
      
 64 
     | 
    
         
            +
                    if options.only_outdated && !is_outdated(analysis.current_version, analysis.latest_version)
         
     | 
| 
      
 65 
     | 
    
         
            +
                      next
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
                    final_analysis << analysis
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
                  
         
     | 
| 
      
 70 
     | 
    
         
            +
                  reporter = Podrpt::ReportGenerator.new(final_analysis, options)
         
     | 
| 
      
 71 
     | 
    
         
            +
                  report_text = reporter.build_report_text
         
     | 
| 
      
 72 
     | 
    
         
            +
                  
         
     | 
| 
      
 73 
     | 
    
         
            +
                  Podrpt::SlackNotifier.notify(options.slack_webhook_url, report_text, dry_run: options.dry_run)
         
     | 
| 
      
 74 
     | 
    
         
            +
                end
         
     | 
| 
      
 75 
     | 
    
         
            +
                
         
     | 
| 
      
 76 
     | 
    
         
            +
                def self.parse_run_options(args)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  options = Podrpt::Options.new(
         
     | 
| 
      
 78 
     | 
    
         
            +
                    project_dir: Dir.pwd,
         
     | 
| 
      
 79 
     | 
    
         
            +
                    risk_yaml: 'PodsRisk.yaml',
         
     | 
| 
      
 80 
     | 
    
         
            +
                    allowlist_yaml: 'PodsAllowlist.yaml',
         
     | 
| 
      
 81 
     | 
    
         
            +
                    only_outdated: true,
         
     | 
| 
      
 82 
     | 
    
         
            +
                    trunk_workers: 8,
         
     | 
| 
      
 83 
     | 
    
         
            +
                    slack_webhook_url: ENV['SLACK_WEBHOOK_URL'] || Podrpt::Configuration.load_slack_url,
         
     | 
| 
      
 84 
     | 
    
         
            +
                    dry_run: false
         
     | 
| 
      
 85 
     | 
    
         
            +
                  )
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                  OptionParser.new do |opts|
         
     | 
| 
      
 88 
     | 
    
         
            +
                    opts.banner = "Usage: podrpt run [options]"
         
     | 
| 
      
 89 
     | 
    
         
            +
                    opts.on("--project-dir DIR", "Project DIR") { |v| options.project_dir = v }
         
     | 
| 
      
 90 
     | 
    
         
            +
                    opts.on("--slack-webhook-url URL", "URL Webhook (overwriting config)") { |v| options.slack_webhook_url = v }
         
     | 
| 
      
 91 
     | 
    
         
            +
                    opts.on("--show-all", "Show all pods") { |v| options.only_outdated = false }
         
     | 
| 
      
 92 
     | 
    
         
            +
                    opts.on("--sync-risk-yaml", "Sync PodsRisk.yaml") { |v| options.sync_risk_yaml = v }
         
     | 
| 
      
 93 
     | 
    
         
            +
                    opts.on("--dry-run", "Simulates sending to Slack, printing the payload in the terminal") { |v| options.dry_run = v }
         
     | 
| 
      
 94 
     | 
    
         
            +
                  end.parse!(args)
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                  unless options.slack_webhook_url || options.dry_run
         
     | 
| 
      
 97 
     | 
    
         
            +
                    puts "❌ ERROR: Slack URL not configured. Run 'podrpt init' or use --dry-run."
         
     | 
| 
      
 98 
     | 
    
         
            +
                    exit 1
         
     | 
| 
      
 99 
     | 
    
         
            +
                  end
         
     | 
| 
      
 100 
     | 
    
         
            +
                  options
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
                
         
     | 
| 
      
 103 
     | 
    
         
            +
                def self.load_allowlist(path); return {} unless File.exist?(path); config = YAML.load_file(path); config&.key?('allowlist') ? config['allowlist'] : {}; rescue; {}; end
         
     | 
| 
      
 104 
     | 
    
         
            +
                def self.apply_allowlist_filter(all_pods, allowlist_config)
         
     | 
| 
      
 105 
     | 
    
         
            +
                  return all_pods if allowlist_config.nil? || allowlist_config.empty?
         
     | 
| 
      
 106 
     | 
    
         
            +
                  allowed_sets = allowlist_config.transform_values(&:to_set); vendor_prefixes = allowed_sets.keys
         
     | 
| 
      
 107 
     | 
    
         
            +
                  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 }
         
     | 
| 
      
 108 
     | 
    
         
            +
                end
         
     | 
| 
      
 109 
     | 
    
         
            +
                def self.load_risk_config(path)
         
     | 
| 
      
 110 
     | 
    
         
            +
                  return { 'default' => { 'risk' => 500, 'owners' => [] }, 'pods' => {} } unless File.exist?(path)
         
     | 
| 
      
 111 
     | 
    
         
            +
                  config = YAML.load_file(path) || {}; config['default'] ||= { 'risk' => 500, 'owners' => [] }; config['pods'] ||= {}; config
         
     | 
| 
      
 112 
     | 
    
         
            +
                end
         
     | 
| 
      
 113 
     | 
    
         
            +
                def self.sync_risk_yaml(path, pods, config)
         
     | 
| 
      
 114 
     | 
    
         
            +
                  pods.keys.sort_by(&:downcase).each { |name| config['pods'][name] ||= config['default'].dup }
         
     | 
| 
      
 115 
     | 
    
         
            +
                  File.write(path, config.to_yaml)
         
     | 
| 
      
 116 
     | 
    
         
            +
                  puts "[podrpt] PodsRisk.yaml synced with #{config['pods'].size} pods."
         
     | 
| 
      
 117 
     | 
    
         
            +
                end
         
     | 
| 
      
 118 
     | 
    
         
            +
                def self.is_outdated(current, latest); latest && !latest.empty? && Podrpt::VersionComparer.compare(latest, current) > 0; end
         
     | 
| 
      
 119 
     | 
    
         
            +
              end
         
     | 
| 
      
 120 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,38 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'yaml'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Podrpt
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Configuration
         
     | 
| 
      
 5 
     | 
    
         
            +
                CONFIG_FILE = '.podrpt.yml'.freeze
         
     | 
| 
      
 6 
     | 
    
         
            +
                RISK_FILE = 'PodsRisk.yaml'.freeze
         
     | 
| 
      
 7 
     | 
    
         
            +
                ALLOWLIST_FILE = 'PodsAllowlist.yaml'.freeze
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                def self.save_slack_url(url)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  config = File.exist?(CONFIG_FILE) ? YAML.load_file(CONFIG_FILE) || {} : {}
         
     | 
| 
      
 11 
     | 
    
         
            +
                  config['slack_webhook_url'] = url
         
     | 
| 
      
 12 
     | 
    
         
            +
                  File.write(CONFIG_FILE, config.to_yaml)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  puts "✅ Slack URL saved to #{CONFIG_FILE}"
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def self.load_slack_url
         
     | 
| 
      
 17 
     | 
    
         
            +
                  return nil unless File.exist?(CONFIG_FILE)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  YAML.load_file(CONFIG_FILE)['slack_webhook_url']
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def self.create_risk_file
         
     | 
| 
      
 22 
     | 
    
         
            +
                  return if File.exist?(RISK_FILE)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  content = {
         
     | 
| 
      
 24 
     | 
    
         
            +
                    'default' => { 'risk' => 500, 'owners' => [] },
         
     | 
| 
      
 25 
     | 
    
         
            +
                    'pods' => { '' => { 'risk' => 0, 'owners' => [''] } }
         
     | 
| 
      
 26 
     | 
    
         
            +
                  }.to_yaml
         
     | 
| 
      
 27 
     | 
    
         
            +
                  File.write(RISK_FILE, content)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  puts "✅ Risk file '#{RISK_FILE}' created."
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def self.create_allowlist_file
         
     | 
| 
      
 32 
     | 
    
         
            +
                  return if File.exist?(ALLOWLIST_FILE)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  content = { 'allowlist' => { '' => [''] } }.to_yaml
         
     | 
| 
      
 34 
     | 
    
         
            +
                  File.write(ALLOWLIST_FILE, content)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  puts "✅ Allow list '#{ALLOWLIST_FILE}' created."
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
            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
         
     | 
    
        data/podrpt-0.1.0.gem
    ADDED
    
    | 
         Binary file 
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: podrpt
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version:  
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.0.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Andrew Alves
         
     | 
| 
         @@ -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,16 @@ 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
         
     | 
| 
      
 95 
     | 
    
         
            +
            - podrpt-0.1.0.gem
         
     | 
| 
       86 
96 
     | 
    
         
             
            - sig/podrpt.rbs
         
     | 
| 
       87 
97 
     | 
    
         
             
            homepage: https://github.com/swiftdrew/podrpt
         
     | 
| 
       88 
98 
     | 
    
         
             
            licenses:
         
     |