Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger Github checks via Github API instead of commits + Refactor checks #84

Merged
merged 10 commits into from
Oct 2, 2024
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

.rspec

.vscode

config/database.yml

coverage
Expand Down
67 changes: 0 additions & 67 deletions app/services/check_out_worker_repo.rb

This file was deleted.

136 changes: 29 additions & 107 deletions app/services/compats/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,49 @@
# The purpose of this service is to check a compat, i.e. determine whether its set of dependencies is compatible with its Rails release or not. To do so, several approaches are taken, from least to most complex.
module Compats
class Check < Baseline::Service
RAILS_GEMS = %w(
actioncable
actionmailbox
actionmailer
actionpack
actiontext
actionview
activejob
activemodel
activerecord
activestorage
activesupport
rails
railties
)
CHECK_STRATEGIES = [
Compats::Checks::EmptyDependenciesCheck,
Compats::Checks::RailsGemsCheck,
Compats::Checks::DependencySubsetsCheck,
Compats::Checks::BundlerGithubCheck
]

attr_accessor :compat

# This method checks a compat by calling all check strategies. It only does checks on pending compats.
#
# If any of them marks the compat as incompatible, the compat is marked as incompatible.
#
# If any of them mark the compat as compatible, the compat is marked as compatible.
#
# @param [Compat] compat The compat to check
def call(compat)
check_uniqueness on_error: :return

if compat.checked?
raise Error, "Compat is already checked."
end

@compat = compat

call_all_private_methods_without_args

compat.checked!
end

private

# This method checks for the simplest case: if the compat has no dependencies, it's marked as compatible.
def check_empty_dependencies
return unless @compat.pending?

if @compat.dependencies.blank?
@compat.status = :compatible
@compat.status_determined_by = "empty_dependencies"
end
CHECK_STRATEGIES.each do |klass|
klass.new(compat).call
end
end

# This method checks if the dependencies include any Rail gems, and if so, if any of them have a different version than the compat's Rails version. If that's the case, the compat is marked as incompatible.
def check_rails_gems
return unless @compat.pending?

@compat.dependencies.each do |gem_name, requirement|
next unless RAILS_GEMS.include?(gem_name)
requirement_unmet = requirement.split(/\s*,\s*/).any? do |r|
!Gem::Requirement.new(r).satisfied_by?(@compat.rails_release.version)
end
if requirement_unmet
@compat.status = :incompatible
@compat.status_determined_by = "rails_gems"
return
end
end
# This method checks a compat by calling all check strategies. It doesn't care about the compat's current status.
# It will override the current status.
#
# If any of them marks the compat as incompatible, the compat is marked as incompatible.
#
# If any of them mark the compat as compatible, the compat is marked as compatible.
#
# @param [Compat] compat The compat to check
def check!(compat)
CHECK_STRATEGIES.each do |klass|
klass.new(compat).check!
end
end

# This method checks if any other compats exist, that are marked as incompatible and have a subset of the compat's dependencies. If so, the compat must be incompatible and is marked as such.
def check_dependency_subsets
return unless @compat.pending? && (2..10).cover?(@compat.dependencies.size)

subsets = (1..@compat.dependencies.size - 1).flat_map do |count|
@compat.dependencies.keys.combination(count).map { @compat.dependencies.slice *_1 }
end

subsets.in_groups_of(100, false).each do |group|
if @compat.rails_release.compats.where("dependencies::jsonb = ?", group.to_json).incompatible.any?
@compat.status = :incompatible
@compat.status_determined_by = "dependency_subsets"
return
end
end
end
private

# This method checks if any other compats exist, that are marked as compatible and have a superset of the compat's dependencies. If so, the compat must be compatible and is marked as such.
def check_dependency_supersets
Expand Down Expand Up @@ -166,49 +132,5 @@ def check_dependency_supersets
# require "byebug"; byebug
# @compat.status_determined_by = "bundler_local"
# end

# This method checks a compat by creating a new branch in the "checker" repository, adding a Gemfile with the compat's dependencies and pushing it to GitHub. A GitHub Actions workflow is then triggered in the "checker" repo, which tries to run `bundler lock` to resolve the dependencies. Afterwards, GitHub sends a notification to the "github_notifications" API endpoint, which creates a new GithubNotification and processes it in `GithubNotifications::Process`.
def check_with_bundler_github
return unless @compat.pending? && Rails.env.production?

branch = @compat.id.to_s

# Delete branch if it exists
External::Github.delete_branch(branch)

CheckOutWorkerRepo.call do |git|
git.branch(branch).checkout

