Skip to content

Instantly share code, notes, and snippets.

@mtancoigne
Created February 27, 2023 09:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mtancoigne/1d1315af5fb576d09dc1dca275c6a643 to your computer and use it in GitHub Desktop.
Save mtancoigne/1d1315af5fb576d09dc1dca275c6a643 to your computer and use it in GitHub Desktop.
Find outdated gems in sub-directories
#!/usr/bin/env ruby
# frozen_string_literal: true
# Find projects with outdated dependencies
#
# Usage:
# airsec <gem>
# airsec <gem> [[min version] max version]
#
# Examples:
# airsec rails # Finds projects with outdated version of the "rails" gem
# airsec rails 7 # Finds projects with version of the "rails" gem < 7.0.0
# airsec rails 6 7.0.2 # Finds projects with version of the "rails" gem between 6.0.0 and 7.0.2
require 'json'
require 'net/https'
require 'uri/https'
# Ignored paths
IGNORES = [
'node_modules/', # Some node modules have gemfiles
# 'unmonitored/project'
]
##
# Methods to find Gem lockfiles with specific outdated gem versions, or gems in a given range
class AirSec
attr_reader :directory, :gem, :max_version, :min_version, :ignores, :list
##
# @param directory [String] Working directory
# @param gem [String] Gem name, as found on rubygems
# @param min_version [String|nil] Optional minimum version (inclusive)
# @param max_version [String|nil] Optional maximum version (exclusive). Defaults to last version on Rubygem
# @param ignores [Array<String>] Optional list of paths to ignore
def initialize(directory, gem, min_version: nil, max_version: nil, ignores: [])
@last_version = nil
@list = {}
@directory = directory
@gem = gem
@min_version = min_version
@max_version = max_version || last_version
@ignores = ignores
limits = ["< #{@max_version}"]
limits << ">= #{min_version}" if min_version
@dependency = Gem::Dependency.new gem, limits
end
def find
Dir.glob(File.join(@directory, '**', 'Gemfile.lock')).sort.each do |node|
path = File.dirname node
next if File.directory?(node) || ignore?(path)
version = find_version node
next if skip? version
add version, path
end
@list = @list.keys.sort.to_h { |k| [k, @list[k]] }
end
def last_version
return @last_version if @last_version
uri = URI.parse("https://rubygems.org/api/v1/gems/#{@gem}.json")
response = Net::HTTP.get_response(uri)
raise "Gem '#{@gem}' not found" if response.code == 404
hash = JSON.parse(response.body)
@last_version = Gem::Version.new hash['version']
end
def summarize
puts "Directories using \"#{@gem}\" gem, in version #{@min_version ? ">= #{@min_version}, " : ''}< #{@max_version}#{@last_version ? ' (latest)' : ''}:"
@list.each_pair do |version, paths|
puts "\033[31m#{version}\033[39m"
paths.each { |path| puts " - #{path}" }
end
end
def to_json(*args)
@list.to_json(args)
end
private
def skip?(version)
version.nil? || !@dependency.match?(@gem, version)
end
def add(version, path)
@list[version] ||= []
@list[version] << path
end
def find_version(file_path)
line = File.readlines(file_path, chomp: true).find { |l| matches? l }
return unless line
match = matches? line
return unless match
match[:version]
end
def matches?(line)
line.match(/^ #{@gem} \((?<version>.*)\)/)
end
def ignore?(path)
@ignores.each { |string| return true if path.include? string }
false
end
end
raise 'Invalid args amount' if ARGV.count < 1 || ARGV.count > 3
gem = ARGV.shift
max = ARGV.pop
min = ARGV.pop
finder = AirSec.new '.', gem, min_version: min, max_version: max, ignores: IGNORES
finder.find
finder.summarize
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment