Skip to content

Commit

Permalink
Merge pull request #84 from railsbump/trigger-github-checks-via-api
Browse files Browse the repository at this point in the history
Trigger Github checks via Github API instead of commits + Refactor checks
  • Loading branch information
etagwerker authored Oct 2, 2024
2 parents 343ece2 + 9c06372 commit 34b4fce
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 178 deletions.
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

0 comments on commit 34b4fce

Please sign in to comment.