├── log
└── .keep
├── repos
└── .keep
├── app
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ ├── concerns
│ │ └── .keep
│ ├── batch_entry.rb
│ ├── batch_job.rb
│ └── branch.rb
├── assets
│ ├── images
│ │ └── .keep
│ ├── stylesheets
│ │ └── application.css
│ └── javascripts
│ │ └── application.js
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── main_controller.rb
│ └── application_controller.rb
├── views
│ ├── layouts
│ │ ├── _footer.html.erb
│ │ ├── _navbar.html.erb
│ │ └── application.html.erb
│ └── main
│ │ ├── index.html.erb
│ │ └── _commit_monitor.html.erb
├── helpers
│ └── application_helper.rb
└── workers
│ ├── batch_job_monitor.rb
│ ├── schedulers
│ └── stale_issue_marker.rb
│ ├── commit_monitor_handlers
│ └── commit_range
│ │ ├── branch_mergeability_checker.rb
│ │ ├── path_based_labeler.rb
│ │ ├── rubocop_checker.rb
│ │ ├── rubocop_checker
│ │ └── rubocop_results_filter.rb
│ │ ├── github_pr_commenter
│ │ ├── diff_filename_checker.rb
│ │ ├── diff_content_checker.rb
│ │ └── commit_metadata_checker.rb
│ │ └── github_pr_commenter.rb
│ ├── github_notification_monitor_worker.rb
│ ├── concerns
│ ├── batch_job_worker_mixin.rb
│ ├── code_analysis_mixin.rb
│ ├── batch_entry_worker_mixin.rb
│ ├── branch_worker_mixin.rb
│ └── sidekiq_worker_mixin.rb
│ ├── pull_request_monitor_handlers
│ ├── merge_target_titler.rb
│ └── wip_labeler.rb
│ ├── pull_request_monitor.rb
│ ├── pr_mergeability_checker.rb
│ └── stale_issue_marker.rb
├── bundler.d
└── .gitkeep
├── lib
├── assets
│ └── .keep
├── tasks
│ ├── .keep
│ ├── commit_monitor.rake
│ └── pull_request_monitor.rake
├── git_service
│ ├── error.rb
│ ├── unmergeable_error.rb
│ ├── repo.rb
│ ├── credentials.rb
│ ├── diff.rb
│ └── commit.rb
├── github_service
│ ├── issue_comment.rb
│ ├── message_builder.rb
│ ├── commands
│ │ ├── assign.rb
│ │ ├── close_issue.rb
│ │ ├── add_reviewer.rb
│ │ ├── set_milestone.rb
│ │ ├── unassign.rb
│ │ ├── remove_reviewer.rb
│ │ ├── move_issue.rb
│ │ ├── remove_label.rb
│ │ ├── base.rb
│ │ └── add_label.rb
│ ├── notification.rb
│ ├── response
│ │ └── ratelimit_logger.rb
│ ├── concerns
│ │ └── is_team_member.rb
│ └── command_dispatcher.rb
├── threadsafe_service_mixin.rb
├── offense_message.rb
├── linter
│ ├── haml.rb
│ ├── rubocop.rb
│ └── yaml.rb
├── console_methods.rb
├── service_mixin.rb
├── offense_message
│ └── entry.rb
├── message_collector.rb
└── minigit_service.rb
├── public
├── favicon.ico
├── robots.txt
├── 500.html
├── 422.html
└── 404.html
├── config
├── settings
│ ├── development.yml
│ └── test.yml
├── initializers
│ ├── config.rb
│ ├── session_store.rb
│ ├── mime_types.rb
│ ├── application_controller_renderer.rb
│ ├── cookies_serializer.rb
│ ├── config_listener.rb
│ ├── filter_parameter_logging.rb
│ ├── permissions_policy.rb
│ ├── assets.rb
│ ├── wrap_parameters.rb
│ ├── backtrace_silencers.rb
│ ├── inflections.rb
│ └── content_security_policy.rb
├── boot.rb
├── environment.rb
├── cable.yml
├── routes.rb
├── settings.yml
├── database.vagrant.yml
├── credentials.yml.enc
├── database.yml.sample
├── sidecloq.yml
├── locales
│ └── en.yml
├── storage.yml
├── application.rb
├── puma.rb
└── environments
│ ├── test.rb
│ └── development.rb
├── vendor
├── assets
│ ├── javascripts
│ │ └── .keep
│ └── stylesheets
│ │ └── .keep
└── stubs
│ └── parser
│ └── current.rb
├── .production_env
├── .rspec
├── .whitesource
├── spec
├── workers
│ ├── commit_monitor_handlers
│ │ └── commit_range
│ │ │ ├── rubocop_checker
│ │ │ ├── data
│ │ │ │ ├── .rubocop_local.yml
│ │ │ │ ├── with_results_without_column_numbers_and_cop_names
│ │ │ │ │ ├── example.haml
│ │ │ │ │ └── results.json
│ │ │ │ ├── with_haml_file_using_haml-lint
│ │ │ │ │ ├── example.haml
│ │ │ │ │ └── results.json
│ │ │ │ ├── .rubocop.yml
│ │ │ │ ├── with_results_with_offenses
│ │ │ │ │ ├── no_offenses.rb
│ │ │ │ │ ├── ruby_warning.rb
│ │ │ │ │ ├── ruby_syntax_error.rb
│ │ │ │ │ └── coding_convention.rb
│ │ │ │ ├── with_results_with_no_offenses
│ │ │ │ │ ├── no_offenses.rb
│ │ │ │ │ └── results.json
│ │ │ │ ├── with_void_warnings_in_spec_files
│ │ │ │ │ ├── non_spec_file_with_void_warning.rb
│ │ │ │ │ ├── spec
│ │ │ │ │ │ ├── non_spec_file_in_spec_dir_with_void_warning.rb
│ │ │ │ │ │ └── spec_file_with_void_warning_spec.rb
│ │ │ │ │ └── results.json
│ │ │ │ └── with_lines_not_in_the_diff
│ │ │ │ │ ├── example.rb
│ │ │ │ │ └── results.json
│ │ │ └── rubocop_results_filter_spec.rb
│ │ │ ├── branch_mergeability_checker_spec.rb
│ │ │ └── github_pr_commenter
│ │ │ └── diff_filename_checker_spec.rb
│ ├── batch_job_monitor_spec.rb
│ ├── pull_request_monitor_handlers
│ │ ├── wip_labeler_spec.rb
│ │ └── merge_target_titler_spec.rb
│ ├── concerns
│ │ ├── batch_job_worker_mixin_spec.rb
│ │ ├── batch_entry_worker_mixin_spec.rb
│ │ └── code_analysis_mixin_spec.rb
│ ├── github_notification_monitor_worker_spec.rb
│ ├── commit_monitor_spec.rb
│ ├── pr_mergeability_checker_spec.rb
│ └── pull_request_monitor_spec.rb
├── factories
│ ├── repo.rb
│ └── branch.rb
├── support
│ ├── batch_entry_worker_helper.rb
│ ├── sidekiq_spec_helper.rb
│ ├── services_helper.rb
│ ├── shared_examples
│ │ ├── state_predicates.rb
│ │ └── threadsafe_service_mixin_examples.rb
│ ├── settings_helper.rb
│ └── rubocop_spec_helper.rb
├── lib
│ ├── linter
│ │ ├── base_spec.rb
│ │ ├── rubocop_spec.rb
│ │ └── haml_spec.rb
│ ├── github_service
│ │ ├── commands
│ │ │ ├── assign_spec.rb
│ │ │ ├── add_reviewer_spec.rb
│ │ │ ├── remove_reviewer_spec.rb
│ │ │ └── unassign_spec.rb
│ │ └── command_dispatcher_spec.rb
│ ├── message_collector_spec.rb
│ ├── offense_message_spec.rb
│ ├── github_service_spec.rb
│ ├── offense_message
│ │ └── entry_spec.rb
│ └── github_notification_monitor_spec.rb
├── miq_bot_spec.rb
├── models
│ └── batch_entry_spec.rb
└── spec_helper.rb
├── .rubocop.yml
├── .rubocop_cc.yml
├── bin
├── rake
├── bundle
├── rails
├── before_install
└── setup
├── .rubocop_local.yml
├── renovate.json
├── config.ru
├── db
├── migrate
│ ├── 20151207193108_remove_path_from_repo.rb
│ ├── 20150814233450_add_state_to_batch_jobs.rb
│ ├── 20160713200646_add_pr_title_to_branch.rb
│ ├── 20150814183817_rename_commit_monitor_repos_to_repos.rb
│ ├── 20150814191457_rename_branches_foreign_key.rb
│ ├── 20171006050814_add_linter_offense_count_to_branch.rb
│ ├── 20250305205548_add_uniqueness_constraint_on_branches.rb
│ ├── 20140215060546_add_mergeable_to_commit_monitor_branch.rb
│ ├── 20150814183834_rename_commit_monitor_branches_to_branches.rb
│ ├── 20140117063400_add_upstream_user_to_commit_monitor_repo.rb
│ ├── 20140117055336_add_pull_request_to_commit_monitor_branch.rb
│ ├── 20140214211340_add_commits_list_to_commit_monitor_branches.rb
│ ├── 20140115031432_create_commit_monitor_repos.rb
│ ├── 20150806034945_add_on_complete_class_and_args_to_batch_jobs.rb
│ ├── 20140116232751_create_commit_monitor_branches.rb
│ ├── 20160119143158_add_merge_head_to_branch.rb
│ ├── 20150730043503_create_batch_tables.rb
│ ├── 20151204171252_update_branch_name_for_pr_branches.rb
│ ├── 20140128174930_replace_timestamps_with_last_checked_on_and_last_changed_on_for_commit_monitor_branches.rb
│ ├── 20150827194533_collapse_repo_upstream_user_into_name.rb
│ └── 20170127173128_fix_limit_in_schema.rb
├── seeds.rb
└── schema.rb
├── Procfile.tmpl
├── Rakefile
├── container-assets
├── container_env
└── entrypoint
├── .codeclimate.yml
├── .gitignore
├── .github
└── workflows
│ ├── build_and_deploy.yml
│ └── ci.yaml
├── Gemfile
└── Dockerfile
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/repos/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bundler.d/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/settings/development.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.production_env:
--------------------------------------------------------------------------------
1 | RAILS_ENV=production
2 | START_PORT=3002
3 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 | --color
3 | --order random
4 |
--------------------------------------------------------------------------------
/.whitesource:
--------------------------------------------------------------------------------
1 | {
2 | "settingsInheritedFrom": "ManageIQ/whitesource-config@master"
3 | }
--------------------------------------------------------------------------------
/lib/git_service/error.rb:
--------------------------------------------------------------------------------
1 | module GitService
2 | Error = Class.new(StandardError)
3 | end
4 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/.rubocop_local.yml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/initializers/config.rb:
--------------------------------------------------------------------------------
1 | Config.setup do |config|
2 | config.const_name = "Settings"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/git_service/unmergeable_error.rb:
--------------------------------------------------------------------------------
1 | module GitService
2 | UnmergeableError = Class.new(Error)
3 | end
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | manageiq-style: ".rubocop_base.yml"
3 | inherit_from:
4 | - ".rubocop_local.yml"
5 |
--------------------------------------------------------------------------------
/.rubocop_cc.yml:
--------------------------------------------------------------------------------
1 | inherit_from:
2 | - ".rubocop_base.yml"
3 | - ".rubocop_cc_base.yml"
4 | - ".rubocop_local.yml"
5 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/.rubocop_local.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | Exclude:
3 | - spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/**/*
4 |
--------------------------------------------------------------------------------
/spec/factories/repo.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :repo do
3 | sequence(:name) { |n| "SomeUser/repo_#{n}" }
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/layouts/_footer.html.erb:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
2 |
3 | require "bundler/setup" # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/spec/support/batch_entry_worker_helper.rb:
--------------------------------------------------------------------------------
1 | def stub_job_completion
2 | allow_any_instance_of(described_class).to receive(:check_job_complete)
3 | end
4 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_without_column_numbers_and_cop_names/example.haml:
--------------------------------------------------------------------------------
1 | %html
2 | -puts
3 |
--------------------------------------------------------------------------------
/lib/tasks/commit_monitor.rake:
--------------------------------------------------------------------------------
1 | namespace :commit_monitor do
2 | task :poll_single => :environment do
3 | CommitMonitor.perform_async
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_haml_file_using_haml-lint/example.haml:
--------------------------------------------------------------------------------
1 | %html
2 | - if smth
3 | - else
4 | - end
5 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/lib/tasks/pull_request_monitor.rake:
--------------------------------------------------------------------------------
1 | namespace :pull_request_monitor do
2 | task :poll_single => :environment do
3 | PullRequestMonitor.perform_async
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "inheritConfig": true,
4 | "inheritConfigRepoName": "manageiq/renovate-config"
5 | }
6 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_miq_bot_session'
4 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | manageiq-style: ".rubocop_base.yml"
3 | inherit_from:
4 | - ".rubocop_local.yml"
5 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_with_offenses/no_offenses.rb:
--------------------------------------------------------------------------------
1 | class NoOffenses
2 | def initialize
3 | @x = 1
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/db/migrate/20151207193108_remove_path_from_repo.rb:
--------------------------------------------------------------------------------
1 | class RemovePathFromRepo < ActiveRecord::Migration[4.2]
2 | def change
3 | remove_column :repos, :path, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_with_no_offenses/no_offenses.rb:
--------------------------------------------------------------------------------
1 | class NoOffenses
2 | def initialize
3 | @x = 1
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/controllers/main_controller.rb:
--------------------------------------------------------------------------------
1 | class MainController < ApplicationController
2 | def index
3 | @branches = Branch.includes(:repo).sort_by { |b| [b.repo.name, b.name] }
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150814233450_add_state_to_batch_jobs.rb:
--------------------------------------------------------------------------------
1 | class AddStateToBatchJobs < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :batch_jobs, :state, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20160713200646_add_pr_title_to_branch.rb:
--------------------------------------------------------------------------------
1 | class AddPrTitleToBranch < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :branches, :pr_title, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_with_offenses/ruby_warning.rb:
--------------------------------------------------------------------------------
1 | class RubyWarning
2 | def some_method
3 | unused_variable = 1
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_with_offenses/ruby_syntax_error.rb:
--------------------------------------------------------------------------------
1 | class RubySyntaxError
2 | def initialize
3 | @x = 1
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/main/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Commit Monitor
4 |
5 | <%= render "commit_monitor" %>
6 |
7 |
--------------------------------------------------------------------------------
/spec/support/sidekiq_spec_helper.rb:
--------------------------------------------------------------------------------
1 | def stub_sidekiq_logger(klass = nil)
2 | klass ||= described_class
3 | allow_any_instance_of(klass).to receive(:logger).and_return(double("logger").as_null_object)
4 | end
5 |
--------------------------------------------------------------------------------
/db/migrate/20150814183817_rename_commit_monitor_repos_to_repos.rb:
--------------------------------------------------------------------------------
1 | class RenameCommitMonitorReposToRepos < ActiveRecord::Migration[4.2]
2 | def change
3 | rename_table :commit_monitor_repos, :repos
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150814191457_rename_branches_foreign_key.rb:
--------------------------------------------------------------------------------
1 | class RenameBranchesForeignKey < ActiveRecord::Migration[4.2]
2 | def change
3 | rename_column :branches, :commit_monitor_repo_id, :repo_id
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_void_warnings_in_spec_files/non_spec_file_with_void_warning.rb:
--------------------------------------------------------------------------------
1 | class NonSpecFileWithVoidWarning
2 | object_id == 1
3 | object_id == 2
4 | end
5 |
--------------------------------------------------------------------------------
/Procfile.tmpl:
--------------------------------------------------------------------------------
1 | rails: bundle exec rails s -p $PORT
2 | sidekiq: bundle exec sidekiq -q miq_bot
3 | sidekiq_glacial: bundle exec sidekiq -q miq_bot_glacial
4 | #development.log: tail -f -n 0 log/development.log
5 |
--------------------------------------------------------------------------------
/db/migrate/20171006050814_add_linter_offense_count_to_branch.rb:
--------------------------------------------------------------------------------
1 | class AddLinterOffenseCountToBranch < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :branches, :linter_offense_count, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20250305205548_add_uniqueness_constraint_on_branches.rb:
--------------------------------------------------------------------------------
1 | class AddUniquenessConstraintOnBranches < ActiveRecord::Migration[6.1]
2 | def up
3 | add_index(:branches, [:name, :repo_id], unique: true)
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/github_service/issue_comment.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | class IssueComment < SimpleDelegator
3 | # https://developer.github.com/v3/issues
4 |
5 | def author
6 | user.login
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20140215060546_add_mergeable_to_commit_monitor_branch.rb:
--------------------------------------------------------------------------------
1 | class AddMergeableToCommitMonitorBranch < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :commit_monitor_branches, :mergeable, :boolean
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20150814183834_rename_commit_monitor_branches_to_branches.rb:
--------------------------------------------------------------------------------
1 | class RenameCommitMonitorBranchesToBranches < ActiveRecord::Migration[4.2]
2 | def change
3 | rename_table :commit_monitor_branches, :branches
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/db/migrate/20140117063400_add_upstream_user_to_commit_monitor_repo.rb:
--------------------------------------------------------------------------------
1 | class AddUpstreamUserToCommitMonitorRepo < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :commit_monitor_repos, :upstream_user, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20140117055336_add_pull_request_to_commit_monitor_branch.rb:
--------------------------------------------------------------------------------
1 | class AddPullRequestToCommitMonitorBranch < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :commit_monitor_branches, :pull_request, :boolean
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20140214211340_add_commits_list_to_commit_monitor_branches.rb:
--------------------------------------------------------------------------------
1 | class AddCommitsListToCommitMonitorBranches < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :commit_monitor_branches, :commits_list, :text
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_lines_not_in_the_diff/example.rb:
--------------------------------------------------------------------------------
1 | class CodingConvention
2 | HASH = {
3 | :keys => :values,
4 | :arent => :aligned,
5 | :kind_of_like => :this
6 | }
7 | end
8 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_void_warnings_in_spec_files/spec/non_spec_file_in_spec_dir_with_void_warning.rb:
--------------------------------------------------------------------------------
1 | class NonSpecFileInSpecDirWithVoidWarning
2 | object_id == 1
3 | object_id == 2
4 | end
5 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: miq_bot_production
11 |
--------------------------------------------------------------------------------
/spec/support/services_helper.rb:
--------------------------------------------------------------------------------
1 | def stub_github_prs(*prs)
2 | prs.flatten!
3 | prs.collect! { |i| double("Github PR #{i}", :number => i) } if prs.first.kind_of?(Numeric)
4 |
5 | expect(GithubService).to receive(:pull_requests).and_return(prs)
6 | end
7 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_with_offenses/coding_convention.rb:
--------------------------------------------------------------------------------
1 | class CodingConvention
2 | HASH = {
3 | :keys => :values,
4 | :arent => :aligned,
5 | :kind_of_like => :this
6 | }.freeze
7 | end
8 |
--------------------------------------------------------------------------------
/spec/workers/batch_job_monitor_spec.rb:
--------------------------------------------------------------------------------
1 | describe BatchJobMonitor do
2 | it "#perform_check" do
3 | BatchJob.create!
4 |
5 | expect_any_instance_of(BatchJob).to receive(:check_complete)
6 |
7 | described_class.new.perform_check
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_void_warnings_in_spec_files/spec/spec_file_with_void_warning_spec.rb:
--------------------------------------------------------------------------------
1 | describe "VoidWarning" do
2 | it "has a void warning" do
3 | 1.should == 1
4 | 2.should == 2
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | MiqBot::Application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | def application_version
3 | MiqBot.version
4 | end
5 |
6 | def time_ago_in_words_with_nil_check(time)
7 | time.nil? ? "Never" : "#{time_ago_in_words(time).capitalize} ago"
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/lib/linter/base_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Linter::Base do
2 | subject { described_class.new(double("branch")) }
3 |
4 | describe "#linter_env" do
5 | it "is an empty hash" do
6 | expect(subject.send(:linter_env)).to eq({})
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/lib/linter/rubocop_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Linter::Rubocop do
2 | subject { described_class.new(double("branch")) }
3 |
4 | describe "#linter_env" do
5 | it "is an empty hash" do
6 | expect(subject.send(:linter_env)).to eq({})
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/db/migrate/20140115031432_create_commit_monitor_repos.rb:
--------------------------------------------------------------------------------
1 | class CreateCommitMonitorRepos < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :commit_monitor_repos do |t|
4 | t.string :name
5 | t.string :path
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20150806034945_add_on_complete_class_and_args_to_batch_jobs.rb:
--------------------------------------------------------------------------------
1 | class AddOnCompleteClassAndArgsToBatchJobs < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :batch_jobs, :on_complete_class, :string
4 | add_column :batch_jobs, :on_complete_args, :text
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/lib/github_service/message_builder.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | class MessageBuilder < ::MessageCollector
3 | COMMENT_BODY_MAX_SIZE = 65_535
4 |
5 | def initialize(header = nil, continuation_header = nil)
6 | super(COMMENT_BODY_MAX_SIZE, header, continuation_header)
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/bin/before_install:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -n "$CI" ]; then
4 | echo "== Installing system packages =="
5 | sudo pip install yamllint
6 | echo
7 |
8 | echo "== Setup git user for specs =="
9 | git config --global user.name "ManageIQ Bot"
10 | git config --global user.email "bot@manageiq.org"
11 | echo
12 | fi
13 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq/web'
2 | require 'sidecloq/web'
3 |
4 | Rails.application.routes.draw do
5 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
6 |
7 | mount Sidekiq::Web, at: "/sidekiq"
8 |
9 | root 'main#index'
10 |
11 | get '/github_api_usage' => 'github_api_usage#index'
12 | end
13 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/state_predicates.rb:
--------------------------------------------------------------------------------
1 | shared_examples "state predicates" do |method, matrix|
2 | describe "##{method}" do
3 | matrix.each do |state, expected|
4 | it "when #{state}" do
5 | record = described_class.new(:state => state)
6 | expect(record.public_send(method)).to be expected
7 | end
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20140116232751_create_commit_monitor_branches.rb:
--------------------------------------------------------------------------------
1 | class CreateCommitMonitorBranches < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :commit_monitor_branches do |t|
4 | t.string :name
5 | t.string :commit_uri
6 | t.string :last_commit
7 | t.belongs_to :commit_monitor_repo
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20160119143158_add_merge_head_to_branch.rb:
--------------------------------------------------------------------------------
1 | class AddMergeHeadToBranch < ActiveRecord::Migration[4.2]
2 | class Branch < ActiveRecord::Base; end
3 |
4 | def up
5 | add_column :branches, :merge_target, :string
6 |
7 | Branch.update_all(:merge_target => "master")
8 | end
9 |
10 | def down
11 | remove_column :branches, :merge_target
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/config/settings/test.yml:
--------------------------------------------------------------------------------
1 | :labels:
2 | :unassignable:
3 | jansa/yes: jansa/yes?
4 | :unremovable:
5 | - jansa/no
6 | - jansa/yes
7 | # In test, turn everything on by default
8 | # NOTE: Using empty string is a HACK until we can get
9 | # https://github.com/danielsdeleo/deep_merge/pull/33 released via the
10 | # config gem.
11 | merge_target_titler:
12 | included_repos: ''
13 |
--------------------------------------------------------------------------------
/config/initializers/config_listener.rb:
--------------------------------------------------------------------------------
1 | Listen.to(Rails.root.join("config"), :ignore => /github_notification_monitor\.yml/) do |*paths|
2 | begin
3 | Rails.logger.info "Reloading settings due to changes in #{paths.flatten.join(", ")}"
4 | Settings.reload!
5 | rescue => err
6 | Rails.logger.error "Unable to reload the settings!"
7 | Rails.logger.error err
8 | end
9 | end.start
10 |
--------------------------------------------------------------------------------
/spec/lib/linter/haml_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe Linter::Haml do
2 | subject { described_class.new(double("branch")) }
3 | let(:stub_dir) { File.expand_path(File.join(*%w[.. .. .. vendor stubs]), __dir__) }
4 |
5 | describe "#linter_env" do
6 | it "is an empty hash" do
7 | expect(subject.send(:linter_env)).to eq({"RUBYOPT" => "-I #{stub_dir}"})
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/config/settings.yml:
--------------------------------------------------------------------------------
1 | # Service credentials
2 | github_credentials:
3 | username:
4 | password:
5 |
6 | # General settings
7 | labels:
8 | unassignable: {}
9 | unremovable: []
10 |
11 | # Worker settings
12 | diff_content_checker:
13 | offenses: {}
14 | merge_target_titler:
15 | included_repos: []
16 | path_based_labeler:
17 | rules: {}
18 | stale_issue_marker:
19 | pinned_labels:
20 | - pinned
21 |
--------------------------------------------------------------------------------
/lib/threadsafe_service_mixin.rb:
--------------------------------------------------------------------------------
1 | module ThreadsafeServiceMixin
2 | extend ActiveSupport::Concern
3 |
4 | include ServiceMixin
5 |
6 | module ClassMethods
7 | def call(*args)
8 | synchronize { super }
9 | end
10 |
11 | private
12 |
13 | def mutex
14 | @mutex ||= Mutex.new
15 | end
16 |
17 | def synchronize
18 | mutex.synchronize { yield }
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/layouts/_navbar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | - <%= link_to("Sidekiq", "/sidekiq") %>
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/config/database.vagrant.yml:
--------------------------------------------------------------------------------
1 | ---
2 | base: &base
3 | adapter: postgresql
4 | encoding: utf8
5 | pool: 25
6 | wait_timeout: 5
7 |
8 | development:
9 | <<: *base
10 | database: miq_bot_development
11 |
12 | production:
13 | <<: *base
14 | database: miq_bot_production
15 |
16 | test: &test
17 | <<: *base
18 | database: miq_bot_test
19 | # Silence these: 'NOTICE: CREATE TABLE will create...'
20 | min_messages: warning
21 |
--------------------------------------------------------------------------------
/db/migrate/20150730043503_create_batch_tables.rb:
--------------------------------------------------------------------------------
1 | class CreateBatchTables < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :batch_jobs do |t|
4 | t.timestamps
5 | t.timestamp :expires_at
6 | end
7 |
8 | create_table :batch_entries do |t|
9 | t.belongs_to :batch_job
10 | t.index :batch_job_id
11 |
12 | t.string :state
13 | t.text :result
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/workers/batch_job_monitor.rb:
--------------------------------------------------------------------------------
1 | class BatchJobMonitor
2 | include Sidekiq::Worker
3 | sidekiq_options :queue => :miq_bot, :retry => false
4 |
5 | include SidekiqWorkerMixin
6 |
7 | def perform
8 | if !first_unique_worker?
9 | logger.info "#{self.class} is already running, skipping"
10 | else
11 | perform_check
12 | end
13 | end
14 |
15 | def perform_check
16 | BatchJob.all.each(&:check_complete)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
5 | # notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/lib/github_service/commands/assign.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class Assign < Base
4 | private
5 |
6 | def _execute(issuer:, value:)
7 | user = value.strip.delete('@')
8 |
9 | if valid_assignee?(user)
10 | issue.assign(user)
11 | else
12 | issue.add_comment("@#{issuer} '#{user}' is an invalid assignee, ignoring...")
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | lgVBdCk3JuT7xVJE9W+trX3XdOT8uLYF4pb/MaKjOQMGJSJ7pIEmvg9eqOhC/YvxTfk1KKhovfeCinzTQ0Hs9Qw8tj8ucKmUU8jUiCmTcTcH4VwRL+MZekbvhR7b/s3CMzz2/cyluyZy0S6kYZjKA3GXN0IgLTg1UU3MdpjoChR4MZ6REiCRT43CmyR8eLpHa3GTQB1bVV7L8ON33fbrVhFlMA76Yk8hAqM3uCSFmdF2iblaoprnoahPTTADniKGJ5h5IROsf0bag2AHm+kI8RSA57WLWUOrWr81p8NiA9ribIMETUNA5JPQ5tZsM9Iu9ZmzFUOlu/tULEZiIW5UttEobVkHXBAKB3F415jvAkFj4R9+kQxiPQyITPzHnxNkePyToRCvGu/VMkqkPe5hFbHYiyuey9JRnr5/--w8X2QuBiDTwYY8yC--dGMBDDAtAcdl+N89ZRQD9g==
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
9 | require_relative 'seeds.local.rb' if File.exist?(File.expand_path('seeds.local.rb', __dir__))
10 |
--------------------------------------------------------------------------------
/lib/offense_message.rb:
--------------------------------------------------------------------------------
1 | class OffenseMessage
2 | attr_accessor :entries
3 |
4 | def initialize
5 | @entries = []
6 | end
7 |
8 | def lines
9 | entries.sort.group_by(&:group).collect do |group, sub_entries|
10 | [
11 | format_group(group),
12 | sub_entries.collect(&:to_s),
13 | ""
14 | ]
15 | end.flatten.compact[0...-1]
16 | end
17 |
18 | private
19 |
20 | def format_group(group)
21 | "**#{group}**" if group
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/support/settings_helper.rb:
--------------------------------------------------------------------------------
1 | def stub_settings(hash)
2 | settings = Config::Options.new
3 | settings.merge!(Settings.to_hash)
4 | settings.merge!(hash)
5 | stub_const("Settings", settings)
6 | end
7 |
8 | # Need a special stub for nil settings until https://github.com/danielsdeleo/deep_merge/pull/33
9 | # is released with the config gem
10 | def stub_nil_settings(hash)
11 | settings = Config::Options.new
12 | settings.merge!(hash)
13 | stub_const("Settings", settings)
14 | end
15 |
--------------------------------------------------------------------------------
/config/database.yml.sample:
--------------------------------------------------------------------------------
1 | ---
2 | base: &base
3 | adapter: postgresql
4 | encoding: utf8
5 | host: localhost
6 | username:
7 | password:
8 | pool: 25
9 | wait_timeout: 5
10 |
11 | development:
12 | <<: *base
13 | database: miq_bot_development
14 |
15 | production:
16 | <<: *base
17 | database: miq_bot_production
18 |
19 | test: &test
20 | <<: *base
21 | database: miq_bot_test
22 | # Silence these: 'NOTICE: CREATE TABLE will create...'
23 | min_messages: warning
24 |
--------------------------------------------------------------------------------
/lib/github_service/notification.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | class Notification < SimpleDelegator
3 | # https://developer.github.com/v3/activity/notifications/
4 |
5 | def mark_thread_as_read
6 | GithubService.mark_thread_as_read(thread_id, "read" => false)
7 | end
8 |
9 | def issue_number
10 | subject.url&.match(/\/([0-9]+)\Z/)&.try(:[], 1)
11 | end
12 |
13 | private
14 |
15 | def thread_id
16 | url.match(/[0-9]+\Z/).to_s
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/config/sidecloq.yml:
--------------------------------------------------------------------------------
1 | BatchJobMonitor:
2 | class: BatchJobMonitor
3 | cron: "* * * * *" # minutely
4 |
5 | CommitMonitor:
6 | class: CommitMonitor
7 | cron: "*/5 * * * *" # every 5th minute
8 |
9 | GithubNotificationMonitorWorker:
10 | class: GithubNotificationMonitorWorker
11 | cron: "* * * * *" # minutely
12 |
13 | PullRequestMonitor:
14 | class: PullRequestMonitor
15 | cron: "*/5 * * * *" # every 5th minute
16 |
17 | StaleIssueMarker:
18 | class: StaleIssueMarker
19 | cron: "0 0 * * 1" # midnight every Monday
20 |
--------------------------------------------------------------------------------
/app/workers/schedulers/stale_issue_marker.rb:
--------------------------------------------------------------------------------
1 | module Schedulers
2 | class StaleIssueMarker
3 | include Sidekiq::Worker
4 | sidekiq_options :queue => :miq_bot_glacial, :retry => false
5 |
6 | include SidekiqWorkerMixin
7 |
8 | def perform
9 | if !first_unique_worker?
10 | logger.info "#{self.class} is already running, skipping"
11 | else
12 | process_stale_issues
13 | end
14 | end
15 |
16 | def process_stale_issues
17 | StaleIssueMarker.perform_async
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/github_service/commands/close_issue.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class CloseIssue < Base
4 | restrict_to :organization
5 |
6 | private
7 |
8 | def _execute(issuer:, value:)
9 | if issue.pull_request? && issuer != issue.author
10 | issue.add_comment("@#{issuer} Only @#{issue.author} or a committer can close this pull request.")
11 | else
12 | GithubService.close_issue(issue.fq_repo_name, issue.number)
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/factories/branch.rb:
--------------------------------------------------------------------------------
1 | require 'securerandom'
2 |
3 | FactoryBot.define do
4 | factory :branch do
5 | sequence(:name) { |n| "branch_#{n}" }
6 | commit_uri { "https://example.com/#{repo.name}/commit/$commit" }
7 | last_commit { SecureRandom.hex(40) }
8 |
9 | repo
10 | end
11 |
12 | factory :pr_branch, :parent => :branch do
13 | sequence(:name) { |n| "prs/#{n}/head" }
14 | sequence(:pr_title) { |n| "PR title #{n}" }
15 | merge_target { "master" }
16 |
17 | pull_request { true }
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
9 |
--------------------------------------------------------------------------------
/lib/linter/haml.rb:
--------------------------------------------------------------------------------
1 | module Linter
2 | class Haml < Base
3 | private
4 |
5 | def config_files
6 | [".haml-lint.yml"] + Linter::Rubocop::CONFIG_FILES
7 | end
8 |
9 | def linter_executable
10 | "haml-lint"
11 | end
12 |
13 | def linter_env
14 | parser_stub_path = Rails.root.join("vendor", "stubs").to_s
15 | {"RUBYOPT" => "-I #{parser_stub_path}"}
16 | end
17 |
18 | def options
19 | {:reporter => 'json'}
20 | end
21 |
22 | def filtered_files(files)
23 | files.select { |file| file.end_with?(".haml") }
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/db/migrate/20151204171252_update_branch_name_for_pr_branches.rb:
--------------------------------------------------------------------------------
1 | class UpdateBranchNameForPrBranches < ActiveRecord::Migration[4.2]
2 | class Branch < ActiveRecord::Base; end # Don't use the real model
3 |
4 | def up
5 | Branch.where(:pull_request => true).each do |branch|
6 | pr_number = branch.name.split("/").last
7 | branch.update(:name => "prs/#{pr_number}/head")
8 | end
9 | end
10 |
11 | def down
12 | Branch.where(:pull_request => true).each do |branch|
13 | pr_number = branch.name.split("/")[1]
14 | branch.update(:name => "pr/#{pr_number}")
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/console_methods.rb:
--------------------------------------------------------------------------------
1 | module ConsoleMethods
2 | def simulate_sidekiq(*queue_names, run_once: false)
3 | run = true
4 | while run do
5 | queue_names.each do |queue_name|
6 | puts "[#{Time.now}] Processing '#{queue_name}'..."
7 | Sidekiq::Queue.new(queue_name).each do |job|
8 | worker = job['class'].constantize
9 | args = job['args']
10 | puts "\nProcessing #{worker} with args: #{args}"
11 | worker.new.perform(*job['args'])
12 | job.delete
13 | end
14 | end
15 | run = false if run_once
16 | sleep(2)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/container-assets/container_env:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export RAILS_ENV=production
4 | export PATH=/opt/miq_bot:$PATH
5 |
6 | function urlescape() {
7 | PAYLOAD="$1" ruby -rcgi -e "puts CGI.escape(ENV['PAYLOAD'])"
8 | }
9 |
10 | safeuser=$(urlescape ${DATABASE_USER})
11 | safepass=$(urlescape ${DATABASE_PASSWORD})
12 | if [ -z "${safeuser}" -o -z "${safepass}" ]; then
13 | DATABASE_USERINFO=""
14 | else
15 | DATABASE_USERINFO="${safeuser}:${safepass}@"
16 | fi
17 |
18 | export DATABASE_URL="postgresql://${DATABASE_USERINFO}${DATABASE_HOSTNAME:-localhost}:${DATABASE_PORT:-5432}/${DATABASE_NAME:-miq_bot_production}?encoding=utf8&pool=25&wait_timeout=5"
19 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/branch_mergeability_checker_spec.rb:
--------------------------------------------------------------------------------
1 | describe CommitMonitorHandlers::CommitRange::BranchMergeabilityChecker do
2 | let!(:branch) { create(:branch) }
3 | let!(:pr_branch) { create(:pr_branch, :repo => branch.repo, :merge_target => branch.name) }
4 | let!(:pr_branch2) { create(:pr_branch, :repo => branch.repo) }
5 |
6 | before { stub_sidekiq_logger }
7 |
8 | it "queues up PrMergeabilityChecker for PRs targeting this branch" do
9 | expect(PrMergeabilityChecker).to receive(:perform_async).once.with(pr_branch.id)
10 |
11 | described_class.new.perform(branch.id, ["abcde123"])
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/batch_entry.rb:
--------------------------------------------------------------------------------
1 | class BatchEntry < ActiveRecord::Base
2 | serialize :result
3 | belongs_to :job, :class_name => "BatchJob", :foreign_key => :batch_job_id, :inverse_of => :entries
4 |
5 | validates :state, :inclusion => {:in => %w(started failed succeeded skipped), :allow_nil => true}
6 |
7 | def succeeded?
8 | state == "succeeded"
9 | end
10 |
11 | def failed?
12 | state == "failed"
13 | end
14 |
15 | def skipped?
16 | state == "skipped"
17 | end
18 |
19 | def complete?
20 | failed? || succeeded? || skipped?
21 | end
22 |
23 | def check_job_complete
24 | job.check_complete if complete? && job
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/container-assets/entrypoint:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | source /usr/local/bin/container_env
4 |
5 | mkdir /root/.ssh
6 | cp /root/ssh/* /root/.ssh/
7 | chown 600 /root/.ssh/miq-bot
8 |
9 | pushd /opt/miq_bot/config
10 | ln -s /opt/miq_bot_data/github_notification_monitor.yml
11 |
12 | ln -s /run/secrets/config/master.key
13 | ln -s /run/secrets/config/settings.local.yml
14 | popd
15 |
16 | [[ -n $QUEUE_NAME ]] && COMMAND="sidekiq -q $QUEUE_NAME"
17 | [[ -z $COMMAND ]] && COMMAND="rails server"
18 |
19 | cd /opt/miq_bot
20 |
21 | bundle exec rake db:create
22 | bundle exec rake db:migrate
23 | bundle exec rake assets:precompile
24 |
25 | exec bundle exec ${COMMAND}
26 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/branch_mergeability_checker.rb:
--------------------------------------------------------------------------------
1 | class CommitMonitorHandlers::CommitRange::BranchMergeabilityChecker
2 | include Sidekiq::Worker
3 | sidekiq_options :queue => :miq_bot
4 |
5 | include BranchWorkerMixin
6 |
7 | def self.handled_branch_modes
8 | [:regular]
9 | end
10 |
11 | def perform(branch_id, _new_commits)
12 | return unless find_branch(branch_id, :regular)
13 |
14 | repo.pr_branches.where(:merge_target => branch.name).each do |pr|
15 | logger.info("Queueing PrMergeabilityChecker for PR #{pr.fq_pr_number}.")
16 | PrMergeabilityChecker.perform_async(pr.id)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_with_no_offenses/results.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": {
3 | "rubocop_version": "1.13.0",
4 | "ruby_engine": "ruby",
5 | "ruby_version": "2.7.2",
6 | "ruby_patchlevel": "137",
7 | "ruby_platform": "x86_64-darwin19"
8 | },
9 | "files": [
10 | {
11 | "path": "spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_with_no_offenses/no_offenses.rb",
12 | "offenses": [
13 |
14 | ]
15 | }
16 | ],
17 | "summary": {
18 | "offense_count": 0,
19 | "target_file_count": 1,
20 | "inspected_file_count": 1
21 | }
22 | }
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | prepare:
2 | fetch:
3 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml
4 | path: ".rubocop_base.yml"
5 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_cc_base.yml
6 | path: ".rubocop_cc_base.yml"
7 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/base.yml
8 | path: styles/base.yml
9 | - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml
10 | path: styles/cc_base.yml
11 | plugins:
12 | rubocop:
13 | enabled: true
14 | config: ".rubocop_cc.yml"
15 | channel: rubocop-1-56-3
16 | version: '2'
17 |
--------------------------------------------------------------------------------
/lib/github_service/response/ratelimit_logger.rb:
--------------------------------------------------------------------------------
1 | require 'faraday'
2 |
3 | module GithubService
4 | module Response
5 | class RatelimitLogger < Faraday::Middleware
6 | attr_accessor :logger
7 |
8 | def initialize(app, logger = nil)
9 | super(app)
10 | @logger = logger || begin
11 | require 'logger'
12 | ::Logger.new(STDOUT)
13 | end
14 | end
15 |
16 | def on_complete(env)
17 | api_calls_remaining = env.response_headers['x-ratelimit-remaining']
18 | logger.info { "Executed #{env.method.to_s.upcase} #{env.url}...api calls remaining #{api_calls_remaining}" }
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/workers/github_notification_monitor_worker.rb:
--------------------------------------------------------------------------------
1 | class GithubNotificationMonitorWorker
2 | include Sidekiq::Worker
3 | sidekiq_options :queue => :miq_bot, :retry => false
4 |
5 | include SidekiqWorkerMixin
6 |
7 | def perform
8 | if !first_unique_worker?
9 | logger.info "#{self.class} is already running, skipping"
10 | else
11 | process_repos
12 | end
13 | end
14 |
15 | def process_repos
16 | enabled_repos.each { |repo| process_repo(repo) }
17 | end
18 |
19 | def process_repo(repo)
20 | GithubNotificationMonitor.new(repo.name).process_notifications
21 | rescue => err
22 | logger.error err.message
23 | logger.error err.backtrace.join("\n")
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require_self
12 | *= require_tree .
13 | */
14 |
15 | .page-footer {
16 | padding-top: 9px;
17 | margin: 20px 0 40px;
18 | border-top: 1px solid #eeeeee;
19 | }
20 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require turbolinks
16 | //= require_tree .
17 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/threadsafe_service_mixin_examples.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for "ThreadsafeServiceMixin service" do
2 | context ".new" do
3 | it "is private" do
4 | expect { described_class.new }.to raise_error(NoMethodError)
5 | end
6 | end
7 |
8 | context ".call" do
9 | it "will synchronize multiple callers" do
10 | t = Thread.new do
11 | with_service do |_service|
12 | Thread.current[:locked] = true
13 | sleep 0.01 until Thread.current[:release]
14 | end
15 | end
16 | t.abort_on_exception = true
17 | sleep 0.01 until t[:locked]
18 |
19 | expect(described_class.send(:mutex)).to be_locked
20 |
21 | t[:release] = true
22 | t.join
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/main/_commit_monitor.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | | Repository |
6 | Branch |
7 | Last Commit |
8 | Last Checked |
9 | Last Changed |
10 |
11 |
12 |
13 | <% @branches.each do |branch| %>
14 |
15 | | <%= branch.fq_repo_name %> |
16 | <%= branch.name %> |
17 | <%= link_to(branch.last_commit[0, 8], branch.last_commit_uri, :target => "_blank") %> |
18 | <%= time_ago_in_words_with_nil_check(branch.last_checked_on) %> |
19 | <%= time_ago_in_words_with_nil_check(branch.last_changed_on) %> |
20 |
21 | <% end %>
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/linter/rubocop.rb:
--------------------------------------------------------------------------------
1 | require 'rubocop'
2 |
3 | module Linter
4 | class Rubocop < Base
5 | CONFIG_FILES = %w[.rubocop.yml .rubocop_base.yml .rubocop_local.yml].freeze
6 |
7 | private
8 |
9 | def config_files
10 | CONFIG_FILES
11 | end
12 |
13 | def linter_executable
14 | 'rubocop'
15 | end
16 |
17 | def options
18 | {:format => 'json', :no_display_cop_names => nil}
19 | end
20 |
21 | def filtered_files(files)
22 | files.select do |file|
23 | file.end_with?(".rb") ||
24 | file.end_with?(".ru") ||
25 | file.end_with?(".rake") ||
26 | File.basename(file).in?(%w(Gemfile Rakefile))
27 | end.reject do |file|
28 | file.end_with?("db/schema.rb")
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/github_service/commands/add_reviewer.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class AddReviewer < Base
4 | alias_as 'request_review'
5 |
6 | private
7 |
8 | def _execute(issuer:, value:)
9 | users = value.strip.delete('@').split(/\s*,\s*/)
10 |
11 | valid_users, invalid_users = users.partition { |u| valid_assignee?(u) }
12 |
13 | if valid_users.any?
14 | issue.add_reviewer(valid_users)
15 | end
16 |
17 | if invalid_users.any?
18 | message = "@#{issuer} Cannot add the following #{"reviewer".pluralize(invalid_users.size)} because they are not recognized: "
19 | message << invalid_users.join(", ")
20 | issue.add_comment(message)
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/workers/concerns/batch_job_worker_mixin.rb:
--------------------------------------------------------------------------------
1 | module BatchJobWorkerMixin
2 | extend ActiveSupport::Concern
3 |
4 | module ClassMethods
5 | def perform_batch_async(*args)
6 | BatchJob.perform_async(batch_workers, args,
7 | :on_complete_class => self,
8 | :on_complete_args => args,
9 | :expires_at => 5.minutes.from_now
10 | )
11 | end
12 | end
13 |
14 | attr_reader :batch_job
15 |
16 | def find_batch_job(batch_job_id)
17 | @batch_job = BatchJob.where(:id => batch_job_id).first
18 |
19 | if @batch_job.nil?
20 | logger.warn("BatchJob #{batch_job_id} no longer exists. Skipping.")
21 | return false
22 | end
23 |
24 | true
25 | end
26 |
27 | def complete_batch_job
28 | batch_job.destroy
29 | end
30 | alias skip_batch_job complete_batch_job
31 | end
32 |
--------------------------------------------------------------------------------
/db/migrate/20140128174930_replace_timestamps_with_last_checked_on_and_last_changed_on_for_commit_monitor_branches.rb:
--------------------------------------------------------------------------------
1 | class ReplaceTimestampsWithLastCheckedOnAndLastChangedOnForCommitMonitorBranches < ActiveRecord::Migration[4.2]
2 | class CommitMonitorBranch < ActiveRecord::Base
3 | end
4 |
5 | def change
6 | add_column :commit_monitor_branches, :last_checked_on, :timestamp
7 | add_column :commit_monitor_branches, :last_changed_on, :timestamp
8 |
9 | say_with_time("Moving commit_monitor_branches.updated_at to commit_monitor_branches.last_changed_on") do
10 | CommitMonitorBranch.all.each do |b|
11 | b.update!(:last_changed_on => b.updated_at)
12 | end
13 | end
14 |
15 | remove_column :commit_monitor_branches, :created_at, :timestamp
16 | remove_column :commit_monitor_branches, :updated_at, :timestamp
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/db/migrate/20150827194533_collapse_repo_upstream_user_into_name.rb:
--------------------------------------------------------------------------------
1 | class CollapseRepoUpstreamUserIntoName < ActiveRecord::Migration[4.2]
2 | class Repo < ActiveRecord::Base
3 | end
4 |
5 | def up
6 | say_with_time("Collapse Repo upstream_user into name") do
7 | Repo.all.each do |r|
8 | r.update!(:name => [r.upstream_user, r.name].compact.join("/"))
9 | end
10 | end
11 |
12 | remove_column :repos, :upstream_user
13 | end
14 |
15 | def down
16 | add_column :repos, :upstream_user, :string
17 |
18 | say_with_time("Split out Repo upstream_user from name") do
19 | Repo.all.each do |r|
20 | r.update!(:upstream_user => name_parts(r.name).first, :name => name_parts(r.name).last)
21 | end
22 | end
23 | end
24 |
25 | def name_parts(name)
26 | name.split("/", 2).unshift(nil).last(2)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/github_service/commands/set_milestone.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class SetMilestone < Base
4 | private
5 |
6 | def _execute(issuer:, value:)
7 | milestone = value.strip
8 |
9 | if valid_milestone?(milestone)
10 | issue.set_milestone(milestone)
11 | else
12 | issue.add_comment("@#{issuer} Milestone #{milestone} is not recognized, ignoring...")
13 | end
14 | end
15 |
16 | def valid_milestone?(milestone)
17 | # First reload the cache if it's an invalid milestone
18 | GithubService.refresh_milestones(issue.fq_repo_name) unless GithubService.valid_milestone?(issue.fq_repo_name, milestone)
19 |
20 | # Then see if it's *still* invalid
21 | GithubService.valid_milestone?(issue.fq_repo_name, milestone)
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/workers/concerns/code_analysis_mixin.rb:
--------------------------------------------------------------------------------
1 | module CodeAnalysisMixin
2 | def merged_linter_results
3 | results = {
4 | "files" => [],
5 | "summary" => {
6 | "inspected_file_count" => 0,
7 | "offense_count" => 0,
8 | "target_file_count" => 0,
9 | },
10 | }
11 |
12 | run_all_linters.each do |result|
13 | %w(offense_count target_file_count inspected_file_count).each do |m|
14 | results['summary'][m] += result['summary'][m]
15 | end
16 | results['files'] += result['files']
17 | end
18 |
19 | results
20 | end
21 |
22 | def run_all_linters
23 | unmerged_results = []
24 | unmerged_results << Linter::Rubocop.new(branch).run
25 | unmerged_results << Linter::Haml.new(branch).run
26 | unmerged_results << Linter::Yaml.new(branch).run
27 | unmerged_results.tap(&:compact!)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MIQ Bot
5 | <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
6 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
7 |
8 |
9 |
10 |
11 |
12 |
13 | <%= csrf_meta_tags %>
14 |
15 |
16 |
17 | <%= render "layouts/navbar" %>
18 | <%= yield %>
19 | <%= render "layouts/footer" %>
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/workers/concerns/batch_entry_worker_mixin.rb:
--------------------------------------------------------------------------------
1 | module BatchEntryWorkerMixin
2 | attr_reader :batch_entry
3 |
4 | def find_batch_entry(batch_entry_id)
5 | @batch_entry = BatchEntry.where(:id => batch_entry_id).first
6 |
7 | if @batch_entry.nil?
8 | logger.warn("BatchEntry #{batch_entry_id} no longer exists. Skipping.")
9 | return false
10 | end
11 |
12 | true
13 | end
14 |
15 | def batch_job
16 | batch_entry.job
17 | end
18 |
19 | def complete_batch_entry(updates = {})
20 | update_batch_entry(updates)
21 | check_job_complete
22 | end
23 |
24 | def skip_batch_entry
25 | complete_batch_entry(:state => "skipped")
26 | end
27 |
28 | private
29 |
30 | def update_batch_entry(updates)
31 | updates = updates.reverse_merge(:state => "succeeded")
32 | batch_entry.update!(updates)
33 | end
34 |
35 | def check_job_complete
36 | batch_entry.check_job_complete
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/path_based_labeler.rb:
--------------------------------------------------------------------------------
1 | class CommitMonitorHandlers::CommitRange::PathBasedLabeler
2 | include Sidekiq::Worker
3 | sidekiq_options :queue => :miq_bot
4 |
5 | include BranchWorkerMixin
6 |
7 | def self.handled_branch_modes
8 | [:pr]
9 | end
10 |
11 | def perform(branch_id, _new_commits)
12 | return unless find_branch(branch_id, :pr)
13 |
14 | process_branch
15 | end
16 |
17 | private
18 |
19 | def process_branch
20 | labels = []
21 | label_rules.each do |rule|
22 | if diff_file_names.any? { |file_name| file_name =~ rule.regex }
23 | labels << rule.label
24 | end
25 | end
26 | GithubService.add_labels_to_an_issue(fq_repo_name, pr_number, labels) if labels.present?
27 | rescue GitService::UnmergeableError
28 | # Ignore unmergeable PRs
29 | end
30 |
31 | def label_rules
32 | Settings.path_based_labeler.rules[fq_repo_name] || []
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/workers/pull_request_monitor_handlers/merge_target_titler.rb:
--------------------------------------------------------------------------------
1 | module PullRequestMonitorHandlers
2 | class MergeTargetTitler
3 | include Sidekiq::Worker
4 | sidekiq_options :queue => :miq_bot
5 |
6 | include BranchWorkerMixin
7 |
8 | def perform(branch_id)
9 | return unless find_branch(branch_id, :pr)
10 |
11 | process_branch
12 | end
13 |
14 | private
15 |
16 | def process_branch
17 | apply_title if merge_target != "master" && !already_titled?
18 | end
19 |
20 | def already_titled?
21 | pr_title_tags.map(&:downcase).include?(merge_target.downcase)
22 | end
23 |
24 | def apply_title
25 | logger.info("Updating PR #{pr_number} with title change for merge target #{merge_target}.")
26 | GithubService.update_pull_request(fq_repo_name, pr_number, :title => new_pr_title)
27 | end
28 |
29 | def new_pr_title
30 | "[#{merge_target.upcase}] #{pr_title}"
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_without_column_numbers_and_cop_names/results.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": {
3 | "rubocop_version": "1.13.0",
4 | "ruby_engine": "ruby",
5 | "ruby_version": "2.7.2",
6 | "ruby_patchlevel": "137",
7 | "ruby_platform": "x86_64-darwin19"
8 | },
9 | "files": [
10 | {
11 | "path": "spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_results_without_column_numbers_and_cop_names/example.haml",
12 | "offenses": [
13 | {
14 | "severity": "warning",
15 | "message": "The - symbol should have one space separating it from code",
16 | "location": {
17 | "line": 2
18 | },
19 | "linter_name": "SpaceBeforeScript"
20 | }
21 | ]
22 | }
23 | ],
24 | "summary": {
25 | "offense_count": 1,
26 | "target_file_count": 1,
27 | "inspected_file_count": 1
28 | }
29 | }
--------------------------------------------------------------------------------
/spec/workers/pull_request_monitor_handlers/wip_labeler_spec.rb:
--------------------------------------------------------------------------------
1 | describe PullRequestMonitorHandlers::WipLabeler do
2 | let(:branch) { create(:pr_branch) }
3 |
4 | before do
5 | stub_sidekiq_logger
6 | stub_settings(:wip_labeler => {:enabled_repos => [branch.repo.name]})
7 | end
8 |
9 | context "when the PR title does not have [WIP]" do
10 | it "removes the wip label if it exists" do
11 | expect(GithubService).to receive(:remove_label).with(branch.repo.name, branch.pr_number, "wip")
12 |
13 | described_class.new.perform(branch.id)
14 | end
15 | end
16 |
17 | context "when the PR title has [WIP]" do
18 | it "adds the wip label if it does not exist" do
19 | branch.update(:pr_title => "[WIP] #{branch.pr_title}")
20 |
21 | expect(GithubService).to receive(:add_labels_to_an_issue).with(branch.repo.name, branch.pr_number, ["wip"])
22 |
23 | described_class.new.perform(branch.id)
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/vendor/stubs/parser/current.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Injects a "stub" 'parser/current' file (that should be appended to the Ruby
4 | # $LOADPATH), which avoids the annoying loading the original, which includes
5 | # the unskippable warnings like this:
6 | #
7 | # warning: parser/current is loading parser/ruby27, which recognizes
8 | # warning: 2.7.2-compliant syntax, but you are running 2.7.1.
9 | # warning: please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
10 | #
11 | # in STDERR, which are flagged by the bot as errors (when they aren't useful to
12 | # the end user).
13 | #
14 | # This stub will just define the `Parser::CurrentRuby` constant manually using
15 | # the conventions of the `parser` gem.
16 | #
17 |
18 | require "parser/ruby#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}"
19 |
20 | module Parser
21 | CurrentRuby = self.const_get("Ruby#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}")
22 | end
23 |
--------------------------------------------------------------------------------
/lib/service_mixin.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/module/delegation"
2 | require "active_support/concern"
3 |
4 | module ServiceMixin
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | # Hide new in favor of using .call with block for consistency with No
9 | private_class_method :new
10 |
11 | class << self
12 | attr_accessor :credentials, :logger
13 | end
14 | delegate :credentials, :logger, :to => self
15 | end
16 |
17 | module ClassMethods
18 | def call(*options)
19 | raise "no block given" unless block_given?
20 | yield new(*options)
21 | end
22 | end
23 |
24 | private
25 |
26 | def delegate_to_service(method_name, *args)
27 | service.send(method_name, *args)
28 | end
29 |
30 | def method_missing(method_name, *args) # rubocop:disable Style/MethodMissing
31 | delegate_to_service(method_name, *args)
32 | end
33 |
34 | def respond_to_missing?(*args)
35 | service.respond_to?(*args)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_haml_file_using_haml-lint/results.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": {
3 | "rubocop_version": "1.13.0",
4 | "ruby_engine": "ruby",
5 | "ruby_version": "2.7.2",
6 | "ruby_patchlevel": "137",
7 | "ruby_platform": "x86_64-darwin19"
8 | },
9 | "files": [
10 | {
11 | "path": "spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_haml_file_using_haml-lint/example.haml",
12 | "offenses": [
13 | {
14 | "severity": "error",
15 | "message": "You don't need to use \"- end\" in Haml. Un-indent to close a block:\n- if foo?\n %strong Foo!\n- else\n Not foo.\n%p This line is un-indented, so it isn't part of the \"if\" block",
16 | "location": {
17 | "line": 3
18 | },
19 | "linter_name": "Syntax"
20 | }
21 | ]
22 | }
23 | ],
24 | "summary": {
25 | "offense_count": 1,
26 | "target_file_count": 1,
27 | "inspected_file_count": 1
28 | }
29 | }
--------------------------------------------------------------------------------
/lib/github_service/concerns/is_team_member.rb:
--------------------------------------------------------------------------------
1 | module IsTeamMember
2 | def triage_member?(username)
3 | IsTeamMember.triage_team_members.include?(username)
4 | end
5 |
6 | # List of usernames for the triage team
7 | #
8 | # Cache triage_team_members, and refresh cache every 24 hours
9 | #
10 | # Note: This is created as a class method
11 | #
12 | cache_with_timeout(:triage_team_members, 24 * 60 * 60) do
13 | if member_organization_name && triage_team_name
14 | team = GithubService.org_teams(member_organization_name)
15 | .detect { |t| t.name == triage_team_name }
16 |
17 | if team.nil?
18 | []
19 | else
20 | GithubService.team_members(team.id).map(&:login)
21 | end
22 | else
23 | []
24 | end
25 | end
26 |
27 | module_function
28 |
29 | def triage_team_name
30 | @triage_team_name ||= Settings.triage_team_name || nil
31 | end
32 |
33 | def member_organization_name
34 | @member_organization_name ||= Settings.member_organization_name || nil
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | .ruby-version
8 | .rubocop-*
9 |
10 | # Ignore bundler config.
11 | /.bundle
12 |
13 | # Ignore the default SQLite database.
14 | /db/*.sqlite3
15 | /db/*.sqlite3-journal
16 |
17 | # Ignore all logfiles and tempfiles.
18 | /log/**/*.log
19 | /tmp
20 | /spec/tmp
21 |
22 | # Ignore configuration files
23 | /config/data_backup
24 | /config/database.yml
25 | /config/github_notification_monitor.yml
26 | /config/settings.local.yml
27 | /config/settings/*.local.yml
28 | /config/environments/*.local.yml
29 |
30 | # Ignore development Procfile
31 | /Procfile
32 |
33 | # Ignore production artifacts
34 | /public/assets
35 |
36 | # Ignore local repos folder
37 | /repos
38 |
39 | # Ignore local seeds file
40 | db/seeds.local.rb
41 |
42 | /config/master.key
43 |
--------------------------------------------------------------------------------
/app/workers/pull_request_monitor_handlers/wip_labeler.rb:
--------------------------------------------------------------------------------
1 | module PullRequestMonitorHandlers
2 | class WipLabeler
3 | include Sidekiq::Worker
4 | sidekiq_options :queue => :miq_bot
5 |
6 | include BranchWorkerMixin
7 |
8 | LABEL = "wip".freeze
9 |
10 | def perform(branch_id)
11 | return unless find_branch(branch_id, :pr)
12 |
13 | process_branch
14 | end
15 |
16 | private
17 |
18 | def process_branch
19 | if wip_in_title?
20 | apply_label
21 | else
22 | remove_label
23 | end
24 | end
25 |
26 | def wip_in_title?
27 | pr_title_tags.map(&:downcase).include?(LABEL)
28 | end
29 |
30 | def apply_label
31 | logger.info("Updating PR #{pr_number} with label #{LABEL.inspect}.")
32 | GithubService.add_labels_to_an_issue(fq_repo_name, pr_number, [LABEL])
33 | end
34 |
35 | def remove_label
36 | logger.info("Updating PR #{pr_number} without label #{LABEL.inspect}.")
37 | GithubService.remove_label(fq_repo_name, pr_number, LABEL)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/db/migrate/20170127173128_fix_limit_in_schema.rb:
--------------------------------------------------------------------------------
1 | class FixLimitInSchema < ActiveRecord::Migration[4.2]
2 | def up
3 | change_column :batch_entries, :state, :string, :limit => 255
4 | change_column :batch_jobs, :on_complete_class, :string, :limit => 255
5 | change_column :batch_jobs, :state, :string, :limit => 255
6 | change_column :branches, :name, :string, :limit => 255
7 | change_column :branches, :commit_uri, :string, :limit => 255
8 | change_column :branches, :last_commit, :string, :limit => 255
9 | change_column :repos, :name, :string, :limit => 255
10 | end
11 |
12 | def down
13 | change_column :batch_entries, :state, :string, :limit => nil
14 | change_column :batch_jobs, :on_complete_class, :string, :limit => nil
15 | change_column :batch_jobs, :state, :string, :limit => nil
16 | change_column :branches, :name, :string, :limit => nil
17 | change_column :branches, :commit_uri, :string, :limit => nil
18 | change_column :branches, :last_commit, :string, :limit => nil
19 | change_column :repos, :name, :string, :limit => nil
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/lib/github_service/commands/assign_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe GithubService::Commands::Assign do
2 | subject { described_class.new(issue) }
3 | let(:issue) { double(:fq_repo_name => "foo/bar") }
4 | let(:command_issuer) { "chessbyte" }
5 |
6 | before do
7 | allow(GithubService).to receive(:valid_assignee?).with("foo/bar", "gooduser") { true }
8 | allow(GithubService).to receive(:valid_assignee?).with("foo/bar", "baduser") { false }
9 | end
10 |
11 | after do
12 | subject.execute!(:issuer => command_issuer, :value => command_value)
13 | end
14 |
15 | context "with a valid user" do
16 | let(:command_value) { "gooduser" }
17 |
18 | it "assigns to that user" do
19 | expect(issue).to receive(:assign).with("gooduser")
20 | end
21 | end
22 |
23 | context "with an invalid user" do
24 | let(:command_value) { "baduser" }
25 |
26 | it "does not assign, reports failure" do
27 | expect(issue).not_to receive(:assign)
28 | expect(issue).to receive(:add_comment).with("@#{command_issuer} 'baduser' is an invalid assignee, ignoring...")
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/.github/workflows/build_and_deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy
2 | on:
3 | push:
4 | tags:
5 | - 'v[0-9]+.[0-9]+.[0-9]+'
6 | jobs:
7 | build_and_deploy:
8 | if: github.repository_owner == 'ManageIQ'
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v6
12 | - name: Set up Ruby
13 | uses: ruby/setup-ruby@v1
14 | with:
15 | ruby-version: "3.1"
16 | bundler-cache: true
17 | - name: Docker login
18 | run: echo ${{ secrets.DOCKER_REGISTRY_PASSWORD }} | docker login docker.io --password-stdin --username ${{ secrets.DOCKER_REGISTRY_USERNAME }}
19 | - name: Build
20 | run: bundle exec rake production:release:build[${{ github.ref_name }}]
21 | - name: Set up kubectl
22 | uses: azure/setup-kubectl@v4
23 | - name: Set up ibmcloud CLI
24 | uses: IBM/actions-ibmcloud-cli@v1
25 | with:
26 | api_key: ${{ secrets.IBMCLOUD_API_KEY }}
27 | region: us-east
28 | group: manageiq
29 | plugins: container-service
30 | - name: Deploy
31 | run: bundle exec rake production:release:deploy[${{ github.ref_name }}]
32 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | puts "\n== Copying sample files =="
21 | unless File.exist?("config/database.yml")
22 | FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap and inline scripts
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/spec/miq_bot_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe MiqBot do
2 | describe ".version" do
3 | before { MiqBot.instance_variable_set(:@version, nil) }
4 | after { MiqBot.instance_variable_set(:@version, nil) }
5 |
6 | it "with git dir present" do
7 | expect(described_class).to receive(:`).with("GIT_DIR=#{File.expand_path("..", __dir__)}/.git git describe --tags").and_return("v0.21.2-91-g6800275\n")
8 |
9 | expect(described_class.version).to eq("v0.21.2-91-g6800275")
10 | end
11 |
12 | context "with git dir not present" do
13 | let!(:tmpdir) do
14 | Pathname.new(Dir.mktmpdir("fake_rails_root")).tap do |tmpdir|
15 | expect(Rails).to receive(:root).at_least(:once).and_return(tmpdir)
16 | end
17 | end
18 | after { FileUtils.rm_rf(tmpdir) }
19 |
20 | context "and VERSION file present" do
21 | before { tmpdir.join("VERSION").write("v0.21.2-91-g6800275\n") }
22 |
23 | it "returns the content of the VERSION file" do
24 | expect(described_class.version).to eq("v0.21.2-91-g6800275")
25 | end
26 | end
27 |
28 | context "and VERSION file not present" do
29 | it "returns nothing" do
30 | expect(described_class.version).to be_empty
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/offense_message/entry.rb:
--------------------------------------------------------------------------------
1 | class OffenseMessage
2 | class Entry
3 | attr_reader :severity
4 | attr_reader :message
5 | attr_reader :group
6 | attr_reader :locator
7 |
8 | SEVERITY = {
9 | :error => ":bomb: :boom: :fire: :fire_engine:",
10 | :warn => ":warning:",
11 | :high => ":exclamation:",
12 | :low => ":grey_exclamation:",
13 | :unknown => ":grey_question:",
14 | }.freeze
15 |
16 | def initialize(severity, message, group = nil, locator = nil)
17 | raise ArgumentError, "severity must be one of #{SEVERITY.keys.join(", ")}" unless SEVERITY.key?(severity)
18 | raise ArgumentError, "message is required" if message.blank?
19 | @severity = severity
20 | @message = message
21 | @group = group
22 | @locator = locator
23 | end
24 |
25 | def <=>(other_line)
26 | sort_attributes <=> other_line.sort_attributes
27 | end
28 |
29 | def to_s
30 | ["- [ ] #{SEVERITY[severity]}", locator, message].compact.join(" - ")
31 | end
32 |
33 | protected
34 |
35 | def sort_attributes
36 | [group.to_s, order_severity, locator.to_s, message.to_s]
37 | end
38 |
39 | private
40 |
41 | def order_severity
42 | SEVERITY.keys.index(severity)
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/git_service/repo.rb:
--------------------------------------------------------------------------------
1 | module GitService
2 | class Repo
3 | def initialize(repo)
4 | @repo = repo
5 | end
6 |
7 | def commit(sha)
8 | GitService::Commit.new(rugged_repo, sha)
9 | end
10 |
11 | def git_fetch
12 | require 'rugged'
13 | rugged_repo.remotes.each do |remote|
14 | fetch_options = {}
15 | username = uri_for_remote(remote.url).user
16 | hostname = uri_for_remote(remote.url).hostname
17 | credentials = Credentials.find_for_user_and_host(username, hostname)
18 |
19 | fetch_options[:credentials] = credentials if credentials
20 |
21 | rugged_repo.fetch(remote.name, **fetch_options)
22 | end
23 | end
24 |
25 | private
26 |
27 | def rugged_repo
28 | @rugged_repo ||= Rugged::Repository.new(@repo.path.to_s)
29 | end
30 |
31 | def uri_for_remote(url)
32 | @remote_uris ||= {}
33 | @remote_uris[url] ||= begin
34 | if url.start_with?("http", "ssh://")
35 | URI(url)
36 | elsif url.match?(/\A[-\w:.]+@.*:/) # exp: git@github.com:org/repo
37 | URI(url.sub(':', '/').prepend("ssh://"))
38 | end
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/message_collector.rb:
--------------------------------------------------------------------------------
1 | require 'stringio'
2 |
3 | class MessageCollector
4 | attr_accessor :max_size, :header, :continuation_header
5 |
6 | def initialize(max_size = nil, header = nil, continuation_header = nil)
7 | @max_size = max_size
8 | @header = header
9 | @continuation_header = continuation_header
10 | @lines = []
11 | end
12 |
13 | def write(line)
14 | if max_size && line.length >= max_size
15 | raise ArgumentError, "line length must be less than #{max_size}"
16 | end
17 | @lines << line
18 | end
19 |
20 | def write_lines(lines)
21 | lines.each { |l| write(l) }
22 | end
23 |
24 | def comments
25 | build_comments
26 | @comments.collect(&:string)
27 | end
28 |
29 | private
30 |
31 | def build_comments
32 | @comments = []
33 | start_new_comment(header)
34 | @lines.each { |line| add_to_comment(line) }
35 | end
36 |
37 | def start_new_comment(comment_header)
38 | @comment = StringIO.new
39 | @comments << @comment
40 | add_to_comment(comment_header) if comment_header
41 | end
42 |
43 | def add_to_comment(line)
44 | start_new_comment(continuation_header) if will_exceed_comment_max_size?(line)
45 | @comment.puts(line)
46 | end
47 |
48 | def will_exceed_comment_max_size?(line)
49 | max_size ? (@comment.length + line.length + 1 >= max_size) : false
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/rubocop_checker.rb:
--------------------------------------------------------------------------------
1 | require 'rugged'
2 |
3 | class CommitMonitorHandlers::CommitRange::RubocopChecker
4 | include Sidekiq::Worker
5 | sidekiq_options :queue => :miq_bot_glacial
6 |
7 | include BranchWorkerMixin
8 | include CodeAnalysisMixin
9 |
10 | def self.handled_branch_modes
11 | [:pr]
12 | end
13 |
14 | attr_reader :results
15 |
16 | def perform(branch_id, _new_commits)
17 | return unless find_branch(branch_id, :pr)
18 |
19 | process_branch
20 | end
21 |
22 | private
23 |
24 | def process_branch
25 | @results = merged_linter_results
26 | unless @results["files"].blank?
27 | diff_details = diff_details_for_merge
28 | @results = RubocopResultsFilter.new(results, diff_details).filtered
29 | end
30 |
31 | replace_rubocop_comments
32 | rescue GitService::UnmergeableError
33 | nil # Avoid working on unmergeable PRs
34 | end
35 |
36 | def rubocop_comments
37 | MessageBuilder.new(results, branch).comments
38 | end
39 |
40 | def replace_rubocop_comments
41 | logger.info("Updating PR #{pr_number} with rubocop comment.")
42 | GithubService.replace_comments(fq_repo_name, pr_number, rubocop_comments) do |old_comment|
43 | rubocop_comment?(old_comment)
44 | end
45 | end
46 |
47 | def rubocop_comment?(comment)
48 | comment.body.start_with?("")
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/github_service/commands/unassign.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class Unassign < Base
4 | private
5 |
6 | def _execute(issuer:, value:)
7 | users = value.strip.delete('@').split(/\s*,\s*/)
8 |
9 | assgined_users = list_assigned_users
10 |
11 | valid_users, invalid_users = users.partition { |u| assgined_users.include?(u) }
12 |
13 | if valid_users.any?
14 | octokit_remove_assignees(issue.fq_repo_name, issue.number, valid_users)
15 | end
16 |
17 | if invalid_users.any?
18 | message = "@#{issuer} #{"User".pluralize(invalid_users.size)} '#{invalid_users.join(", ")}' #{invalid_users.size == 1 ? "is" : "are"} not in the list of assignees, ignoring..."
19 | issue.add_comment(message)
20 | end
21 | end
22 |
23 | def list_assigned_users
24 | GithubService.issue(issue.fq_repo_name, issue.number).assignees.map(&:login)
25 | end
26 |
27 | # FIXME: NoMethodError on `remove_assignees`
28 | # https://github.com/octokit/octokit.rb/blob/master/lib/octokit/client/issues.rb#L346
29 | def octokit_remove_assignees(repo, number, assignees, options = {})
30 | service = GithubService.instance_variable_get("@service")
31 | service.delete("repos/#{repo}/issues/#{number}/assignees", options.merge(:assignees => assignees))
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/rubocop_checker/rubocop_results_filter.rb:
--------------------------------------------------------------------------------
1 | module CommitMonitorHandlers
2 | module CommitRange
3 | class RubocopChecker
4 | class RubocopResultsFilter
5 | attr_reader :filtered
6 |
7 | def initialize(results, diff_details)
8 | @results = results
9 | @diff_details = diff_details
10 | @filtered = filter_rubocop_results
11 | end
12 |
13 | private
14 |
15 | def filter_rubocop_results
16 | filter_on_diff
17 | filter_void_warnings_in_spec_files
18 |
19 | @results["summary"]["offense_count"] =
20 | @results["files"].inject(0) { |sum, f| sum + f["offenses"].length }
21 |
22 | @results
23 | end
24 |
25 | def filter_on_diff
26 | @results["files"].each do |f|
27 | f["offenses"].select! do |o|
28 | o["severity"].in?(%w(error fatal)) ||
29 | @diff_details[f["path"]].include?(o["location"]["line"])
30 | end
31 | end
32 | end
33 |
34 | def filter_void_warnings_in_spec_files
35 | @results["files"].each do |f|
36 | next unless f["path"].match %r{(?:^|/)spec/.+_spec.rb}
37 |
38 | f["offenses"].reject! do |o|
39 | o["cop_name"].in?(%w(Void Lint/Void))
40 | end
41 | end
42 | end
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/git_service/credentials.rb:
--------------------------------------------------------------------------------
1 | module GitService
2 | class Credentials
3 | # Example:
4 | #
5 | # GitService::Credentials.host_config = {
6 | # '*' => {
7 | # :username => 'git',
8 | # :private_key => '~/.ssh/id_rsa'
9 | # },
10 | # 'github.com' => {
11 | # :username => 'git',
12 | # :private_key => '~/.ssh/id_rsa'
13 | # }
14 | # }
15 | #
16 | def self.host_config=(host_config = {})
17 | @host_config = host_config.to_h
18 | end
19 |
20 | # Generic method for finding git credentials based on what is available
21 | #
22 | # Will use ssh_agent if all other options have been exhausted.
23 | #
24 | def self.find_for_user_and_host(username, hostname)
25 | from_ssh_config(username, hostname) || from_ssh_agent(username)
26 | end
27 |
28 | def self.from_ssh_config(username, hostname)
29 | ssh_config = Net::SSH::Config.for(hostname)
30 |
31 | return if ssh_config.empty? || ssh_config[:keys].nil?
32 |
33 | ssh_config[:username] = username || ssh_config[:user] # favor URL username if present
34 | ssh_config[:privatekey] = File.expand_path(ssh_config[:keys].first) # only use first key
35 |
36 | Rugged::Credentials::SshKey.new(ssh_config)
37 | end
38 |
39 | def self.from_ssh_agent(username)
40 | Rugged::Credentials::SshKeyFromAgent.new(:username => username)
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/github_pr_commenter/diff_filename_checker.rb:
--------------------------------------------------------------------------------
1 | require 'rugged'
2 |
3 | module CommitMonitorHandlers::CommitRange
4 | class GithubPrCommenter::DiffFilenameChecker
5 | include Sidekiq::Worker
6 | sidekiq_options :queue => :miq_bot_glacial
7 |
8 | include BatchEntryWorkerMixin
9 | include BranchWorkerMixin
10 |
11 | def perform(batch_entry_id, branch_id, _new_commits)
12 | return unless find_batch_entry(batch_entry_id)
13 | return skip_batch_entry unless find_branch(branch_id, :pr)
14 |
15 | complete_batch_entry(:result => process_files)
16 | end
17 |
18 | private
19 |
20 | def process_files
21 | @offenses = []
22 |
23 | check_diff_files
24 |
25 | @offenses
26 | end
27 |
28 | def check_diff_files
29 | branch.git_service.diff.new_files.each do |file|
30 | validate_migration_timestamp(file)
31 | end
32 | rescue GitService::UnmergeableError
33 | nil # Avoid working on unmergeable PRs
34 | end
35 |
36 | def validate_migration_timestamp(file)
37 | return unless file.include?("db/migrate/")
38 | ts = File.basename(file).split("_").first
39 | return if valid_timestamp?(ts)
40 |
41 | @offenses << OffenseMessage::Entry.new(:error, "Bad Migration Timestamp", file)
42 | end
43 |
44 | def valid_timestamp?(ts)
45 | Time.parse(ts)
46 | rescue ArgumentError
47 | false
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
48 |
49 |
50 |
51 |
52 |
53 |
We're sorry, but something went wrong.
54 |
55 | If you are the application owner check the logs for more information.
56 |
57 |
58 |
--------------------------------------------------------------------------------
/spec/workers/pull_request_monitor_handlers/merge_target_titler_spec.rb:
--------------------------------------------------------------------------------
1 | describe PullRequestMonitorHandlers::MergeTargetTitler do
2 | let(:branch) { create(:pr_branch) }
3 |
4 | before do
5 | stub_sidekiq_logger
6 | stub_settings(:merge_target_titler => {:enabled_repos => [branch.repo.name]})
7 | end
8 |
9 | context "when the branch has a non-master merge target" do
10 | before do
11 | branch.update!(:merge_target => "darga")
12 | end
13 |
14 | it "and not already titled" do
15 | expect(GithubService).to receive(:update_pull_request)
16 | .with(branch.repo.name, branch.pr_number, a_hash_including(:title => "[DARGA] #{branch.pr_title}"))
17 | described_class.new.perform(branch.id)
18 | end
19 |
20 | it "and already titled" do
21 | branch.update!(:pr_title => "[DARGA] #{branch.pr_title}")
22 |
23 | expect(GithubService).to_not receive(:pull_requests)
24 |
25 | described_class.new.perform(branch.id)
26 | end
27 |
28 | it "and already titled, but not at the start" do
29 | branch.update!(:pr_title => "[WIP] [DARGA] #{branch.pr_title}")
30 |
31 | expect(GithubService).to_not receive(:pull_requests)
32 |
33 | described_class.new.perform(branch.id)
34 | end
35 | end
36 |
37 | it "when the branch has a master merge target" do
38 | branch.update!(:merge_target => "master")
39 |
40 | expect(GithubService).to_not receive(:pull_requests)
41 |
42 | described_class.new.perform(branch.id)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
48 |
49 |
50 |
51 |
52 |
53 |
The change you wanted was rejected.
54 |
Maybe you tried to change something you didn't have access to.
55 |
56 | If you are the application owner check the logs for more information.
57 |
58 |
59 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module MiqBot
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 7.0
13 |
14 | # Configuration for the application, engines, and railties goes here.
15 | #
16 | # These settings can be overridden in specific environments using the files
17 | # in config/environments, which are processed later.
18 | #
19 | # config.time_zone = "Central Time (US & Canada)"
20 | # config.eager_load_paths << Rails.root.join("extras")
21 |
22 | config.eager_load_paths << Rails.root.join("app/workers/concerns").to_s
23 | config.eager_load_paths << Rails.root.join("lib/github_service/concerns").to_s
24 | config.eager_load_paths << Rails.root.join("lib").to_s
25 |
26 | # Use yaml_unsafe_load for column serialization to handle Symbols
27 | config.active_record.use_yaml_unsafe_load = true
28 |
29 | console do
30 | TOPLEVEL_BINDING.eval('self').extend(ConsoleMethods)
31 | end
32 | end
33 |
34 | def self.version
35 | @version ||=
36 | if Rails.root.join('.git').exist?
37 | `GIT_DIR=#{Rails.root.join('.git')} git describe --tags`.chomp
38 | elsif Rails.root.join("VERSION").exist?
39 | Rails.root.join("VERSION").read.chomp
40 | else
41 | ""
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
48 |
49 |
50 |
51 |
52 |
53 |
The page you were looking for doesn't exist.
54 |
You may have mistyped the address or the page may have moved.
55 |
56 | If you are the application owner check the logs for more information.
57 |
58 |
59 |
--------------------------------------------------------------------------------
/spec/lib/message_collector_spec.rb:
--------------------------------------------------------------------------------
1 | describe MessageCollector do
2 | let(:max_size) { 1000 }
3 | let(:header) { "header stuff\n\n" }
4 | let(:continuation_header) { "continued...\n\n" }
5 | subject { described_class.new(max_size, header, continuation_header) }
6 |
7 | describe "#write / #comments" do
8 | it "with simple line" do
9 | subject.write("a line")
10 | expect(subject.comments).to eq ["#{header}a line\n"]
11 | end
12 |
13 | it "with a line that's too long" do
14 | expect { subject.write("a" * max_size) }.to raise_error(ArgumentError)
15 | end
16 |
17 | it "with a line that will warp to a new comment" do
18 | line = "a" * (max_size - header.length)
19 | subject.write("a line")
20 | subject.write(line)
21 | expect(subject.comments).to eq [
22 | "#{header}a line\n",
23 | "#{continuation_header}#{line}\n"
24 | ]
25 | end
26 | end
27 |
28 | describe "#write_lines / #comments" do
29 | it "with simple lines" do
30 | subject.write_lines(["a line", "another line"])
31 | expect(subject.comments).to eq ["#{header}a line\nanother line\n"]
32 | end
33 |
34 | it "with a line that's too long" do
35 | expect { subject.write_lines(["a line", "a" * max_size]) }.to raise_error(ArgumentError)
36 | end
37 |
38 | it "with a line that will wrap to a new comment" do
39 | line = "a" * (max_size - header.length)
40 | subject.write_lines(["a line", line])
41 | expect(subject.comments).to eq [
42 | "#{header}a line\n",
43 | "#{continuation_header}#{line}\n"
44 | ]
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: '0 0 * * 0'
8 |
9 | jobs:
10 | ci:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | ruby-version:
15 | - '3.1'
16 | services:
17 | postgres:
18 | image: manageiq/postgresql:13
19 | env:
20 | POSTGRESQL_USER: root
21 | POSTGRESQL_PASSWORD: smartvm
22 | POSTGRESQL_DATABASE: miq_bot_test
23 | options: --health-cmd pg_isready --health-interval 2s --health-timeout 5s --health-retries 5
24 | ports:
25 | - 5432:5432
26 | redis:
27 | image: redis
28 | options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
29 | ports:
30 | - 6379:6379
31 | env:
32 | PGUSER: root
33 | PGPASSWORD: smartvm
34 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
35 | steps:
36 | - uses: actions/checkout@v6
37 | - name: Set up system
38 | run: bin/before_install
39 | - name: Set up Ruby
40 | uses: ruby/setup-ruby@v1
41 | with:
42 | ruby-version: ${{ matrix.ruby-version }}
43 | bundler-cache: true
44 | timeout-minutes: 30
45 | - name: Prepare tests
46 | run: bin/setup
47 | - name: Run tests
48 | run: bundle exec rake
49 | - name: Report code coverage
50 | if: ${{ github.ref == 'refs/heads/master' && matrix.ruby-version == '3.1' }}
51 | continue-on-error: true
52 | uses: paambaati/codeclimate-action@v9
53 | - name: Container Build
54 | run: docker build .
55 |
--------------------------------------------------------------------------------
/lib/github_service/commands/remove_reviewer.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class RemoveReviewer < Base
4 | private
5 |
6 | def _execute(issuer:, value:)
7 | user = value.strip.delete('@')
8 |
9 | if valid_assignee?(user)
10 | if requested_reviewers.include?(user)
11 | # FIXME: waiting for merge of https://github.com/octokit/octokit.rb/pull/990
12 | begin
13 | issue.remove_reviewer(user)
14 | rescue NoMethodError
15 | # TODO: Remove this exception handling after dependence merge.
16 | octokit_request_pull_request_review(issue.fq_repo_name, issue.number, "reviewers" => [user])
17 | end
18 | else
19 | issue.add_comment("@#{issuer} '#{user}' is not in the list of requested reviewers, ignoring...")
20 | end
21 | else
22 | issue.add_comment("@#{issuer} '#{user}' is an invalid reviewer, ignoring...")
23 | end
24 | end
25 |
26 | # returns an array of user logins who were requested for a pull request review
27 | def requested_reviewers
28 | GithubService.pull_request_review_requests(issue.fq_repo_name, issue.number).users.map(&:login)
29 | end
30 |
31 | # TODO: Remove this.
32 | def octokit_request_pull_request_review(repo, id, reviewers, options = {})
33 | service = GithubService.instance_variable_get("@service")
34 | options = options.merge(:reviewers => reviewers.values.flatten)
35 | service.delete("repos/#{repo}/pulls/#{id}/requested_reviewers", options)
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/support/rubocop_spec_helper.rb:
--------------------------------------------------------------------------------
1 | def rubocop_results
2 | # To regenerate the results.json files, just delete them
3 | unless File.exist?(rubocop_json_file)
4 | rubocop = JSON.parse(`rubocop --config #{rubocop_check_config} --format=json --no-display-cop-names #{rubocop_check_path}`)
5 | hamllint = JSON.parse(`haml-lint --reporter=json #{rubocop_check_path}`)
6 |
7 | %w(offense_count target_file_count inspected_file_count).each do |m|
8 | rubocop['summary'][m] += hamllint['summary'][m]
9 | end
10 | rubocop['files'] += hamllint['files']
11 | File.write(rubocop_json_file, JSON.pretty_generate(rubocop))
12 | end
13 | JSON.parse(File.read(rubocop_json_file))
14 | end
15 |
16 | def rubocop_check_config
17 | rubocop_check_path.join("..", ".rubocop.yml").expand_path
18 | end
19 |
20 | def rubocop_json_file
21 | rubocop_check_path.join("results.json")
22 | end
23 |
24 | def rubocop_check_path
25 | Pathname.new(@example.file_path).expand_path.dirname.join("data", rubocop_check_directory).relative_path_from(Rails.root)
26 | end
27 |
28 | def rubocop_check_path_file(file)
29 | rubocop_check_path.join(file)
30 | end
31 |
32 | def rubocop_check_directory
33 | @example.description.gsub(" ", "_")
34 | end
35 |
36 | def rubocop_version
37 | RuboCop::Version.version
38 | end
39 |
40 | def rubocop_version_short
41 | RuboCop::Version.document_version
42 | end
43 |
44 | def rubocop_rails_version_short
45 | RuboCop::Rails::Version.document_version
46 | end
47 |
48 | def hamllint_version
49 | HamlLint::VERSION
50 | end
51 |
52 | def yamllint_version
53 | _out, err, _ps = Open3.capture3("yamllint -v")
54 | err.split.last
55 | end
56 |
--------------------------------------------------------------------------------
/spec/workers/concerns/batch_job_worker_mixin_spec.rb:
--------------------------------------------------------------------------------
1 | describe BatchJobWorkerMixin do
2 | let(:includer_class) do
3 | Class.new do
4 | include BatchJobWorkerMixin
5 |
6 | def self.batch_workers
7 | @batch_workers ||= [Class.new, Class.new]
8 | end
9 |
10 | def logger
11 | @logger ||= RSpec::Mocks::Double.new("logger")
12 | end
13 | end
14 | end
15 |
16 | subject { includer_class.new }
17 | let(:job) { BatchJob.create! }
18 |
19 | it ".perform_batch_async" do
20 | expect(BatchJob).to receive(:perform_async) do |workers, worker_args, job_attributes|
21 | expect(workers).to eq(includer_class.batch_workers)
22 | expect(worker_args).to eq(%w(arg1 arg2))
23 |
24 | expect(job_attributes[:on_complete_class]).to eq(includer_class)
25 | expect(job_attributes[:on_complete_args]).to eq(%w(arg1 arg2))
26 | expect(job_attributes[:expires_at]).to be_kind_of(Time)
27 | end
28 |
29 | includer_class.perform_batch_async("arg1", "arg2")
30 | end
31 |
32 | describe "#find_batch_job" do
33 | it "with an existing job" do
34 | expect(subject.find_batch_job(job.id)).to be true
35 | end
36 |
37 | it "with a missing job" do
38 | expect(subject.logger).to receive(:warn) do |message|
39 | expect(message).to match(/no longer exists/)
40 | end
41 | expect(subject.find_batch_job(-1)).to be false
42 | end
43 | end
44 |
45 | it "#complete_batch_job" do
46 | subject.find_batch_job(job.id)
47 |
48 | subject.complete_batch_job
49 |
50 | expect { job.reload }.to raise_error(ActiveRecord::RecordNotFound)
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/app/workers/concerns/branch_worker_mixin.rb:
--------------------------------------------------------------------------------
1 | module BranchWorkerMixin
2 | extend ActiveSupport::Concern
3 | include SidekiqWorkerMixin
4 |
5 | attr_accessor :branch
6 |
7 | delegate :repo,
8 | :fq_repo_name,
9 | :fq_branch_name,
10 | :git_service,
11 | :pr_number,
12 | :pr_title,
13 | :pr_title_tags,
14 | :merge_target,
15 | :to => :branch
16 |
17 | def find_branch(branch_id, required_mode = nil)
18 | @branch ||= Branch.where(:id => branch_id).first
19 |
20 | if branch.nil?
21 | logger.warn("Branch #{branch_id} no longer exists. Skipping.")
22 | return false
23 | end
24 |
25 | if required_mode && branch.mode != required_mode
26 | logger.error("Branch #{fq_branch_name} is not a #{required_mode} branch. Skipping.")
27 | return false
28 | end
29 |
30 | unless enabled_for?(repo)
31 | logger.error("Branch #{fq_branch_name} is not enabled. Skipping.")
32 | return false
33 | end
34 |
35 | true
36 | end
37 |
38 | def commits
39 | branch.commits_list
40 | end
41 |
42 | def commit_range
43 | [commits.first, commits.last]
44 | end
45 |
46 | def commit_range_text
47 | case commit_range.uniq.length
48 | when 1 then branch.commit_uri_to(commit_range.first)
49 | when 2 then branch.compare_uri_for(*commit_range)
50 | end
51 | end
52 |
53 | def diff_details_for_merge
54 | repo.with_git_service do |git|
55 | git.diff_details(branch.local_merge_target, commits.last)
56 | end
57 | end
58 |
59 | def diff_file_names
60 | @diff_file_names ||= git_service.diff.new_files
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/git_service/diff.rb:
--------------------------------------------------------------------------------
1 | module GitService
2 | class Diff
3 | attr_reader :raw_diff
4 | def initialize(raw_diff)
5 | @raw_diff = raw_diff
6 | end
7 |
8 | def new_files
9 | raw_diff.deltas.collect { |delta| delta.try(:new_file).try(:[], :path) }.compact
10 | end
11 |
12 | def with_each_patch
13 | raw_diff.patches.each { |patch| yield(patch) }
14 | end
15 |
16 | def with_each_hunk
17 | with_each_patch do |patch|
18 | patch.hunks.each { |hunk| yield(hunk, patch) }
19 | end
20 | end
21 |
22 | def with_each_line
23 | with_each_hunk do |hunk, parent_patch|
24 | hunk.lines.each { |line| yield(line, hunk, parent_patch) }
25 | end
26 | end
27 |
28 | def file_status
29 | raw_diff.patches.each_with_object({}) do |patch, h|
30 | if new_file = patch.delta.new_file.try(:[], :path)
31 | additions = h.fetch_path(new_file, :additions) || 0
32 | h.store_path(new_file, :additions, (additions + patch.additions))
33 | end
34 | if old_file = patch.delta.old_file.try(:[], :path)
35 | deletions = h.fetch_path(old_file, :deletions) || 0
36 | h.store_path(new_file, :deletions, (deletions + patch.deletions))
37 | end
38 | end
39 | end
40 |
41 | def status_summary
42 | changed, added, deleted = raw_diff.stat
43 | [
44 | changed.positive? ? "#{changed} #{"file".pluralize(changed)} changed" : nil,
45 | added.positive? ? "#{added} #{"insertion".pluralize(added)}(+)" : nil,
46 | deleted.positive? ? "#{deleted} #{"deletion".pluralize(deleted)}(-)" : nil
47 | ].compact.join(", ")
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/app/models/batch_job.rb:
--------------------------------------------------------------------------------
1 | class BatchJob < ActiveRecord::Base
2 | has_many :entries, :class_name => "BatchEntry", :dependent => :destroy, :inverse_of => :job
3 |
4 | serialize :on_complete_args, Array
5 |
6 | validates :state, :inclusion => {:in => %w(finalizing), :allow_nil => true}
7 |
8 | SEMAPHORE = Mutex.new
9 |
10 | def self.perform_async(workers, worker_args, job_attributes)
11 | new_entries = workers.size.times.collect { BatchEntry.new }
12 | create!(job_attributes.merge(:entries => new_entries))
13 |
14 | workers.zip(new_entries).each do |w, e|
15 | w.perform_async(e.id, *worker_args)
16 | end
17 | end
18 |
19 | def on_complete_class
20 | super.try(:constantize)
21 | end
22 |
23 | def on_complete_class=(klass)
24 | super(klass.to_s)
25 | end
26 |
27 | def finalizing?
28 | state == "finalizing"
29 | end
30 |
31 | def expired?
32 | expires_at && Time.now > expires_at
33 | end
34 |
35 | def entries_complete?
36 | entries.all?(&:complete?) && entries.any?
37 | end
38 |
39 | def check_complete
40 | # NOTE: The mutex may need to be upgraded to a database row lock
41 | # if we go multi-process
42 | SEMAPHORE.synchronize do
43 | begin
44 | reload
45 | rescue ActiveRecord::RecordNotFound
46 | return
47 | end
48 |
49 | return if finalizing?
50 | return unless expired? || entries_complete?
51 | finalize!
52 | end
53 | end
54 |
55 | private
56 |
57 | def finalize!
58 | if on_complete_class
59 | update!(:state => "finalizing")
60 | on_complete_class.perform_async(id, *on_complete_args)
61 | else
62 | destroy
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/spec/lib/offense_message_spec.rb:
--------------------------------------------------------------------------------
1 | describe OffenseMessage do
2 | describe "#lines" do
3 | it "with full entries" do
4 | message = described_class.new
5 | message.entries = [
6 | OffenseMessage::Entry.new(:high, "Message 4", "Group B", "Locator B"),
7 | OffenseMessage::Entry.new(:high, "Message 3", "Group B", "Locator A"),
8 | OffenseMessage::Entry.new(:high, "Message 2", "Group A", "Locator B"),
9 | OffenseMessage::Entry.new(:high, "Message 1", "Group A", "Locator A"),
10 | ]
11 | expect(message.lines).to eq([
12 | "**Group A**",
13 | "- [ ] :exclamation: - Locator A - Message 1",
14 | "- [ ] :exclamation: - Locator B - Message 2",
15 | "",
16 | "**Group B**",
17 | "- [ ] :exclamation: - Locator A - Message 3",
18 | "- [ ] :exclamation: - Locator B - Message 4"
19 | ])
20 | end
21 |
22 | it "with entries - with and without groups" do
23 | message = described_class.new
24 | message.entries = [
25 | OffenseMessage::Entry.new(:high, "Message 4", "Group B", "Locator B"),
26 | OffenseMessage::Entry.new(:high, "Message 3", "Group B", "Locator A"),
27 | OffenseMessage::Entry.new(:high, "Message 2", nil, "Locator B"),
28 | OffenseMessage::Entry.new(:high, "Message 1", nil, "Locator A"),
29 | ]
30 | expect(message.lines).to eq([
31 | "- [ ] :exclamation: - Locator A - Message 1",
32 | "- [ ] :exclamation: - Locator B - Message 2",
33 | "",
34 | "**Group B**",
35 | "- [ ] :exclamation: - Locator A - Message 3",
36 | "- [ ] :exclamation: - Locator B - Message 4"
37 | ])
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/workers/pull_request_monitor.rb:
--------------------------------------------------------------------------------
1 | class PullRequestMonitor
2 | include Sidekiq::Worker
3 | sidekiq_options :queue => :miq_bot_glacial, :retry => false
4 |
5 | include SidekiqWorkerMixin
6 |
7 | def self.enabled_repos
8 | super.includes(:branches).select(&:can_have_prs?)
9 | end
10 |
11 | def self.handlers
12 | @handlers ||= begin
13 | workers_path = Rails.root.join("app/workers")
14 | Dir.glob(workers_path.join("pull_request_monitor_handlers/*.rb")).collect do |f|
15 | path = Pathname.new(f).relative_path_from(workers_path).to_s
16 | path.chomp(".rb").classify.constantize
17 | end
18 | end
19 | end
20 |
21 | def self.handlers_for(repo)
22 | handlers.select { |h| h.enabled_for?(repo) }
23 | end
24 |
25 | def perform
26 | if !first_unique_worker?
27 | logger.info "#{self.class} is already running, skipping"
28 | else
29 | process_repos
30 | end
31 | end
32 |
33 | def process_repos
34 | enabled_repos.each { |repo| process_repo(repo) }
35 | end
36 |
37 | def process_repo(repo)
38 | return unless repo.can_have_prs?
39 |
40 | results = repo.synchronize_pr_branches(github_prs(repo))
41 |
42 | branches = results[:updated] + results[:added]
43 | branches.product(self.class.handlers_for(repo)) { |b, h| h.perform_async(b.id) }
44 | end
45 |
46 | private
47 |
48 | def github_prs(repo)
49 | GithubService.pull_requests(repo.name).map do |github_pr|
50 | {
51 | :number => github_pr.number,
52 | :html_url => github_pr.head.repo.try(:html_url) || github_pr.base.repo.html_url,
53 | :merge_target => github_pr.base.ref,
54 | :pr_title => github_pr.title
55 | }
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/linter/yaml.rb:
--------------------------------------------------------------------------------
1 | module Linter
2 | class Yaml < Base
3 | private
4 |
5 | def parse_output(output)
6 | lines = output.chomp.split("\n")
7 | parsed = lines.collect { |line| line_to_hash(line) }
8 | grouped = parsed.group_by { |hash| hash["filename"] }
9 | file_count = parsed.collect { |hash| hash["filename"] }.uniq.count
10 | {
11 | "files" => grouped.collect do |filename, offenses|
12 | {
13 | "path" => filename.sub(%r{\A\./}, ""),
14 | "offenses" => offenses.collect { |offense_hash| offense_hash.except("filename") }
15 | }
16 | end,
17 | "summary" => {
18 | "offense_count" => lines.size,
19 | "target_file_count" => file_count,
20 | "inspected_file_count" => file_count
21 | }
22 | }
23 | end
24 |
25 | def linter_executable
26 | "yamllint"
27 | end
28 |
29 | def config_files
30 | [".yamllint"]
31 | end
32 |
33 | def options
34 | {:f => "parsable", nil => ["."]}
35 | end
36 |
37 | def filtered_files(files)
38 | files.select { |f| f.end_with?(".yml", ".yaml") }
39 | end
40 |
41 | def line_to_hash(line)
42 | filename, line, column, severity_message_cop = line.split(":", 4)
43 | severity_message, cop = severity_message_cop.split(/ \((.*)\)\Z/)
44 | severity, message = severity_message.match(/\[(.*)\] (.*)/).captures
45 |
46 | {
47 | "filename" => filename,
48 | "severity" => severity,
49 | "message" => message,
50 | "cop_name" => cop,
51 | "location" => {
52 | "line" => line.to_i,
53 | "column" => column.to_i
54 | }
55 | }
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/spec/lib/github_service/commands/add_reviewer_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe GithubService::Commands::AddReviewer do
2 | subject { described_class.new(issue) }
3 | let(:issue) { double(:fq_repo_name => "org/repo") }
4 | let(:command_issuer) { "nickname" }
5 |
6 | before do
7 | allow(GithubService).to receive(:valid_assignee?).with("org/repo", "good_user") { true }
8 | allow(GithubService).to receive(:valid_assignee?).with("org/repo", "bad_user") { false }
9 | end
10 |
11 | after do
12 | subject.execute!(:issuer => command_issuer, :value => command_value)
13 | end
14 |
15 | context "with a valid user" do
16 | let(:command_value) { "good_user" }
17 |
18 | it "review request that user" do
19 | expect(issue).to receive(:add_reviewer).with(["good_user"])
20 | end
21 | end
22 |
23 | context "with a valid users" do
24 | let(:command_value) { "good_user, good_user" }
25 |
26 | it "review request that user" do
27 | expect(issue).to receive(:add_reviewer).with(%w(good_user good_user))
28 | end
29 | end
30 |
31 | context "with an invalid user" do
32 | let(:command_value) { "bad_user" }
33 |
34 | it "does not review request, reports failure" do
35 | expect(issue).not_to receive(:add_reviewer)
36 | expect(issue).to receive(:add_comment).with("@#{command_issuer} Cannot add the following reviewer because they are not recognized: bad_user")
37 | end
38 | end
39 |
40 | context "with an invalid users" do
41 | let(:command_value) { "bad_user, bad_user" }
42 |
43 | it "does not review request, reports failure" do
44 | expect(issue).not_to receive(:add_reviewer)
45 | expect(issue).to receive(:add_comment).with("@#{command_issuer} Cannot add the following reviewers because they are not recognized: bad_user, bad_user")
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/lib/github_service/commands/remove_reviewer_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe GithubService::Commands::RemoveReviewer do
2 | subject { described_class.new(issue) }
3 | let(:issue) { double(:fq_repo_name => "org/repo") }
4 | let(:command_issuer) { "nickname" }
5 |
6 | before do
7 | allow(GithubService).to receive(:valid_assignee?).with("org/repo", "listed_user").and_return(true)
8 | allow(GithubService).to receive(:valid_assignee?).with("org/repo", "good_user").and_return(true)
9 | allow(GithubService).to receive(:valid_assignee?).with("org/repo", "bad_user").and_return(false)
10 |
11 | allow(subject).to receive(:requested_reviewers).and_return(["listed_user"])
12 | end
13 |
14 | after do
15 | subject.execute!(:issuer => command_issuer, :value => command_value)
16 | end
17 |
18 | context "with a valid user who is actually requested for a review" do
19 | let(:command_value) { "listed_user" }
20 |
21 | it "remove review request from that user" do
22 | expect(issue).to receive(:remove_reviewer).with("listed_user")
23 | end
24 | end
25 |
26 | context "with a valid user who is not actually requested for a review" do
27 | let(:command_value) { "good_user" }
28 |
29 | it "does not remove review request who is not actually requested" do
30 | expect(issue).not_to receive(:remove_reviewer)
31 | expect(issue).to receive(:add_comment).with("@#{command_issuer} 'good_user' is not in the list of requested reviewers, ignoring...")
32 | end
33 | end
34 |
35 | context "with an invalid user" do
36 | let(:command_value) { "bad_user" }
37 |
38 | it "does not remove review request, reports failure" do
39 | expect(issue).not_to receive(:remove_reviewer)
40 | expect(issue).to receive(:add_comment).with("@#{command_issuer} 'bad_user' is an invalid reviewer, ignoring...")
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # The Ruby version should match the lowest acceptable version of the application
4 | ruby "~> 3.1.4"
5 |
6 | plugin 'bundler-inject'
7 | require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil
8 |
9 | gem 'rails', '~> 7.0.8', '>= 7.0.8.7'
10 | gem 'concurrent-ruby', '< 1.3.5' # Temporary pin down as concurrent-ruby 1.3.5 breaks Rails 7.0, and rails-core doesn't
11 | # plan to ship a new 7.0 to fix it. See https://github.com/rails/rails/pull/54264
12 |
13 | # Use PostgreSQL as the database for Active Record
14 | gem 'pg'
15 |
16 | # Use SCSS for stylesheets
17 | gem 'sass-rails', '~> 5.1.0'
18 |
19 | # Use jquery as the JavaScript library
20 | gem 'jquery-rails'
21 |
22 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
23 | gem 'turbolinks'
24 |
25 | gem 'foreman'
26 | gem 'puma'
27 |
28 | gem 'config'
29 | gem 'listen'
30 |
31 | # Sidekiq specific gems
32 | gem 'sidecloq'
33 | gem 'sidekiq'
34 |
35 | # Services gems
36 | gem 'minigit', '~> 0.0.4'
37 | gem 'net-ssh', '~> 7.3.0'
38 |
39 | gem 'awesome_spawn', '~> 1.6'
40 | gem 'default_value_for', '~> 4.0'
41 | gem 'haml_lint', '~> 0.51', :require => false
42 | gem 'irb'
43 | gem 'manageiq-style', '~> 1.5', ">=1.5.6", :require => false
44 | gem 'more_core_extensions', '~> 4.4', :require => 'more_core_extensions/all'
45 | gem 'rugged', :require => false
46 |
47 | gem 'faraday'
48 | gem 'faraday-http-cache', '~> 2.5.0'
49 | gem 'octokit', '~> 4.25.0', :require => false
50 |
51 | group :development, :test do
52 | gem 'rspec'
53 | gem 'rspec-rails'
54 | gem 'simplecov', '>= 0.21.2'
55 | gem 'timecop'
56 | end
57 |
58 | group :test do
59 | gem 'factory_bot_rails'
60 | gem 'webmock'
61 | end
62 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/github_pr_commenter/diff_content_checker.rb:
--------------------------------------------------------------------------------
1 | require 'rugged'
2 |
3 | module CommitMonitorHandlers::CommitRange
4 | class GithubPrCommenter::DiffContentChecker
5 | include Sidekiq::Worker
6 | sidekiq_options :queue => :miq_bot
7 |
8 | include BatchEntryWorkerMixin
9 | include BranchWorkerMixin
10 |
11 | def perform(batch_entry_id, branch_id, _new_commits)
12 | return unless find_batch_entry(batch_entry_id)
13 | return skip_batch_entry unless find_branch(branch_id, :pr)
14 |
15 | complete_batch_entry(:result => process_lines)
16 | end
17 |
18 | private
19 |
20 | def process_lines
21 | @offenses = []
22 |
23 | check_diff_lines
24 |
25 | @offenses
26 | end
27 |
28 | def check_diff_lines
29 | branch.git_service.diff.with_each_line do |line, _parent_hunk, parent_patch|
30 | next unless line.addition?
31 | check_line(line, parent_patch)
32 | end
33 | rescue GitService::UnmergeableError
34 | nil # Avoid working on unmergeable PRs
35 | end
36 |
37 | def check_line(line, patch)
38 | file_path = patch.delta.new_file[:path]
39 | Settings.diff_content_checker.offenses.each do |offender, options|
40 | next if options.except.try(:any?) { |except| file_path.start_with?(except) }
41 |
42 | regexp = options.type == :regexp ? Regexp.new(offender.to_s) : /\b#{Regexp.escape(offender.to_s)}\b/i
43 | add_offense(offender, options, file_path, line) if regexp.match(line.content)
44 | end
45 | end
46 |
47 | def add_offense(offender, options, file_path, line)
48 | line_number = line.new_lineno
49 | message = options.message || "Detected `#{offender}`"
50 |
51 | @offenses << OffenseMessage::Entry.new(options.severity, message, file_path, line_number)
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/github_service/commands/move_issue.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class MoveIssue < Base
4 | restrict_to :organization
5 |
6 | private
7 |
8 | def _execute(issuer:, value:)
9 | @dest_organization_name, @dest_repo_name = value.split("/", 2).unshift(issue.organization_name).last(2)
10 | @dest_fq_repo_name = "#{@dest_organization_name}/#{@dest_repo_name}"
11 |
12 | validate
13 |
14 | if errors.present?
15 | issue.add_comment("@#{issuer} :x: `move_issue` failed. #{errors.to_sentence.capitalize}.")
16 | else
17 | new_issue = GithubService.create_issue(@dest_fq_repo_name, issue.title, new_issue_body)
18 | issue.add_comment("This issue has been moved to #{new_issue.html_url}")
19 | GithubService.close_issue(issue.fq_repo_name, issue.number)
20 | end
21 | end
22 |
23 | def errors
24 | @errors ||= []
25 | end
26 |
27 | def validate
28 | if issue.repo_name.downcase == @dest_repo_name.downcase
29 | errors << "issue already exists on the '#{issue.repo_name}' repository"
30 | end
31 | if issue.pull_request?
32 | errors << "a pull request cannot be moved"
33 | end
34 | if issue.organization_name.downcase != @dest_organization_name.downcase
35 | errors << "cannot move issue to repository outside of the #{issue.organization_name} organization"
36 | end
37 | unless GithubService.repository?(@dest_fq_repo_name)
38 | errors << "repository does not exist or is unreachable"
39 | end
40 | end
41 |
42 | def new_issue_body
43 | <<-EOS
44 | #{issue.body}
45 |
46 | ---
47 |
48 | *This issue was moved to this repository from #{issue.html_url}, originally opened by @#{issue.author}*
49 | EOS
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/workers/concerns/batch_entry_worker_mixin_spec.rb:
--------------------------------------------------------------------------------
1 | describe BatchEntryWorkerMixin do
2 | subject do
3 | Class.new do
4 | include BatchEntryWorkerMixin
5 |
6 | def logger
7 | @logger ||= RSpec::Mocks::Double.new("logger")
8 | end
9 | end.new
10 | end
11 | let!(:job) { BatchJob.create!(:entries => [entry]) }
12 | let(:entry) { BatchEntry.new }
13 |
14 | describe "#find_batch_entry" do
15 | it "with an existing entry" do
16 | expect(subject.find_batch_entry(entry.id)).to be true
17 | end
18 |
19 | it "with a missing entry" do
20 | expect(subject.logger).to receive(:warn) do |message|
21 | expect(message).to match(/no longer exists/)
22 | end
23 | expect(subject.find_batch_entry(-1)).to be false
24 | end
25 | end
26 |
27 | it "#batch_job" do
28 | subject.find_batch_entry(entry.id)
29 |
30 | expect(subject.batch_job).to eq(job)
31 | end
32 |
33 | describe "#complete_batch_entry" do
34 | before do
35 | subject.find_batch_entry(entry.id)
36 | expect(subject.batch_entry).to receive(:check_job_complete)
37 | end
38 |
39 | it "with no changes" do
40 | subject.complete_batch_entry
41 |
42 | expect(subject.batch_entry.state).to eq("succeeded")
43 | expect(subject.batch_entry.result).to be_nil
44 | end
45 |
46 | it "with changes" do
47 | subject.complete_batch_entry(:result => ["something"])
48 |
49 | expect(subject.batch_entry.state).to eq("succeeded")
50 | expect(subject.batch_entry.result).to eq(["something"])
51 | end
52 |
53 | it "with changes to " do
54 | subject.complete_batch_entry(:state => "failed", :result => ["failure 1", "failure 2"])
55 |
56 | expect(subject.batch_entry.state).to eq("failed")
57 | expect(subject.batch_entry.result).to eq(["failure 1", "failure 2"])
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/github_pr_commenter.rb:
--------------------------------------------------------------------------------
1 | module CommitMonitorHandlers::CommitRange
2 | class GithubPrCommenter
3 | include Sidekiq::Worker
4 | sidekiq_options :queue => :miq_bot_glacial
5 |
6 | include BatchJobWorkerMixin
7 | include BranchWorkerMixin
8 |
9 | def self.batch_workers
10 | [DiffContentChecker, DiffFilenameChecker, CommitMetadataChecker]
11 | end
12 |
13 | def self.handled_branch_modes
14 | [:pr]
15 | end
16 |
17 | def perform(batch_job_id, branch_id, _new_commits)
18 | return unless find_batch_job(batch_job_id)
19 | return skip_batch_job unless find_branch(branch_id, :pr)
20 |
21 | replace_batch_comments
22 | complete_batch_job
23 | end
24 |
25 | private
26 |
27 | def tag
28 | ""
29 | end
30 |
31 | def header
32 | "#{tag}Some comments on #{"commit".pluralize(commits.length)} #{commit_range_text}\n"
33 | end
34 |
35 | def continuation_header
36 | "#{tag}**...continued**\n"
37 | end
38 |
39 | def replace_batch_comments
40 | logger.info("Adding batch comment to PR #{pr_number}.")
41 |
42 | GithubService.replace_comments(fq_repo_name, pr_number, new_comments) do |old_comment|
43 | batch_comment?(old_comment)
44 | end
45 | end
46 |
47 | def batch_comment?(comment)
48 | comment.body.start_with?(tag)
49 | end
50 |
51 | def new_comments
52 | return [] unless merged_results.any?
53 |
54 | content = OffenseMessage.new
55 | content.entries = merged_results
56 |
57 | message_builder = GithubService::MessageBuilder.new(header, continuation_header)
58 | message_builder.write("")
59 | message_builder.write_lines(content.lines)
60 | message_builder.comments
61 | end
62 |
63 | def merged_results
64 | @merged_results ||= batch_job.entries.collect(&:result).flatten.compact
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/models/batch_entry_spec.rb:
--------------------------------------------------------------------------------
1 | describe BatchEntry do
2 | include_examples "state predicates", :succeeded?,
3 | nil => false,
4 | "started" => false,
5 | "failed" => false,
6 | "skipped" => false,
7 | "succeeded" => true
8 |
9 | include_examples "state predicates", :failed?,
10 | nil => false,
11 | "started" => false,
12 | "failed" => true,
13 | "skipped" => false,
14 | "succeeded" => false
15 |
16 | include_examples "state predicates", :skipped?,
17 | nil => false,
18 | "started" => false,
19 | "failed" => false,
20 | "skipped" => true,
21 | "succeeded" => false
22 |
23 | include_examples "state predicates", :complete?,
24 | nil => false,
25 | "started" => false,
26 | "failed" => true,
27 | "skipped" => true,
28 | "succeeded" => true
29 |
30 | describe "#check_job_complete" do
31 | let(:job) { BatchJob.create! }
32 | let(:entry) { described_class.create!(:job => job) }
33 |
34 | context "when complete" do
35 | before do
36 | entry.update(:state => "succeeded")
37 | end
38 |
39 | it "with job still available" do
40 | expect(job).to receive(:check_complete)
41 |
42 | entry.check_job_complete
43 | end
44 |
45 | it "when job destroyed externally" do
46 | entry.reload # To remove job caching
47 | job.destroy
48 |
49 | expect { entry.check_job_complete }.to_not raise_error
50 | end
51 | end
52 |
53 | it "when not complete" do
54 | expect(job).to_not receive(:check_complete)
55 |
56 | entry.check_job_complete
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/github_service/command_dispatcher.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | class CommandDispatcher
3 | class << self
4 | def registry
5 | @registry ||= Hash.new do |h, k|
6 | normalized = k.to_s.tr("-", "_") # Support - or _ in command
7 | normalized.chop! if normalized.end_with?("s") # Support singular or plural
8 | h[normalized] if h.key?(normalized)
9 | end
10 | end
11 |
12 | def register_command(command_name, command_class)
13 | registry[command_name.to_s] = command_class
14 | end
15 | end
16 |
17 | attr_reader :issue
18 |
19 | def initialize(issue)
20 | @issue = issue.kind_of?(GithubService::Issue) ? issue : GithubService::Issue.new(issue)
21 | @fq_repo_name = @issue.fq_repo_name
22 | end
23 |
24 | def dispatch!(issuer:, text:)
25 | lines = text.split("\n")
26 | lines.each do |line|
27 | match = command_regex.match(line.strip)
28 | next unless match
29 | next if issuer == bot_name
30 |
31 | command = match[:command]
32 | command_value = match[:command_value]
33 | command_class = self.class.registry[command]
34 |
35 | if command_class.present?
36 | Rails.logger.info("Dispatching '#{command}' to #{command_class} on issue ##{issue.number} | issuer: #{issuer}, value: #{command_value}")
37 | command_class.new(issue).execute!(:issuer => issuer, :value => command_value)
38 | else
39 | message = <<-EOMSG
40 | @#{issuer} unrecognized command '#{command}', ignoring...
41 |
42 | Accepted commands are: #{self.class.registry.keys.join(", ")}
43 | EOMSG
44 | issue.add_comment(message)
45 | end
46 | end
47 | end
48 |
49 | private
50 |
51 | def command_regex
52 | /\A@#{bot_name}\s+(?[a-z_-]+)(?:\s*)(?.*)\z/i
53 | end
54 |
55 | def bot_name
56 | GithubService.bot_name
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/rubocop_results_filter_spec.rb:
--------------------------------------------------------------------------------
1 | describe CommitMonitorHandlers::CommitRange::RubocopChecker::RubocopResultsFilter do
2 | describe "#filtered" do
3 | subject { described_class.new(rubocop_results, @diff_details) }
4 |
5 | before { |example| @example = example }
6 |
7 | it "with lines not in the diff" do
8 | @diff_details = {
9 | rubocop_check_path_file("example.rb").to_s => [4]
10 | }
11 |
12 | filtered = subject.filtered
13 |
14 | expect(filtered["files"].length).to eq(1)
15 | expect(filtered["files"][0]["offenses"].length).to eq(1)
16 | expect(filtered["files"][0]["offenses"][0]["location"]["line"]).to eq(4)
17 |
18 | expect(filtered["summary"]["offense_count"]).to eq(1)
19 | end
20 |
21 | it "with void warnings in spec files" do
22 | @diff_details = {
23 | rubocop_check_path_file("non_spec_file_with_void_warning.rb").to_s => [2],
24 | rubocop_check_path_file("spec/non_spec_file_in_spec_dir_with_void_warning.rb").to_s => [2],
25 | rubocop_check_path_file("spec/spec_file_with_void_warning_spec.rb").to_s => [3]
26 | }
27 |
28 | filtered = subject.filtered
29 |
30 | expect(filtered["files"].length).to eq(3)
31 | expect(filtered["summary"]["offense_count"]).to eq(2)
32 |
33 | spec_file = filtered["files"].detect do |f|
34 | f["path"].include?("spec_file_with_void_warning_spec.rb")
35 | end
36 | expect(spec_file["offenses"]).to be_empty
37 | end
38 |
39 | it "with haml file using haml-lint" do
40 | @diff_details = {
41 | rubocop_check_path_file("example.haml").to_s => [4]
42 | }
43 |
44 | filtered = subject.filtered
45 |
46 | expect(filtered["files"].length).to eq(1)
47 | expect(filtered["files"][0]["offenses"].length).to eq(1)
48 | expect(filtered["files"][0]["offenses"][0]["location"]["line"]).to eq(3)
49 |
50 | expect(filtered["summary"]["offense_count"]).to eq(1)
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/github_service/commands/remove_label.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class RemoveLabel < Base
4 | include IsTeamMember
5 |
6 | alias_as 'rm_label'
7 |
8 | private
9 |
10 | def _execute(issuer:, value:)
11 | unremovable = []
12 | valid, invalid = extract_label_names(value)
13 | process_extracted_labels(issuer, valid, invalid, unremovable)
14 |
15 | if invalid.any?
16 | message = "@#{issuer} Cannot remove the following label#{"s" if invalid.length > 1} because they are not recognized: "
17 | message << invalid.join(", ")
18 | issue.add_comment(message)
19 | end
20 |
21 | if unremovable.any?
22 | labels = "label#{"s" if unremovable.length > 1}"
23 | triage_perms = "[triage team permissions](https://github.com/orgs/ManageIQ/teams/core-triage)"
24 | message = "@#{issuer} Cannot remove the following #{labels} since they require #{triage_perms}: "
25 | message << unremovable.join(", ")
26 | issue.add_comment(message)
27 | end
28 |
29 | valid.each do |l|
30 | issue.remove_label(l) if issue.applied_label?(l)
31 | end
32 | end
33 |
34 | def extract_label_names(value)
35 | label_names = value.split(",").map { |label| label.strip.downcase }
36 | validate_labels(label_names)
37 | end
38 |
39 | def process_extracted_labels(issuer, valid_labels, _invalid_labels, unremovable)
40 | unless triage_member?(issuer)
41 | valid_labels.each { |label| unremovable << label if Settings.labels.unremovable.include?(label) }
42 | unremovable.each { |label| valid_labels.delete(label) }
43 | end
44 | end
45 |
46 | def validate_labels(label_names)
47 | # First reload the cache if there are any invalid labels
48 | GithubService.refresh_labels(issue.fq_repo_name) unless label_names.all? { |l| GithubService.valid_label?(issue.fq_repo_name, l) }
49 |
50 | # Then see if any are *still* invalid and split the list
51 | label_names.partition { |l| GithubService.valid_label?(issue.fq_repo_name, l) }
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/app/workers/pr_mergeability_checker.rb:
--------------------------------------------------------------------------------
1 | class PrMergeabilityChecker
2 | include Sidekiq::Worker
3 | sidekiq_options :queue => :miq_bot
4 |
5 | include BranchWorkerMixin
6 |
7 | LABEL = "unmergeable".freeze
8 |
9 | def perform(branch_id)
10 | return unless find_branch(branch_id, :pr)
11 | logger.info("Determining mergeability of PR #{branch.fq_pr_number}.")
12 |
13 | process_mergeability
14 | end
15 |
16 | private
17 |
18 | def tag
19 | ""
20 | end
21 |
22 | def unmergeable_comment
23 | "#{tag}This pull request is not mergeable. Please rebase and repush."
24 | end
25 |
26 | def process_mergeability
27 | was_mergeable = branch.mergeable?
28 | currently_mergeable = branch.git_service.mergeable?
29 |
30 | if was_mergeable && !currently_mergeable
31 | write_to_github
32 | apply_label
33 | elsif !was_mergeable && currently_mergeable
34 | remove_comments
35 | remove_label
36 | end
37 |
38 | # Update columns directly to avoid collisions wrt the serialized column issue
39 | branch.update_columns(:mergeable => currently_mergeable)
40 | end
41 |
42 | def remove_comments
43 | comment_ids = GithubService.issue_comments(fq_repo_name, branch.pr_number).select do |com|
44 | com.user.login == Settings.github_credentials.username && com.body.start_with?(tag)
45 | end.map(&:id)
46 |
47 | GithubService.delete_comments(fq_repo_name, comment_ids)
48 | end
49 |
50 | def write_to_github
51 | logger.info("Updating PR #{branch.fq_pr_number} with mergability comment.")
52 |
53 | GithubService.add_comment(
54 | fq_repo_name,
55 | branch.pr_number,
56 | unmergeable_comment
57 | )
58 | end
59 |
60 | def apply_label
61 | logger.info("Updating PR #{branch.fq_pr_number} with label #{LABEL.inspect}.")
62 |
63 | GithubService.add_labels_to_an_issue(fq_repo_name, branch.pr_number, [LABEL])
64 | end
65 |
66 | def remove_label
67 | logger.info("Updating PR #{branch.fq_pr_number} removing label #{LABEL.inspect}.")
68 | GithubService.remove_label(fq_repo_name, branch.pr_number, LABEL)
69 | rescue Octokit::NotFound
70 | # This label is not currently applied, skip
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_lines_not_in_the_diff/results.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": {
3 | "rubocop_version": "1.13.0",
4 | "ruby_engine": "ruby",
5 | "ruby_version": "2.7.2",
6 | "ruby_patchlevel": "137",
7 | "ruby_platform": "x86_64-darwin19"
8 | },
9 | "files": [
10 | {
11 | "path": "spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_lines_not_in_the_diff/example.rb",
12 | "offenses": [
13 | {
14 | "severity": "convention",
15 | "message": "Freeze mutable objects assigned to constants.",
16 | "cop_name": "Style/MutableConstant",
17 | "corrected": false,
18 | "correctable": true,
19 | "location": {
20 | "start_line": 2,
21 | "start_column": 10,
22 | "last_line": 6,
23 | "last_column": 3,
24 | "length": 78,
25 | "line": 2,
26 | "column": 10
27 | }
28 | },
29 | {
30 | "severity": "convention",
31 | "message": "Align the keys and values of a hash literal if they span more than one line.",
32 | "cop_name": "Layout/HashAlignment",
33 | "corrected": false,
34 | "correctable": true,
35 | "location": {
36 | "start_line": 3,
37 | "start_column": 5,
38 | "last_line": 3,
39 | "last_column": 20,
40 | "length": 16,
41 | "line": 3,
42 | "column": 5
43 | }
44 | },
45 | {
46 | "severity": "convention",
47 | "message": "Align the keys and values of a hash literal if they span more than one line.",
48 | "cop_name": "Layout/HashAlignment",
49 | "corrected": false,
50 | "correctable": true,
51 | "location": {
52 | "start_line": 4,
53 | "start_column": 5,
54 | "last_line": 4,
55 | "last_column": 22,
56 | "length": 18,
57 | "line": 4,
58 | "column": 5
59 | }
60 | }
61 | ]
62 | }
63 | ],
64 | "summary": {
65 | "offense_count": 3,
66 | "target_file_count": 1,
67 | "inspected_file_count": 1
68 | }
69 | }
--------------------------------------------------------------------------------
/spec/lib/github_service/commands/unassign_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | RSpec.describe GithubService::Commands::Unassign do
4 | subject { described_class.new(issue) }
5 | let(:issue) { double("Issue", :fq_repo_name => "org/repo") }
6 | let(:command_issuer) { "nickname" }
7 | let(:assigned_users) { ["listed_user"] }
8 |
9 | before do
10 | allow(subject).to receive(:list_assigned_users).and_return(assigned_users)
11 | end
12 |
13 | after do
14 | subject.execute!(:issuer => command_issuer, :value => command_value)
15 | end
16 |
17 | context "with a user who is in the list of assignees" do
18 | let(:command_value) { "listed_user" }
19 |
20 | it "unassigns pull request or issue from that user" do
21 | allow(issue).to receive(:number).and_return(42)
22 | expect(subject).to receive(:octokit_remove_assignees).with("org/repo", 42, %w(listed_user)).once
23 | end
24 | end
25 |
26 | context "with a user who is not in the list of assignees" do
27 | let(:command_value) { "non_listed_user" }
28 |
29 | it "do not unassign pull request or issue from that user" do
30 | expect(issue).not_to receive(:octokit_remove_assignees)
31 | expect(issue).to receive(:add_comment).with("@#{command_issuer} User 'non_listed_user' is not in the list of assignees, ignoring...")
32 | end
33 | end
34 |
35 | context "with users who are in the list of assignees" do
36 | let(:assigned_users) { %w(listed_user1 listed_user2) }
37 | let(:command_value) { "listed_user1, listed_user2" }
38 |
39 | it "unassigns pull request or issue from these users" do
40 | allow(issue).to receive(:number).and_return(42)
41 | expect(subject).to receive(:octokit_remove_assignees).with("org/repo", 42, %w(listed_user1 listed_user2)).once
42 | end
43 | end
44 |
45 | context "with users who are not in the list of assignees" do
46 | let(:assigned_users) { %w(listed_user1 listed_user2) }
47 | let(:command_value) { "non_listed_user1, non_listed_user2" }
48 |
49 | it "do not unassign pull request or issue from these users" do
50 | expect(issue).not_to receive(:octokit_remove_assignees)
51 | expect(issue).to receive(:add_comment).with("@#{command_issuer} Users 'non_listed_user1, non_listed_user2' are not in the list of assignees, ignoring...")
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/spec/workers/github_notification_monitor_worker_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe GithubNotificationMonitorWorker do
2 | before { stub_sidekiq_logger(described_class) }
3 | subject { described_class.new }
4 |
5 | def stub_github_notification_monitors(*org_repo_pairs)
6 | repo_names = org_repo_pairs.collect { |pair| pair.join("/") }
7 | stub_settings(:github_notification_monitor => {:repo_names => repo_names})
8 |
9 | org_repo_pairs.collect.with_index do |(org, repo), i|
10 | fq_repo_name = "#{org}/#{repo}"
11 | create(:repo, :name => fq_repo_name)
12 |
13 | double("github notification monitor #{i}").tap do |notification_monitor|
14 | allow(GithubNotificationMonitor).to receive(:new).with(fq_repo_name).and_return(notification_monitor)
15 | end
16 | end
17 | end
18 |
19 | describe "#perform" do
20 | it "skips if the list of repo names is not provided" do
21 | stub_settings(:github_notification_monitor => {:repo_names => nil})
22 |
23 | expect(GithubNotificationMonitor).to_not receive(:new)
24 | subject.perform
25 | end
26 |
27 | it "skips if the list of repo names is empty" do
28 | stub_settings(:github_notification_monitor => {:repo_names => []})
29 |
30 | expect(GithubNotificationMonitor).to_not receive(:new)
31 | subject.perform
32 | end
33 |
34 | it "gets notifications from a notification monitor" do
35 | nm = stub_github_notification_monitors(["SomeOrg", "some_repo"]).first
36 |
37 | expect(nm).to receive(:process_notifications).once
38 | subject.perform
39 | end
40 |
41 | it "gets notifications from multiple notification monitors" do
42 | nm1, nm2 = stub_github_notification_monitors(["SomeOrg", "some_repo1"], ["SomeOrg", "some_repo2"])
43 |
44 | expect(nm1).to receive(:process_notifications).once
45 | expect(nm2).to receive(:process_notifications).once
46 | subject.perform
47 | end
48 |
49 | it "handles errors raised by a notification monitor" do
50 | nm1, nm2 = stub_github_notification_monitors(["SomeOrg", "some_repo1"], ["SomeOrg", "some_repo2"])
51 |
52 | expect(nm1).to receive(:process_notifications).once.and_raise("boom")
53 | expect(nm2).to receive(:process_notifications).once
54 |
55 | expect { subject.perform }.not_to raise_error
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[7.0].define(version: 2025_03_05_205548) do
14 | # These are extensions that must be enabled in order to support this database
15 | enable_extension "plpgsql"
16 |
17 | create_table "batch_entries", id: :serial, force: :cascade do |t|
18 | t.integer "batch_job_id"
19 | t.string "state", limit: 255
20 | t.text "result"
21 | t.index ["batch_job_id"], name: "index_batch_entries_on_batch_job_id"
22 | end
23 |
24 | create_table "batch_jobs", id: :serial, force: :cascade do |t|
25 | t.datetime "created_at", precision: nil
26 | t.datetime "updated_at", precision: nil
27 | t.datetime "expires_at", precision: nil
28 | t.string "on_complete_class", limit: 255
29 | t.text "on_complete_args"
30 | t.string "state", limit: 255
31 | end
32 |
33 | create_table "branches", id: :serial, force: :cascade do |t|
34 | t.string "name", limit: 255
35 | t.string "commit_uri", limit: 255
36 | t.string "last_commit", limit: 255
37 | t.integer "repo_id"
38 | t.boolean "pull_request"
39 | t.datetime "last_checked_on", precision: nil
40 | t.datetime "last_changed_on", precision: nil
41 | t.text "commits_list"
42 | t.boolean "mergeable"
43 | t.string "merge_target"
44 | t.string "pr_title"
45 | t.integer "linter_offense_count"
46 | t.index ["name", "repo_id"], name: "index_branches_on_name_and_repo_id", unique: true
47 | end
48 |
49 | create_table "repos", id: :serial, force: :cascade do |t|
50 | t.string "name", limit: 255
51 | t.datetime "created_at", precision: nil
52 | t.datetime "updated_at", precision: nil
53 | end
54 |
55 | end
56 |
--------------------------------------------------------------------------------
/spec/lib/github_service_spec.rb:
--------------------------------------------------------------------------------
1 | describe GithubService do
2 | describe "#username_lookup" do
3 | let(:lookup_username) { "NickLaMuro" }
4 | let(:lookup_status) { 200 }
5 |
6 | before do
7 | # HTTP lookup
8 | stub_request(:head, "https://github.com/#{lookup_username}")
9 | .with(:headers => {'Accept' => '*/*', 'User-Agent' => 'Ruby'})
10 | .to_return(:status => lookup_status, :body => "", :headers => {})
11 | end
12 |
13 | after do
14 | lookup_cache.delete(lookup_username)
15 | end
16 |
17 | def lookup_cache
18 | described_class.send(:username_lookup_cache)
19 | end
20 |
21 | context "for a valid user" do
22 | before do
23 | github_service_add_stub :url => "/users/#{lookup_username}",
24 | :response_body => {'id' => 123}.to_json
25 | end
26 |
27 | it "looks up a user and stores the user's ID in the cache" do
28 | expect(described_class.username_lookup(lookup_username)).to eq(123)
29 | expect(lookup_cache).to eq("NickLaMuro" => 123)
30 | end
31 | end
32 |
33 | context "for a user that is not found" do
34 | let(:lookup_status) { 404 }
35 |
36 | it "looks up a user and stores the user's ID in the cache" do
37 | expect(described_class.username_lookup(lookup_username)).to eq(nil)
38 | expect(lookup_cache).to eq("NickLaMuro" => nil)
39 | end
40 |
41 | it "does a lookup call only once" do
42 | http_instance = Net::HTTP.new("github.com", 443)
43 | fake_not_found = Net::HTTPNotFound.new(nil, nil, nil)
44 | expect(Net::HTTP).to receive(:new).and_return(http_instance)
45 | expect(http_instance).to receive(:request_head).once.and_return(fake_not_found)
46 |
47 | expect(described_class.username_lookup(lookup_username)).to eq(nil)
48 | expect(described_class.username_lookup(lookup_username)).to eq(nil)
49 | end
50 | end
51 |
52 | context "when GitHub is having a bad time..." do
53 | let(:lookup_status) { 500 }
54 |
55 | it "looks up a user and does not stores the username in the cache" do
56 | expect do
57 | described_class.username_lookup(lookup_username)
58 | end.to raise_error(RuntimeError, "Error on GitHub with username lookup")
59 | expect(lookup_cache).to eq({})
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_spec.rb:
--------------------------------------------------------------------------------
1 | describe CommitMonitor do
2 | context "#compare_commits_list (private)" do
3 | let(:left) { ["b35cfe137e193239d887a87182af971c6d1c7f07", "71eddc02941a4a8e08985202f49f8c88251b1bc1", "67b120c9ebf4467819fbfd329e06ad288621c53c"] }
4 | let(:right) { ["467ece9fe399a8e77ce6287a851acc62b6f9b5f6", "2bff87929646d40aca36649fc640310162b774f0", "e04b64754a32a6bedffc56d92e4a9f85b052296f"] }
5 |
6 | it "with matching lists" do
7 | expect(described_class.new.send(:compare_commits_list, left, left)).to eq(
8 | {:same => left, :left_only => [], :right_only => []}
9 | )
10 | end
11 |
12 | it "with non-matching lists" do
13 | expect(described_class.new.send(:compare_commits_list, left, right)).to eq(
14 | {:same => [], :left_only => left, :right_only => right}
15 | )
16 | end
17 |
18 | it "with partial matching lists" do
19 | l = left.dup << right[0]
20 | r = left.dup << right[1]
21 |
22 | expect(described_class.new.send(:compare_commits_list, l, r)).to eq(
23 | {:same => left, :left_only => [right[0]], :right_only => [right[1]]}
24 | )
25 | end
26 |
27 | it "with partial matching lists and left list longer" do
28 | l = left.dup << right[0]
29 | r = left.dup
30 |
31 | expect(described_class.new.send(:compare_commits_list, l, r)).to eq(
32 | {:same => left, :left_only => [right[0]], :right_only => []}
33 | )
34 | end
35 |
36 | it "with partial matching lists and right list longer" do
37 | l = left.dup
38 | r = left.dup << right[0]
39 |
40 | expect(described_class.new.send(:compare_commits_list, l, r)).to eq(
41 | {:same => left, :left_only => [], :right_only => [right[0]]}
42 | )
43 | end
44 |
45 | it "with empty lists" do
46 | expect(described_class.new.send(:compare_commits_list, [], [])).to eq(
47 | {:same => [], :left_only => [], :right_only => []}
48 | )
49 | end
50 |
51 | it "with empty left list" do
52 | expect(described_class.new.send(:compare_commits_list, [], right)).to eq(
53 | {:same => [], :left_only => [], :right_only => right}
54 | )
55 | end
56 |
57 | it "with empty right list" do
58 | expect(described_class.new.send(:compare_commits_list, left, [])).to eq(
59 | {:same => [], :left_only => left, :right_only => []}
60 | )
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/workers/pr_mergeability_checker_spec.rb:
--------------------------------------------------------------------------------
1 | describe PrMergeabilityChecker do
2 | before(:each) do
3 | stub_sidekiq_logger
4 | end
5 |
6 | let(:pr_branch) { create(:pr_branch, :name => 'prs/1/head', :mergeable => true) }
7 | let(:repo_name) { pr_branch.repo.name }
8 |
9 | context 'when PR was mergeable and becomes unmergeable' do
10 | it 'comments on the PR' do
11 | git_service = instance_double('GitService::Branch', :mergeable? => false)
12 | allow(GitService::Branch).to receive(:new) { git_service }
13 | allow(GithubService).to receive(:add_labels_to_an_issue)
14 |
15 | expect(GithubService).to receive(:add_comment)
16 | .with(repo_name, 1, a_string_including("not mergeable"))
17 |
18 | described_class.new.perform(pr_branch.id)
19 | end
20 |
21 | it "adds an 'unmergeable' label to the PR" do
22 | git_service = instance_double('GitService::Branch', :mergeable? => false)
23 | allow(GitService::Branch).to receive(:new) { git_service }
24 | allow(GithubService).to receive(:add_comment)
25 |
26 | expect(GithubService).to receive(:add_labels_to_an_issue)
27 | .with(repo_name, 1, ['unmergeable'])
28 |
29 | described_class.new.perform(pr_branch.id)
30 | end
31 | end
32 |
33 | context 'when PR was unmergeable and becomes mergeable' do
34 | let(:username) { "tux" }
35 | let(:user) { double("User", :id => 42, :login => username) }
36 | let(:body) { "This pull request is not mergeable. Please rebase and repush." }
37 | let(:comment) { double("Comment", :id => 9, :body => body, :user => user) }
38 | let(:pr_branch) { create(:pr_branch, :name => 'prs/1/head', :mergeable => false) }
39 |
40 | before do
41 | stub_settings(Hash(:github_credentials => {:username => username}))
42 | end
43 |
44 | it "removes an 'unmergeable' label and comments from the PR" do
45 | git_service = instance_double('GitService::Branch', :mergeable? => true)
46 |
47 | allow(GitService::Branch).to receive(:new) { git_service }
48 | allow(GithubService).to receive(:add_comment)
49 |
50 | expect(GithubService).to receive(:remove_label).with(repo_name, 1, 'unmergeable')
51 | expect(GithubService).to receive(:issue_comments).with(repo_name, 1).and_return([comment, comment])
52 | expect(GithubService).to receive(:delete_comments).with(repo_name, [9, 9]).once
53 |
54 | described_class.new.perform(pr_branch.id)
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/spec/lib/offense_message/entry_spec.rb:
--------------------------------------------------------------------------------
1 | describe OffenseMessage::Entry do
2 | context "#to_s" do
3 | it "basic entry" do
4 | entry = described_class.new(:high, "Message")
5 | expect(entry.to_s).to eq("- [ ] :exclamation: - Message")
6 | end
7 |
8 | it "basic entry with locator" do
9 | entry = described_class.new(:high, "Message", nil, "Locator")
10 | expect(entry.to_s).to eq("- [ ] :exclamation: - Locator - Message")
11 | end
12 | end
13 |
14 | context "#<=>" do
15 | shared_examples_for "sortable" do
16 | it "should be sortable" do
17 | expect(entry1 <=> entry2).to eq(-1)
18 | expect(entry1 <=> entry1).to eq(0)
19 | expect(entry2 <=> entry1).to eq(1)
20 | end
21 | end
22 |
23 | context "by group" do
24 | context "with groups" do
25 | let(:entry1) { described_class.new(:high, "Message", "Group A") }
26 | let(:entry2) { described_class.new(:high, "Message", "Group B") }
27 | include_examples "sortable"
28 | end
29 |
30 | context "with and without group" do
31 | let(:entry1) { described_class.new(:high, "Message") }
32 | let(:entry2) { described_class.new(:high, "Message", "Group B") }
33 | include_examples "sortable"
34 | end
35 | end
36 |
37 | context "by severity" do
38 | let(:entry1) { described_class.new(:high, "Message") }
39 | let(:entry2) { described_class.new(:low, "Message") }
40 | include_examples "sortable"
41 | end
42 |
43 | context "by locator" do
44 | context "with locators" do
45 | let(:entry1) { described_class.new(:high, "Message", "Group A", "Locator A") }
46 | let(:entry2) { described_class.new(:high, "Message", "Group A", "Locator B") }
47 | include_examples "sortable"
48 | end
49 |
50 | context "with and without locator" do
51 | let(:entry1) { described_class.new(:high, "Message", "Group A") }
52 | let(:entry2) { described_class.new(:high, "Message", "Group A", "Locator B") }
53 | include_examples "sortable"
54 | end
55 | end
56 |
57 | context "by message" do
58 | let(:entry1) { described_class.new(:high, "Message A") }
59 | let(:entry2) { described_class.new(:high, "Message B") }
60 | include_examples "sortable"
61 | end
62 |
63 | it "as part of an Array" do
64 | entry1 = described_class.new(:low, "Message")
65 | entry2 = described_class.new(:high, "Message")
66 | expect([entry1, entry2].sort).to eq([entry2, entry1])
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Store uploaded files on the local file system in a temporary directory.
37 | config.active_storage.service = :test
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Tell Action Mailer not to deliver emails to the real world.
42 | # The :test delivery method accumulates sent emails in the
43 | # ActionMailer::Base.deliveries array.
44 | config.action_mailer.delivery_method = :test
45 |
46 | # Print deprecation notices to the stderr.
47 | config.active_support.deprecation = :stderr
48 |
49 | # Raise exceptions for disallowed deprecations.
50 | config.active_support.disallowed_deprecation = :raise
51 |
52 | # Tell Active Support which deprecation messages to disallow.
53 | config.active_support.disallowed_deprecation_warnings = []
54 |
55 | # Raises error for missing translations.
56 | # config.i18n.raise_on_missing_translations = true
57 |
58 | # Annotate rendered view with file names.
59 | # config.action_view.annotate_rendered_view_with_filenames = true
60 | end
61 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_void_warnings_in_spec_files/results.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": {
3 | "rubocop_version": "1.13.0",
4 | "ruby_engine": "ruby",
5 | "ruby_version": "2.7.2",
6 | "ruby_patchlevel": "137",
7 | "ruby_platform": "x86_64-darwin19"
8 | },
9 | "files": [
10 | {
11 | "path": "spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_void_warnings_in_spec_files/non_spec_file_with_void_warning.rb",
12 | "offenses": [
13 | {
14 | "severity": "warning",
15 | "message": "Operator `==` used in void context.",
16 | "cop_name": "Lint/Void",
17 | "corrected": false,
18 | "correctable": false,
19 | "location": {
20 | "start_line": 2,
21 | "start_column": 13,
22 | "last_line": 2,
23 | "last_column": 14,
24 | "length": 2,
25 | "line": 2,
26 | "column": 13
27 | }
28 | }
29 | ]
30 | },
31 | {
32 | "path": "spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_void_warnings_in_spec_files/spec/non_spec_file_in_spec_dir_with_void_warning.rb",
33 | "offenses": [
34 | {
35 | "severity": "warning",
36 | "message": "Operator `==` used in void context.",
37 | "cop_name": "Lint/Void",
38 | "corrected": false,
39 | "correctable": false,
40 | "location": {
41 | "start_line": 2,
42 | "start_column": 13,
43 | "last_line": 2,
44 | "last_column": 14,
45 | "length": 2,
46 | "line": 2,
47 | "column": 13
48 | }
49 | }
50 | ]
51 | },
52 | {
53 | "path": "spec/workers/commit_monitor_handlers/commit_range/rubocop_checker/data/with_void_warnings_in_spec_files/spec/spec_file_with_void_warning_spec.rb",
54 | "offenses": [
55 | {
56 | "severity": "warning",
57 | "message": "Operator `==` used in void context.",
58 | "cop_name": "Lint/Void",
59 | "corrected": false,
60 | "correctable": false,
61 | "location": {
62 | "start_line": 3,
63 | "start_column": 14,
64 | "last_line": 3,
65 | "last_column": 15,
66 | "length": 2,
67 | "line": 3,
68 | "column": 14
69 | }
70 | }
71 | ]
72 | }
73 | ],
74 | "summary": {
75 | "offense_count": 3,
76 | "target_file_count": 3,
77 | "inspected_file_count": 3
78 | }
79 | }
--------------------------------------------------------------------------------
/lib/github_service/commands/base.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class Base
4 | class << self
5 | def inherited(subclass)
6 | subclass.extend(CommandMethods)
7 |
8 | class_name = subclass.to_s.demodulize
9 | subclass.register_as(class_name.underscore)
10 | end
11 | end
12 |
13 | class_attribute :restriction
14 |
15 | attr_reader :issue
16 |
17 | def initialize(issue)
18 | @issue = issue
19 | end
20 |
21 | ##
22 | # Public interface to Command classes
23 | # Subclasses of Commands::Base should implement #_execute with
24 | # the following keyword arguments:
25 | #
26 | # issuer - The username of the user that issued the command
27 | # value - The value of the command given
28 | #
29 | # No callers should ever use _execute directly, using execute! instead.
30 | #
31 | def execute!(issuer:, value:)
32 | if user_permitted?(issuer)
33 | _execute(:issuer => issuer, :value => value)
34 | end
35 | end
36 |
37 | private
38 |
39 | def _execute
40 | raise NotImplementedError
41 | end
42 |
43 | def user_permitted?(issuer)
44 | case self.class.restriction
45 | when nil
46 | true
47 | when :organization
48 | if GithubService.organization_member?(issue.organization_name, issuer)
49 | true
50 | else
51 | issue.add_comment("@#{issuer} Only members of the #{issue.organization_name} organization may use this command.")
52 | false
53 | end
54 | end
55 | end
56 |
57 | def valid_assignee?(user)
58 | # First reload the cache if it's an invalid assignee
59 | GithubService.refresh_assignees(issue.fq_repo_name) unless GithubService.valid_assignee?(issue.fq_repo_name, user)
60 |
61 | # Then see if it's *still* invalid
62 | GithubService.valid_assignee?(issue.fq_repo_name, user)
63 | end
64 |
65 | VALID_RESTRICTIONS = [:organization].freeze
66 |
67 | module CommandMethods
68 | def register_as(command_name)
69 | CommandDispatcher.register_command(command_name, self)
70 | end
71 | alias alias_as register_as
72 |
73 | def restrict_to(restriction)
74 | unless VALID_RESTRICTIONS.include?(restriction)
75 | raise RestrictionError, "'#{restriction}' is not a valid restriction"
76 | end
77 | self.restriction = restriction
78 | end
79 | end
80 |
81 | RestrictionError = Class.new(StandardError)
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/app/workers/concerns/sidekiq_worker_mixin.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/module/delegation"
2 | require "active_support/concern"
3 |
4 | module SidekiqWorkerMixin
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | delegate :settings, :enabled_repos, :enabled_repo_names, :enabled_for?, :to => :class
9 | delegate :sidekiq_queue, :workers, :running?, :to => :class
10 | end
11 |
12 | module ClassMethods
13 | #
14 | # Settings helper methods
15 | #
16 |
17 | def settings_key
18 | @settings_key ||= name.split("::").last.underscore
19 | end
20 | private :settings_key
21 |
22 | def settings
23 | Settings[settings_key] || Config::Options.new
24 | end
25 |
26 | def included_and_excluded_repos
27 | i = settings.included_repos.try(:flatten)
28 | e = settings.excluded_repos.try(:flatten)
29 | raise "Do not specify both excluded_repos and included_repos in settings for #{settings_key.inspect}" if i && e
30 | return i, e
31 | end
32 | private :included_and_excluded_repos
33 |
34 | def enabled_repos
35 | i, e = included_and_excluded_repos
36 |
37 | if i && !e
38 | Repo.where(:name => i)
39 | elsif !i && e
40 | Repo.where.not(:name => e)
41 | elsif !i && !e
42 | Repo.all
43 | end
44 | end
45 |
46 | def enabled_repo_names
47 | enabled_repos.collect(&:name)
48 | end
49 |
50 | def enabled_for?(repo)
51 | i, e = included_and_excluded_repos
52 |
53 | if i && !e
54 | i.include?(repo.name)
55 | elsif !i && e
56 | !e.include?(repo.name)
57 | elsif !i && !e
58 | true
59 | end
60 | end
61 |
62 | #
63 | # Sidekiq Helper methods
64 | #
65 |
66 | def sidekiq_queue
67 | sidekiq_options unless get_sidekiq_options # init the sidekiq_options_hash
68 | sidekiq_options_hash["queue"]
69 | end
70 |
71 | def workers
72 | queue = sidekiq_queue.to_s
73 |
74 | workers = Sidekiq::Workers.new
75 | workers = workers.select do |_processid, _threadid, work|
76 | work["queue"] == queue && work.fetch_path("payload", "class") == name
77 | end
78 | workers.sort_by! { |_processid, _threadid, work| work.fetch_path("payload", "enqueued_at") }
79 |
80 | workers
81 | end
82 |
83 | def running?(workers = nil)
84 | (workers || self.workers).any?
85 | end
86 | end
87 |
88 | #
89 | # Sidekiq Helper methods
90 | #
91 |
92 | def first_unique_worker?(workers = nil)
93 | _processid, _threadid, work = (workers || self.workers).first
94 | work.nil? || work.fetch_path("payload", "jid") == jid
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | ENV["RAILS_ENV"] ||= 'test'
3 | require File.expand_path("../../config/environment", __FILE__)
4 |
5 | require 'rspec/rails'
6 | require 'webmock/rspec'
7 |
8 | # Requires supporting ruby files with custom matchers and macros, etc,
9 | # in spec/support/ and its subdirectories.
10 |
11 | Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
12 |
13 | # Checks for pending migrations before tests are run.
14 | # If you are not using ActiveRecord, you can remove this line.
15 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)
16 |
17 | RSpec.configure do |config|
18 | # ## Mock Framework
19 | #
20 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
21 | #
22 | # config.mock_with :mocha
23 | # config.mock_with :flexmock
24 | # config.mock_with :rr
25 |
26 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
27 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
28 |
29 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
30 | # examples within a transaction, remove the following line or assign false
31 | # instead of true.
32 | config.use_transactional_fixtures = true
33 |
34 | # If true, the base class of anonymous controllers will be inferred
35 | # automatically. This will be the default behavior in future versions of
36 | # rspec-rails.
37 | config.infer_base_class_for_anonymous_controllers = false
38 |
39 | # Run specs in random order to surface order dependencies. If you find an
40 | # order dependency and want to debug it, you can fix the order by providing
41 | # the seed, which is printed after each run.
42 | # --seed 1234
43 | config.order = "random"
44 |
45 | config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] }
46 |
47 | config.include FactoryBot::Syntax::Methods
48 |
49 | require "awesome_spawn/spec_helper"
50 | config.include AwesomeSpawn::SpecHelper
51 |
52 | config.before do
53 | allow_any_instance_of(MinigitService).to receive(:service)
54 | .and_raise("Live execution is not allowed in specs. Use stubs/expectations on service instead.")
55 | end
56 |
57 | config.after do
58 | Module.clear_all_cache_with_timeout
59 |
60 | # Disable rubocop check because .empty? doesn't exist on a Sidekiq Queue
61 | raise "miq_bot queue is not empty" unless Sidekiq::Queue.new("miq_bot").size == 0 # rubocop:disable Style/ZeroLengthPredicate
62 | raise "miq_bot_glacial queue is not empty" unless Sidekiq::Queue.new("miq_bot_glacial").size == 0 # rubocop:disable Style/ZeroLengthPredicate
63 | end
64 | end
65 |
66 | WebMock.disable_net_connect!(:allow_localhost => true)
67 |
--------------------------------------------------------------------------------
/spec/workers/concerns/code_analysis_mixin_spec.rb:
--------------------------------------------------------------------------------
1 | describe CodeAnalysisMixin do
2 | let(:example_rubocop_result) { {"metadata"=>{"rubocop_version"=>"0.47.1", "ruby_engine"=>"ruby", "ruby_version"=>"2.3.3", "ruby_patchlevel"=>"222", "ruby_platform"=>"x86_64-linux"}, "files"=>[{"path"=>"app/helpers/application_helper/button/mixins/discovery_mixin.rb", "offenses"=>[]}, {"path"=>"app/helpers/application_helper/button/button_new_discover.rb", "offenses"=>[{"severity"=>"warning", "message"=>"Method `ApplicationHelper::Button::ButtonNewDiscover#visible?` is defined at both /tmp/d20171201-9050-1m4n90/app/helpers/application_helper/button/button_new_discover.rb:5 and /tmp/d20171201-9050-1m4n90/app/helpers/application_helper/button/button_new_discover.rb:9.", "cop_name"=>"Lint/DuplicateMethods", "corrected"=>nil, "location"=>{"line"=>9, "column"=>3, "length"=>3}}]}], "summary"=>{"offense_count"=>1, "target_file_count"=>2, "inspected_file_count"=>2}} }
3 | let(:test_class) do
4 | Class.new do
5 | include CodeAnalysisMixin
6 | attr_reader :branch
7 | end
8 | end
9 | subject { test_class.new }
10 |
11 | context "#merged_linter_results" do
12 | it "should always return a hash with a 'files' and 'summary' key, even with no cops running" do
13 | expect(Linter::Rubocop).to receive(:new).and_return(double(:run => nil))
14 | expect(Linter::Haml).to receive(:new).and_return(double(:run => nil))
15 | expect(Linter::Yaml).to receive(:new).and_return(double(:run => nil))
16 |
17 | expect(subject.merged_linter_results).to eq("files"=>[], "summary"=>{"inspected_file_count"=>0, "offense_count"=>0, "target_file_count"=>0})
18 | end
19 |
20 | it "merges together with one result" do
21 | expect(Linter::Rubocop).to receive(:new).and_return(double(:run => example_rubocop_result))
22 | expect(Linter::Haml).to receive(:new).and_return(double(:run => nil))
23 | expect(Linter::Yaml).to receive(:new).and_return(double(:run => nil))
24 |
25 | expect(subject.merged_linter_results).to eq(
26 | "files" => example_rubocop_result["files"],
27 | "summary" => {"inspected_file_count"=>2, "offense_count"=>1, "target_file_count"=>2}
28 | )
29 | end
30 |
31 | it "merges together with one result" do
32 | expect(Linter::Rubocop).to receive(:new).and_return(double(:run => example_rubocop_result))
33 | expect(Linter::Haml).to receive(:new).and_return(double(:run => example_rubocop_result))
34 | expect(Linter::Yaml).to receive(:new).and_return(double(:run => nil))
35 |
36 | results = subject.merged_linter_results
37 | expect(results["files"]).to include(*example_rubocop_result["files"])
38 | expect(results["summary"]).to eq("inspected_file_count"=>4, "offense_count"=>2, "target_file_count"=>4)
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # NOTE: This is enabled in order to autoload some of the dynamically loaded
12 | # classes in development, such as the GithubService::Commands classes
13 | config.eager_load = true
14 |
15 | # Show full error reports.
16 | config.consider_all_requests_local = true
17 |
18 | # Enable server timing
19 | config.server_timing = true
20 |
21 | # Enable/disable caching. By default caching is disabled.
22 | # Run rails dev:cache to toggle caching.
23 | if Rails.root.join("tmp/caching-dev.txt").exist?
24 | config.action_controller.perform_caching = true
25 | config.action_controller.enable_fragment_cache_logging = true
26 |
27 | config.cache_store = :memory_store
28 | config.public_file_server.headers = {
29 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
30 | }
31 | else
32 | config.action_controller.perform_caching = false
33 |
34 | config.cache_store = :null_store
35 | end
36 |
37 | # Store uploaded files on the local file system (see config/storage.yml for options).
38 | config.active_storage.service = :local
39 |
40 | # Don't care if the mailer can't send.
41 | config.action_mailer.raise_delivery_errors = false
42 |
43 | config.action_mailer.perform_caching = false
44 |
45 | # Print deprecation notices to the Rails logger.
46 | config.active_support.deprecation = :log
47 |
48 | # Raise exceptions for disallowed deprecations.
49 | config.active_support.disallowed_deprecation = :raise
50 |
51 | # Tell Active Support which deprecation messages to disallow.
52 | config.active_support.disallowed_deprecation_warnings = []
53 |
54 | # Raise an error on page load if there are pending migrations.
55 | config.active_record.migration_error = :page_load
56 |
57 | # Highlight code that triggered database queries in logs.
58 | config.active_record.verbose_query_logs = true
59 |
60 | # Suppress logger output for asset requests.
61 | config.assets.quiet = true
62 |
63 | # Raises error for missing translations.
64 | # config.i18n.raise_on_missing_translations = true
65 |
66 | # Annotate rendered view with file names.
67 | # config.action_view.annotate_rendered_view_with_filenames = true
68 |
69 | # Uncomment if you wish to allow Action Cable access from any origin.
70 | # config.action_cable.disable_request_forgery_protection = true
71 | end
72 |
--------------------------------------------------------------------------------
/app/workers/stale_issue_marker.rb:
--------------------------------------------------------------------------------
1 | class StaleIssueMarker
2 | include Sidekiq::Worker
3 | sidekiq_options :queue => :miq_bot_glacial, :retry => false
4 |
5 | include SidekiqWorkerMixin
6 |
7 | SEARCH_SORTING = {:sort => :updated, :direction => :asc}.freeze
8 |
9 | def perform
10 | if !first_unique_worker?
11 | logger.info("#{self.class} is already running, skipping")
12 | else
13 | process_stale_issues
14 | end
15 | end
16 |
17 | private
18 |
19 | # If an issue/pr has any of these labels, it will not be marked as stale or closed
20 | def pinned_labels
21 | Array(settings.pinned_labels || ["pinned"])
22 | end
23 |
24 | # Triage logic:
25 | #
26 | # After 3 month of no activity:
27 | # - add stale tag (if it is no there)
28 | # - add comment
29 | #
30 | def process_stale_issues
31 | handle_newly_stale_issues
32 | end
33 |
34 | def handle_newly_stale_issues
35 | query = "is:open archived:false updated:<#{stale_date.strftime('%Y-%m-%d')}"
36 | query << " " << enabled_repos_query_filter
37 | query << " " << unpinned_query_filter
38 |
39 | GithubService.search_issues(query, **SEARCH_SORTING).each do |issue|
40 | comment_as_stale(issue)
41 | end
42 | end
43 |
44 | def stale_date
45 | @stale_date ||= 3.months.ago
46 | end
47 |
48 | def stale_label
49 | GithubService::Issue::STALE_LABEL
50 | end
51 |
52 | def enabled_repos_query_filter
53 | enabled_repo_names.map { |repo| %(repo:"#{repo}") }.join(" ")
54 | end
55 |
56 | def unpinned_query_filter
57 | pinned_labels.map { |label| %(-label:"#{label}") }.join(" ")
58 | end
59 |
60 | def validate_repo_has_stale_label(repo)
61 | unless GithubService.valid_label?(repo, stale_label)
62 | raise "The label #{stale_label} does not exist on #{repo}"
63 | end
64 | end
65 |
66 | def mark_as_stale(issue)
67 | return if issue.stale?
68 |
69 | validate_repo_has_stale_label(issue.fq_repo_name)
70 | issue.add_labels([stale_label])
71 | end
72 |
73 | def comment_as_stale(issue)
74 | mark_as_stale(issue)
75 |
76 | message = "This #{issue.type} has been automatically marked as stale " \
77 | "because it has not been updated for at least 3 months.\n\n"
78 | message <<
79 | if issue.pull_request?
80 | "If these changes are still valid, please remove the " \
81 | "`stale` label, make any changes requested by reviewers " \
82 | "(if any), and ensure that this issue is being looked " \
83 | "at by the assigned/reviewer(s)."
84 | else
85 | "If you can still reproduce this issue on the current " \
86 | "release or on `master`, please reply with all of the " \
87 | "information you have about it in order to keep the " \
88 | "issue open."
89 | end
90 |
91 | logger.info("[#{Time.now.utc}] - Marking issue #{issue.fq_repo_name}##{issue.number} as stale")
92 | issue.add_comment(message)
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/app/workers/commit_monitor_handlers/commit_range/github_pr_commenter/commit_metadata_checker.rb:
--------------------------------------------------------------------------------
1 | require 'rugged'
2 |
3 | module CommitMonitorHandlers::CommitRange
4 | class GithubPrCommenter::CommitMetadataChecker
5 | include Sidekiq::Worker
6 | sidekiq_options :queue => :miq_bot_glacial
7 |
8 | include BatchEntryWorkerMixin
9 | include BranchWorkerMixin
10 |
11 | def perform(batch_entry_id, branch_id, new_commits)
12 | return unless find_batch_entry(batch_entry_id)
13 | return skip_batch_entry unless find_branch(branch_id, :pr)
14 |
15 | complete_batch_entry(:result => process_commits(new_commits))
16 | end
17 |
18 | private
19 |
20 | def process_commits(new_commits)
21 | @offenses = []
22 |
23 | new_commits.each do |commit_sha, data|
24 | check_for_usernames_in(commit_sha, data["message"])
25 | check_for_merge_commit(commit_sha, data["merge_commit?"])
26 | end
27 |
28 | @offenses
29 | end
30 |
31 | # From https://github.com/join
32 | #
33 | # "Username may only contain alphanumeric characters or single hyphens,
34 | # and cannot begin or end with a hyphen."
35 | #
36 | # To check for the start of a username we do a positive lookbehind to get
37 | # the `@` (but only if it is surrounded by whitespace), and a positive
38 | # lookhead at the end to confirm there is a whitespace char following the
39 | # "var" (isn't an instance variable with a trailing `.`, has an `_`, or is
40 | # actually an email address)
41 | #
42 | # Since there can't be underscores in Github usernames, this makes it so we
43 | # rule out partial matches of variables (@database_records having a
44 | # username lookup of `database`), but still catch full variable names
45 | # without underscores (`@foobarbaz`).
46 | #
47 | USERNAME_REGEXP = /
48 | (?<=^@|\s@) # must start with a '@' (don't capture)
49 | [a-zA-Z0-9] # first character must be alphanumeric
50 | [a-zA-Z0-9\-]* # middle chars may be alphanumeric or hyphens
51 | [a-zA-Z0-9] # last character must be alphanumeric
52 | (?=[\s]) # allow only variables without "_" (not captured)
53 | /x.freeze
54 |
55 | def check_for_usernames_in(commit, message)
56 | message.scan(USERNAME_REGEXP).each do |potential_username|
57 | next unless GithubService.username_lookup(potential_username)
58 |
59 | group = ::Branch.github_commit_uri(fq_repo_name, commit)
60 | message = "Username `@#{potential_username}` detected in commit message. Consider removing."
61 | @offenses << OffenseMessage::Entry.new(:low, message, group)
62 | end
63 | end
64 |
65 | def check_for_merge_commit(commit, merge_commit)
66 | return unless merge_commit
67 |
68 | group = ::Branch.github_commit_uri(fq_repo_name, commit)
69 | message = "Merge commit #{commit} detected. Consider rebasing."
70 | @offenses << OffenseMessage::Entry.new(:warn, message, group)
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/spec/workers/pull_request_monitor_spec.rb:
--------------------------------------------------------------------------------
1 | describe PullRequestMonitor do
2 | describe "#process_repo" do
3 | let(:repo) { create(:repo) }
4 |
5 | let(:github_pr_head_repo) { double("Github repo", :html_url => "https://github.com/SomeUser/some_repo") }
6 | let(:github_pr) do
7 | double("Github PR",
8 | :number => 1,
9 | :title => "PR number 1",
10 |
11 | :base => double("Github PR base",
12 | :ref => "master",
13 | :repo => double("Github repo", :html_url => "https://github.com/#{repo.name}")
14 | ),
15 |
16 | :head => double("Github PR head",
17 | :repo => github_pr_head_repo
18 | )
19 | )
20 | end
21 |
22 | def stub_git_service
23 | expect(repo).to receive(:git_fetch)
24 | double("Git service", :merge_base => "123abc").tap do |git_service|
25 | allow_any_instance_of(Branch).to receive(:git_service).and_return(git_service)
26 | end
27 | end
28 |
29 | it "ignores a repo that can't have PRs (because of no upstream_user)" do
30 | repo.update!(:name => "foo")
31 |
32 | expect(repo).to_not receive(:synchronize_pr_branches)
33 |
34 | described_class.new.process_repo(repo)
35 | end
36 |
37 | it "with Github PRs" do
38 | stub_github_prs(github_pr)
39 | stub_git_service
40 |
41 | expect(repo).to receive(:synchronize_pr_branches).with([{
42 | :number => 1,
43 | :html_url => "https://github.com/SomeUser/some_repo",
44 | :merge_target => "master",
45 | :pr_title => "PR number 1"
46 | }]).and_call_original
47 | PullRequestMonitorHandlers.constants.each do |c|
48 | expect(PullRequestMonitorHandlers.const_get(c)).to receive(:perform_async)
49 | end
50 |
51 | described_class.new.process_repo(repo)
52 | end
53 |
54 | context "when the Github PR head.repo is nil" do
55 | let(:github_pr_head_repo) { nil }
56 |
57 | it "creates a PR branch" do
58 | stub_github_prs(github_pr)
59 | stub_git_service
60 |
61 | expect(repo).to receive(:synchronize_pr_branches).with([{
62 | :number => 1,
63 | :html_url => "https://github.com/#{repo.name}",
64 | :merge_target => "master",
65 | :pr_title => "PR number 1"
66 | }]).and_call_original
67 | PullRequestMonitorHandlers.constants.each do |c|
68 | expect(PullRequestMonitorHandlers.const_get(c)).to receive(:perform_async)
69 | end
70 |
71 | described_class.new.process_repo(repo)
72 | end
73 | end
74 |
75 | it "when there are no Github PRs" do
76 | stub_github_prs([])
77 | stub_git_service
78 |
79 | expect(repo).to receive(:synchronize_pr_branches).with([]).and_call_original
80 | PullRequestMonitorHandlers.constants.each do |c|
81 | expect(PullRequestMonitorHandlers.const_get(c)).to_not receive(:perform_async)
82 | end
83 |
84 | described_class.new.process_repo(repo)
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/spec/lib/github_service/command_dispatcher_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe GithubService::CommandDispatcher do
2 | subject(:command_dispatcher) { described_class.new(issue) }
3 |
4 | let(:bot_name) { "miq-bot" }
5 | let(:command_issuer) { "chessbyte" }
6 | let(:fq_repo_name) { "foo/bar" }
7 | let(:issue) do
8 | double('issue',
9 | :user => double(:login => "chrisarcand"),
10 | :body => "Opened this issue",
11 | :number => 1,
12 | :labels => [],
13 | :repository_url => "https://api.fakegithub.com/repos/#{fq_repo_name}")
14 | end
15 |
16 | describe "#dispatch!" do
17 | before do
18 | allow(Settings).to receive(:github_credentials).and_return(double(:username => bot_name))
19 | end
20 |
21 | after do
22 | command_dispatcher.dispatch!(:issuer => command_issuer, :text => text)
23 | end
24 |
25 | context "when 'assign' command is given" do
26 | let(:text) { "@#{bot_name} assign chrisarcand" }
27 | let(:command_class) { double }
28 |
29 | it "dispatches to Assign" do
30 | expect(GithubService::Commands::Assign).to receive(:new).and_return(command_class)
31 | expect(command_class).to receive(:execute!)
32 | .with(:issuer => command_issuer, :value => "chrisarcand")
33 | end
34 |
35 | context "if the bot is the target" do
36 | let(:command_issuer) { bot_name }
37 |
38 | it "does nothing" do
39 | expect(GithubService::Commands::Assign).to receive(:new).never
40 | expect(command_class).to receive(:execute!).never
41 | end
42 | end
43 | end
44 |
45 | context "when 'add_labels' command is given" do
46 | let(:text) { "@#{bot_name} add-label question, wontfix" }
47 | let(:command_class) { double }
48 |
49 | it "dispatches to AddLabel" do
50 | expect(GithubService::Commands::AddLabel).to receive(:new).and_return(command_class)
51 | expect(command_class).to receive(:execute!)
52 | .with(:issuer => command_issuer, :value => "question, wontfix")
53 | end
54 |
55 | context "with extra space in command" do
56 | let(:text) { "@#{bot_name} add label question, wontfix" }
57 |
58 | it "doesn't dispatch and comments on error" do
59 | expect(GithubService::Commands::AddLabel).to_not receive(:new)
60 | expect(command_dispatcher.issue).to receive(:add_comment)
61 | .with(a_string_including("@#{command_issuer} unrecognized command 'add'"))
62 | end
63 | end
64 | end
65 |
66 | context "when 'remove_labels' command is given" do
67 | let(:text) { "@#{bot_name} remove-label question" }
68 | let(:command_class) { double }
69 |
70 | it "dispatches to RemoveLabel" do
71 | expect(GithubService::Commands::RemoveLabel).to receive(:new).and_return(command_class)
72 | expect(command_class).to receive(:execute!)
73 | .with(:issuer => command_issuer, :value => "question")
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/git_service/commit.rb:
--------------------------------------------------------------------------------
1 | module GitService
2 | class Commit
3 | attr_reader :commit_oid, :rugged_repo
4 | def initialize(rugged_repo, commit_oid)
5 | @commit_oid = commit_oid
6 | @rugged_repo = rugged_repo
7 | end
8 |
9 | def diff(other_ref = parent_oids.first)
10 | Diff.new(rugged_diff(other_ref))
11 | end
12 |
13 | def parent_oids
14 | @parent_oids ||= rugged_commit.parent_oids
15 | end
16 |
17 | def rugged_commit
18 | @rugged_commit ||= Rugged::Commit.lookup(rugged_repo, commit_oid)
19 | end
20 |
21 | def rugged_diff(other_ref = parent_oids.first)
22 | other_commit = Rugged::Commit.lookup(rugged_repo, other_ref)
23 | other_commit.diff(rugged_commit)
24 | end
25 |
26 | def formatted_author
27 | "#{rugged_commit.author[:name]} <#{rugged_commit.author[:email]}>"
28 | end
29 |
30 | def formatted_author_date
31 | rugged_commit.author[:time].to_time.strftime("%c %z")
32 | end
33 |
34 | def formatted_committer
35 | "#{rugged_commit.committer[:name]} <#{rugged_commit.committer[:email]}>"
36 | end
37 |
38 | def formatted_committer_date
39 | rugged_commit.committer[:time].to_time.strftime("%c %z")
40 | end
41 |
42 | # Note: really not needed, but keeps it consistent
43 | def formatted_commit_message
44 | rugged_commit.message
45 | end
46 |
47 | def formatted_commit_stats
48 | diff.file_status.map do |file, stats|
49 | file_stats = file.dup
50 | file_stats << " | "
51 | file_stats << (stats[:additions].to_i + stats[:deletions].to_i).to_s
52 | file_stats << " "
53 | file_stats << "+" if stats[:additions].positive?
54 | file_stats << "-" if stats[:deletions].positive?
55 | file_stats
56 | end
57 | end
58 |
59 | def full_message
60 | message = "commit #{commit_oid}\n"
61 | message << "Merge: #{parent_oids.join(" ")}\n" if parent_oids.length > 1
62 | message << "Author: #{formatted_author}\n"
63 | message << "AuthorDate: #{formatted_author_date}\n"
64 | message << "Commit: #{formatted_committer}\n"
65 | message << "CommitDate: #{formatted_committer_date}\n"
66 | message << "\n"
67 | message << formatted_commit_message.indent(4)
68 | message << "\n"
69 | message << formatted_commit_stats.join("\n").indent(1)
70 | message << "\n #{diff.status_summary}"
71 | message
72 | end
73 |
74 | def details_hash
75 | {
76 | "sha" => commit_oid,
77 | "parent_oids" => parent_oids,
78 | "merge_commit?" => parent_oids.length > 1,
79 | "author" => formatted_author,
80 | "author_date" => formatted_author_date,
81 | "commit" => formatted_committer,
82 | "commit_date" => formatted_committer_date,
83 | "message" => formatted_commit_message,
84 | "files" => diff.file_status.keys,
85 | "stats" => formatted_commit_stats,
86 | "status_summary" => diff.status_summary
87 | }
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/spec/workers/commit_monitor_handlers/commit_range/github_pr_commenter/diff_filename_checker_spec.rb:
--------------------------------------------------------------------------------
1 | describe CommitMonitorHandlers::CommitRange::GithubPrCommenter::DiffFilenameChecker do
2 | let(:batch_entry) { BatchEntry.create!(:job => BatchJob.create!) }
3 | let(:branch) { create(:pr_branch) }
4 | let(:git_service) { double("GitService", :diff => double("RuggedDiff", :new_files => new_files)) }
5 |
6 | before do
7 | stub_sidekiq_logger
8 | stub_job_completion
9 | expect_any_instance_of(Branch).to receive(:git_service).and_return(git_service)
10 | end
11 |
12 | context "Migration Timestamp" do
13 | context "with bad migration dates" do
14 | let(:new_files) do
15 | [
16 | "db/migrate/20151435234623_do_some_stuff.rb", # bad
17 | "db/migrate/20150821123456_do_some_stuff.rb", # good
18 | "blah.rb" # ignored
19 | ]
20 | end
21 |
22 | it "with one bad, one good, one ignored" do
23 | described_class.new.perform(batch_entry.id, branch.id, nil)
24 |
25 | batch_entry.reload
26 | expect(batch_entry.result.length).to eq(1)
27 | expect(batch_entry.result.first).to have_attributes(:group => "db/migrate/20151435234623_do_some_stuff.rb", :message => "Bad Migration Timestamp")
28 | end
29 | end
30 |
31 | context "with multiple bad migration dates" do
32 | let(:new_files) do
33 | [
34 | "db/migrate/20151435234623_do_some_stuff.rb", # bad
35 | "db/migrate/20151435234624_do_some_stuff.rb", # bad
36 | "db/migrate/20150821123456_do_some_stuff.rb", # good
37 | "blah.rb" # ignored
38 | ]
39 | end
40 |
41 | it "with two bad, one good, one ignored" do
42 | described_class.new.perform(batch_entry.id, branch.id, nil)
43 |
44 | batch_entry.reload
45 | expect(batch_entry.result.length).to eq(2)
46 | results = batch_entry.result.sort!
47 | expect(results.first).to have_attributes(:group => "db/migrate/20151435234623_do_some_stuff.rb", :message => "Bad Migration Timestamp")
48 | expect(results.last).to have_attributes(:group => "db/migrate/20151435234624_do_some_stuff.rb", :message => "Bad Migration Timestamp")
49 | end
50 | end
51 |
52 | context "with no bad migration dates" do
53 | let(:new_files) do
54 | [
55 | "db/migrate/20150821123456_do_some_stuff.rb", # good
56 | "blah.rb" # ignored
57 | ]
58 | end
59 |
60 | it "one good, one ignored" do
61 | described_class.new.perform(batch_entry.id, branch.id, nil)
62 |
63 | expect(batch_entry.reload.result).to eq([])
64 | end
65 | end
66 |
67 | context "with no migrations" do
68 | let(:new_files) do
69 | [
70 | "blah.rb" # ignored
71 | ]
72 | end
73 |
74 | it "one ignored" do
75 | described_class.new.perform(batch_entry.id, branch.id, nil)
76 |
77 | expect(batch_entry.reload.result).to eq([])
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/minigit_service.rb:
--------------------------------------------------------------------------------
1 | class MinigitService
2 | include ThreadsafeServiceMixin
3 |
4 | def self.clone(*args)
5 | require 'awesome_spawn'
6 | STDERR.puts "+ #{AwesomeSpawn.build_command_line("git clone", args)}"
7 | STDERR.puts AwesomeSpawn.run!("git clone", :params => args).output
8 | true
9 | rescue AwesomeSpawn::CommandResultError => err
10 | require 'minigit'
11 | raise MiniGit::GitError.new(["clone"], err.result.error.chomp)
12 | end
13 |
14 | # All MiniGit methods return stdout which always has a trailing newline
15 | # that is never wanted, so remove it always.
16 | def delegate_to_service(method_name, *args)
17 | super.chomp
18 | end
19 |
20 | attr_reader :path_to_repo
21 |
22 | def initialize(path_to_repo)
23 | @path_to_repo = path_to_repo
24 | service # initialize the service
25 | end
26 |
27 | def service
28 | @service ||= begin
29 | require 'minigit'
30 | MiniGit.debug = true
31 | MiniGit::Capturing.new(File.expand_path(path_to_repo))
32 | end
33 | end
34 |
35 | def new_commits(since_commit, ref = "HEAD")
36 | rev_list({:reverse => true}, "#{since_commit}..#{ref}").split("\n")
37 | end
38 |
39 | def commit_message(commit)
40 | show({:pretty => "fuller"}, "--stat", "--summary", commit)
41 | end
42 |
43 | def current_ref
44 | rev_parse("HEAD")
45 | end
46 |
47 | def diff_details(commit1, commit2 = nil)
48 | if commit2.nil?
49 | commit2 = commit1
50 | commit1 = "#{commit1}~"
51 | end
52 | output = diff("--patience", "-U0", "--no-color", "#{commit1}...#{commit2}")
53 |
54 | ret = Hash.new { |h, k| h[k] = [] }
55 | path = line_number = nil
56 | output.each_line do |line|
57 | # Note: We are intentionally ignoring deletes "-" for now
58 | case line
59 | when /^--- (?:a\/)?/
60 | next
61 | when /^\+\+\+ (?:b\/)?(.+)/
62 | path = $1.chomp
63 | when /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/
64 | line_number = $1.to_i
65 | when /^[ +]/
66 | ret[path] << line_number
67 | line_number += 1
68 | end
69 | end
70 | ret
71 | end
72 |
73 | def diff_file_names(commit1, commit2 = nil)
74 | if commit2.nil?
75 | commit2 = commit1
76 | commit1 = "#{commit1}~"
77 | end
78 | diff("--name-only", "#{commit1}...#{commit2}").split
79 | end
80 |
81 | #
82 | # Pull Request specific methods
83 | #
84 |
85 | def self.pr_branch(pr_number)
86 | "prs/#{pr_number}/head"
87 | end
88 | delegate :pr_branch, :to => :class
89 |
90 | def self.pr_number(branch)
91 | branch.split("/")[1].to_i
92 | end
93 | delegate :pr_number, :to => :class
94 |
95 | def remotes
96 | remote.split("\n").uniq.compact
97 | end
98 |
99 | def fetches(remote)
100 | config("--get-all", "remote.#{remote}.fetch").split("\n").compact
101 | end
102 |
103 | def ensure_prs_refs
104 | remotes.each do |remote_name|
105 | config("--add", "remote.#{remote_name}.fetch", "+refs/pull/*:refs/prs/*") unless fetches(remote_name).include?("+refs/pull/*:refs/prs/*")
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/spec/lib/github_notification_monitor_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe GithubNotificationMonitor do
2 | subject(:notification_monitor) { described_class.new(fq_repo_name) }
3 |
4 | let(:notification) { double('notification', :issue_number => issue.number) }
5 | let(:issue) do
6 | double('issue',
7 | :author => "notchrisarcand",
8 | :body => "Opened this issue",
9 | :number => 1,
10 | :created_at => 15.minutes.ago,
11 | :labels => [],
12 | :repository_url => "https://api.fakegithub.com/repos/#{fq_repo_name}")
13 | end
14 | let(:comments) do
15 | [
16 | double('comment',
17 | :author => "Commenter One",
18 | :updated_at => 14.minutes.ago,
19 | :body => "This is an old comment."),
20 | double('comment',
21 | :author => "Commenter Two",
22 | :updated_at => 8.minutes.ago,
23 | :body => "This is a new comment."),
24 | double('comment',
25 | :author => "Commenter Three",
26 | :updated_at => 3.minutes.ago,
27 | :body => "This is also a new comment.")
28 | ]
29 | end
30 | let(:username) { "miq-bot" }
31 | let(:fq_repo_name) { "foo/bar" }
32 | let(:command_dispatcher) { double }
33 |
34 | describe "#process_notifications" do
35 | before do
36 | allow(Settings).to receive(:github_credentials).and_return(double(:username => username))
37 | allow(File).to receive(:write)
38 | .with(described_class::GITHUB_NOTIFICATION_MONITOR_YAML_FILE, anything)
39 | allow(YAML).to receive(:load_file)
40 | .with(described_class::GITHUB_NOTIFICATION_MONITOR_YAML_FILE, {:permitted_classes=>[Date, Time]}) do
41 | { "timestamps" => { fq_repo_name => { issue.number => 10.minutes.ago } } }
42 | end
43 | allow(GithubService).to receive(:repository_notifications)
44 | .with(fq_repo_name, a_hash_including("all" => false)).and_return([notification])
45 | allow(GithubService).to receive(:issue)
46 | .with(fq_repo_name, notification.issue_number).and_return(issue)
47 | allow(GithubService).to receive(:issue_comments)
48 | .with(fq_repo_name, issue.number).and_return(comments)
49 | end
50 |
51 | after do
52 | notification_monitor.process_notifications
53 | end
54 |
55 | it "calls the command dispatcher for new comments and marks notification as read" do
56 | expect(GithubService::CommandDispatcher).to receive(:new).with(issue).and_return(command_dispatcher)
57 | expect(command_dispatcher).not_to receive(:dispatch!)
58 | .with(:issuer => "notchrisarcand", :text => issue.body)
59 | expect(command_dispatcher).not_to receive(:dispatch!)
60 | .with(:issuer => "Commenter One", :text => "This is an old comment.")
61 | expect(command_dispatcher).to receive(:dispatch!)
62 | .with(:issuer => "Commenter Two", :text => "This is a new comment.")
63 | expect(command_dispatcher).to receive(:dispatch!)
64 | .with(:issuer => "Commenter Three", :text => "This is also a new comment.")
65 | expect(notification).to receive(:mark_thread_as_read)
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM registry.access.redhat.com/ubi9/ubi:latest
2 |
3 | ARG REF=master
4 |
5 | ENV TERM=xterm \
6 | APP_ROOT=/opt/miq_bot
7 |
8 | LABEL name="miq-bot" \
9 | vendor="ManageIQ" \
10 | url="https://manageiq.org/" \
11 | summary="ManageIQ Bot application image" \
12 | description="ManageIQ Bot is a developer automation tool." \
13 | io.k8s.display-name="ManageIQ Bot" \
14 | io.k8s.description="ManageIQ Bot is a developer automation tool." \
15 | io.openshift.tags="ManageIQ,miq-bot"
16 |
17 | RUN curl -L -o /usr/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 && \
18 | chmod +x /usr/bin/dumb-init
19 |
20 | RUN ARCH=$(uname -m) && \
21 | dnf -y --setopt=protected_packages= remove redhat-release && \
22 | dnf -y install \
23 | http://mirror.stream.centos.org/9-stream/BaseOS/${ARCH}/os/Packages/centos-stream-release-9.0-26.el9.noarch.rpm \
24 | http://mirror.stream.centos.org/9-stream/BaseOS/${ARCH}/os/Packages/centos-stream-repos-9.0-26.el9.noarch.rpm \
25 | http://mirror.stream.centos.org/9-stream/BaseOS/${ARCH}/os/Packages/centos-gpg-keys-9.0-26.el9.noarch.rpm && \
26 | dnf -y install \
27 | https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm && \
28 | dnf -y --disablerepo=ubi-9-baseos-rpms swap openssl-fips-provider openssl-libs && \
29 | dnf -y --disableplugin=subscription-manager module enable nodejs:18 && \
30 | dnf -y module enable ruby:3.1 && \
31 | dnf -y update && \
32 | dnf clean all && \
33 | rm -rf /var/cache/dnf
34 |
35 | RUN dnf -y --disableplugin=subscription-manager --setopt=tsflags=nodocs install \
36 | @development \
37 | cmake \
38 | git \
39 | libcurl-devel \
40 | libffi-devel \
41 | libssh2-devel \
42 | libyaml-devel \
43 | libxml2-devel \
44 | openssl \
45 | openssl-devel \
46 | postgresql-devel \
47 | ruby \
48 | ruby-devel \
49 | shared-mime-info \
50 | sqlite-devel \
51 | yamllint && \
52 | dnf -y update libarchive && \
53 | # Clean up all the things
54 | dnf clean all && \
55 | rm -rf /var/cache/dnf && \
56 | rm -rf /var/lib/dnf/history* && \
57 | rm -rf /var/log/dnf*.log && \
58 | rm -rf /var/log/hawkey.log && \
59 | rm -rf /var/lib/rpm/__db.*
60 |
61 | RUN mkdir -p $APP_ROOT && \
62 | curl -L https://github.com/ManageIQ/miq_bot/archive/$REF.tar.gz | tar xz -C $APP_ROOT --strip 1 && \
63 | chgrp -R 0 $APP_ROOT && \
64 | chmod -R g=u $APP_ROOT && \
65 | cp $APP_ROOT/container-assets/container_env /usr/local/bin && \
66 | cp $APP_ROOT/container-assets/entrypoint /usr/local/bin && \
67 | echo "$REF" > $APP_ROOT/VERSION
68 |
69 | WORKDIR $APP_ROOT
70 |
71 | RUN echo "gem: --no-document" > ~/.gemrc && \
72 | bundle config set --local build.rugged --with-ssh && \
73 | bundle install --jobs=3 --retry=3 && \
74 | # Clean up all the things
75 | rm -rf /usr/share/gems/cache/* && \
76 | rm -rf /usr/share/gems/gems/rugged-*/vendor && \
77 | find /usr/share/gems/gems/ -name *.o -type f -delete && \
78 | find /usr/share/gems/gems/ -maxdepth 2 -name docs -type d -exec rm -r {} + && \
79 | find /usr/share/gems/gems/ -maxdepth 2 -name spec -type d -exec rm -r {} + && \
80 | find /usr/share/gems/gems/ -maxdepth 2 -name test -type d -exec rm -r {} +
81 |
82 | ENTRYPOINT ["/usr/bin/dumb-init", "--single-child", "--"]
83 |
84 | CMD ["entrypoint"]
85 |
--------------------------------------------------------------------------------
/app/models/branch.rb:
--------------------------------------------------------------------------------
1 | class Branch < ActiveRecord::Base
2 | belongs_to :repo
3 |
4 | validates :name, :presence => true, :uniqueness => {:scope => :repo}
5 | validates :commit_uri, :presence => true
6 | validates :last_commit, :presence => true
7 | validates :repo, :presence => true
8 |
9 | serialize :commits_list, Array
10 |
11 | default_value_for(:commits_list) { [] }
12 | default_value_for :mergeable, true
13 | default_value_for :pull_request, false
14 |
15 | after_initialize(:unless => :commit_uri) { self.commit_uri = self.class.github_commit_uri(repo.try(:name)) }
16 |
17 | scope :regular_branches, -> { where(:pull_request => [false, nil]) }
18 | scope :pr_branches, -> { where(:pull_request => true) }
19 |
20 | def self.create_all_from_master(name)
21 | Repo.all.each do |repo|
22 | next if repo.branches.exists?(:name => name)
23 |
24 | b = repo.branches.new(:name => name)
25 | next unless b.git_service.exists?
26 |
27 | b.last_commit = b.git_service.merge_base("master")
28 | b.save!
29 | puts "Created #{name} on #{repo.name}"
30 | end
31 | end
32 |
33 | def self.with_branch_or_pr_number(n)
34 | n = MinigitService.pr_branch(n) if n.kind_of?(Integer)
35 | where(:name => n)
36 | end
37 |
38 | def self.github_commit_uri(repo_name, sha = "$commit")
39 | "https://github.com/#{repo_name}/commit/#{sha}"
40 | end
41 |
42 | def self.github_compare_uri(repo_name, sha1 = "$commit1", sha2 ="$commit2")
43 | "https://github.com/#{repo_name}/compare/#{sha1}~...#{sha2}"
44 | end
45 |
46 | def self.github_pr_uri(repo_name, pr_number = "$pr_number")
47 | "https://github.com/#{repo_name}/pull/#{pr_number}"
48 | end
49 |
50 | def last_commit=(val)
51 | super
52 | self.last_changed_on = Time.now.utc if last_commit_changed?
53 | end
54 |
55 | def commit_uri_to(commit)
56 | commit_uri.gsub("$commit", commit)
57 | end
58 |
59 | def last_commit_uri
60 | commit_uri_to(last_commit)
61 | end
62 |
63 | def local_merge_target
64 | "origin/#{merge_target}"
65 | end
66 |
67 | def compare_uri_for(commit1, commit2)
68 | # TODO: This needs use a different URI than the commit_uri
69 | commit_uri
70 | .gsub("/commit/", "/compare/")
71 | .gsub("$commit", "#{commit1}~...#{commit2}")
72 | end
73 |
74 | def mode
75 | pull_request? ? :pr : :regular
76 | end
77 |
78 | def pr_number
79 | MinigitService.pr_number(name) if pull_request?
80 | end
81 |
82 | def fq_pr_number
83 | "#{fq_repo_name}##{pr_number}" if pull_request?
84 | end
85 |
86 | def pr_title_tags
87 | pr_title.to_s.match(/^(?:\s*\[[\w-]+\])+/).to_s.gsub("[", " [").split.map { |s| s[1...-1] }
88 | end
89 |
90 | def github_pr_uri
91 | self.class.github_pr_uri(repo.name, pr_number) if pull_request?
92 | end
93 |
94 | def write_github_comment(header, continuation_header = nil, message = nil)
95 | raise ArgumentError, "Cannot comment on non-PR branch #{name}." unless pull_request?
96 |
97 | message_builder = GithubService::MessageBuilder.new(header, continuation_header)
98 | message_builder.write(message) if message
99 |
100 | logger.info("Writing comment with header: #{header}")
101 | GithubService.add_comments(repo.name, pr_number, message_builder.comments)
102 | end
103 |
104 | def fq_repo_name
105 | repo.name
106 | end
107 |
108 | def fq_branch_name
109 | "#{fq_repo_name}@#{name}"
110 | end
111 |
112 | def git_service
113 | GitService::Branch.new(self)
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/github_service/commands/add_label.rb:
--------------------------------------------------------------------------------
1 | module GithubService
2 | module Commands
3 | class AddLabel < Base
4 | include IsTeamMember
5 |
6 | def unassignable_labels
7 | @unassignable_labels ||= Settings.labels.unassignable.to_h.stringify_keys
8 | end
9 |
10 | private
11 |
12 | def _execute(issuer:, value:)
13 | valid, invalid = extract_label_names(value)
14 | process_extracted_labels(issuer, valid, invalid)
15 |
16 | if invalid.any?
17 | issue.add_comment(invalid_label_message(issuer, invalid))
18 | end
19 |
20 | if valid.any?
21 | valid.reject! { |l| issue.applied_label?(l) }
22 | issue.add_labels(valid) if valid.any?
23 | end
24 | end
25 |
26 | def extract_label_names(value)
27 | label_names = value.split(",").map { |label| label.strip.downcase }
28 | validate_labels(label_names)
29 | end
30 |
31 | def validate_labels(label_names)
32 | # First reload the cache if there are any invalid labels
33 | GithubService.refresh_labels(issue.fq_repo_name) unless label_names.all? { |l| GithubService.valid_label?(issue.fq_repo_name, l) }
34 |
35 | # Then see if any are *still* invalid and split the list
36 | label_names.partition { |l| GithubService.valid_label?(issue.fq_repo_name, l) }
37 | end
38 |
39 | def process_extracted_labels(issuer, valid_labels, invalid_labels)
40 | correct_invalid_labels(valid_labels, invalid_labels)
41 | handle_unassignable_labels(valid_labels) unless triage_member?(issuer)
42 |
43 | [valid_labels, invalid_labels]
44 | end
45 |
46 | def correct_invalid_labels(valid_labels, invalid_labels)
47 | available_labels = GithubService.labels(issue.fq_repo_name)
48 |
49 | invalid_labels.reject! do |label|
50 | corrections = DidYouMean::SpellChecker.new(:dictionary => available_labels).correct(label)
51 |
52 | if corrections.count == 1
53 | valid_labels << corrections.first
54 | end
55 | end
56 | end
57 |
58 | def handle_unassignable_labels(valid_labels)
59 | valid_labels.map! do |label|
60 | unassignable_labels.key?(label) ? unassignable_labels[label] : label
61 | end
62 | end
63 |
64 | def invalid_label_message(issuer, invalid_labels)
65 | message = issuer == GithubService.bot_name ? "" : "@#{issuer} "
66 | message << "Cannot apply the following label"
67 | message << "s" if invalid_labels.length > 1
68 | message << " because they are not recognized:\n"
69 |
70 | labels = GithubService.labels(issue.fq_repo_name)
71 | invalid_labels.each do |bad_label|
72 | corrections = DidYouMean::SpellChecker.new(:dictionary => labels).correct(bad_label)
73 | possibilities = corrections.map { |l| "`#{l}`" }.join(", ")
74 |
75 | message << "* `#{bad_label}` "
76 | message << "(Did you mean? #{possibilities})" if corrections.any?
77 | message << "\n"
78 | end
79 | message << "\nAll labels for `#{issue.fq_repo_name}`: https://github.com/#{issue.fq_repo_name}/labels"
80 | end
81 | end
82 | end
83 | end
84 |
85 | # HACK: CI / `bundle install --path ...` compat
86 | begin
87 | retry_require_dym = false
88 | require 'did_you_mean'
89 | rescue LoadError => error
90 | if retry_require_dym
91 | raise error
92 | else
93 | $LOAD_PATH.push(*Dir[File.join(Gem.default_dir, "gems", "did_you_mean*", "lib")])
94 | $LOAD_PATH.uniq!
95 | retry_require_dym = true
96 | retry
97 | end
98 | end
99 |
--------------------------------------------------------------------------------