action_file = File.join(git.dir.path, ".github", "workflows", "check.yml")
action_content = File.read(action_file)
.gsub("RUBY_VERSION", @compat.rails_release.compatible_ruby_version.to_s)
.gsub("BUNDLER_VERSION", @compat.rails_release.compatible_bundler_version.to_s)
File.write action_file, action_content

dependencies = @compat.dependencies.dup
dependencies.transform_values! do |contraints|
contraints.split(/\s*,\s*/)
end
dependencies["rails"] ||= []
dependencies["rails"] << "#{@compat.rails_release.version.approximate_recommendation}.0"

gemfile = File.join(git.dir.path, "Gemfile")
gemfile_content = dependencies
.map do |gem, constraints_group|
"gem '#{gem}', #{constraints_group.map { "'#{_1}'" }.join(", ")}"
end
.unshift("source 'https://rubygems.org'")
.join("\n")
File.write gemfile, gemfile_content

git.add [action_file, gemfile]
git.commit @compat.to_s
Octopoller.poll retries: 5 do
git.push "origin", branch
rescue Git::GitExecuteError
:re_poll
end
end
end
end
end
15 changes: 15 additions & 0 deletions app/services/compats/checks/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Compats::Checks
class Base
def initialize(compat)
@compat = compat
end

def call
raise NotImplementedError
end

def check!
raise NotImplementedError
end
end
end
37 changes: 37 additions & 0 deletions app/services/compats/checks/bundler_github_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Compats::Checks

# This method checks a compat by dispatching the check_bundler workflow.
class BundlerGithubCheck < Base
# Define the repository, workflow file, and branch
GITHUB_REPO = 'railsbump/checker'
GITHUB_WORKFLOW = 'check_bundler.yml'
GITHUB_REF = 'main'

def call
return unless @compat.pending? && Rails.env.production?

check!
end

def check!
# Initialize the Octokit client with your GitHub token
client = Octokit::Client.new(access_token: ENV['GITHUB_ACCESS_TOKEN'])

# Trigger the workflow dispatch event
client.workflow_dispatch(GITHUB_REPO, GITHUB_WORKFLOW, GITHUB_REF, inputs: inputs)
end

private

# Define the inputs for the workflow
def inputs
{
rails_version: @compat.rails_release.version.to_s,
ruby_version: @compat.rails_release.minimum_ruby_version.to_s,
bundler_version: @compat.rails_release.minimum_bundler_version.to_s,
dependencies: JSON::dump(@compat.dependencies),
compat_id: @compat.id.to_s
}
end
end
end
29 changes: 29 additions & 0 deletions app/services/compats/checks/dependency_subsets_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Compats::Checks

# This method checks if any other compats exist, that are marked as incompatible
# and have a subset of the compat's dependencies.
#
# If so, the compat must be incompatible and is marked as such.
class DependencySubsetsCheck < Base
def call
return unless @compat.pending? && (2..10).cover?(@compat.dependencies.size)

check!
end

def check!
subsets = (1..@compat.dependencies.size - 1).flat_map do |count|
@compat.dependencies.keys.combination(count).map { @compat.dependencies.slice *_1 }
end

subsets.in_groups_of(100, false).each do |group|
if @compat.rails_release.compats.where("dependencies::jsonb = ?", group.to_json).incompatible.any?
@compat.status = :incompatible
@compat.status_determined_by = "dependency_subsets"
@compat.checked!
return
end
end
end
end
end
20 changes: 20 additions & 0 deletions app/services/compats/checks/empty_dependencies_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Compats::Checks

# This method checks for the simplest case: if the compat has no dependencies,
# it's marked as compatible.
class EmptyDependenciesCheck < Base
def call
return unless @compat.pending?

check!
end

def check!
if @compat.dependencies.blank?
@compat.status = :compatible
@compat.status_determined_by = "empty_dependencies"
@compat.checked!
end
end
end
end
45 changes: 45 additions & 0 deletions app/services/compats/checks/rails_gems_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Compats::Checks

# This method checks if the dependencies include any Rail gems, and if so,
# if any of them have a different version than the compat's Rails version.
#
# If that's the case, the compat is marked as incompatible.
class RailsGemsCheck < Base
RAILS_GEMS = %w(
actioncable
actionmailbox
actionmailer
actionpack
actiontext
actionview
activejob
activemodel
activerecord
activestorage
activesupport
rails
railties
)

def call
return unless @compat.pending?

check!
end

def check!
@compat.dependencies.each do |gem_name, requirement|
next unless RAILS_GEMS.include?(gem_name)
requirement_unmet = requirement.split(/\s*,\s*/).any? do |r|
!Gem::Requirement.new(r).satisfied_by?(@compat.rails_release.version)
end
if requirement_unmet
@compat.status = :incompatible
@compat.status_determined_by = "rails_gems"
@compat.checked!
return
end
end
end
end
end
Loading