diff --git a/Gemfile b/Gemfile index 442d185..c62c672 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,7 @@ source 'https://rubygems.org' -ruby '3.1.2' +ruby '>= 3.1.2' # Token gem 'jwt' diff --git a/db/migrate/20250812101554_add_pull_requests_plans.rb b/db/migrate/20250812101554_add_pull_requests_plans.rb new file mode 100644 index 0000000..d54e4b3 --- /dev/null +++ b/db/migrate/20250812101554_add_pull_requests_plans.rb @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# 20250812101554_add_pull_requests_plans.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +class AddPullRequestsPlans < ActiveRecord::Migration[6.0] + def change + add_reference :plans, :pull_request, foreign_key: true + add_column :plans, :name, :string, null: false, default: '' + end +end diff --git a/db/migrate/20250822071834_add_check_suite_plan.rb b/db/migrate/20250822071834_add_check_suite_plan.rb new file mode 100644 index 0000000..1a29fbc --- /dev/null +++ b/db/migrate/20250822071834_add_check_suite_plan.rb @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# 20250822071834_add_check_suite_plan.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +class AddCheckSuitePlan < ActiveRecord::Migration[6.0] + def change + add_reference :check_suites, :plan, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index cf40164..882e693 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_04_16_153222) do +ActiveRecord::Schema[7.2].define(version: 2025_08_22_071834) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -60,8 +60,10 @@ t.bigint "github_user_id" t.bigint "stopped_in_stage_id" t.bigint "cancelled_previous_check_suite_id" + t.bigint "plan_id" t.index ["cancelled_previous_check_suite_id"], name: "index_check_suites_on_cancelled_previous_check_suite_id" t.index ["github_user_id"], name: "index_check_suites_on_github_user_id" + t.index ["plan_id"], name: "index_check_suites_on_plan_id" t.index ["pull_request_id"], name: "index_check_suites_on_pull_request_id" t.index ["stopped_in_stage_id"], name: "index_check_suites_on_stopped_in_stage_id" end @@ -130,7 +132,10 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "check_suite_id" + t.bigint "pull_request_id" + t.string "name", default: "", null: false t.index ["check_suite_id"], name: "index_plans_on_check_suite_id" + t.index ["pull_request_id"], name: "index_plans_on_pull_request_id" end create_table "pull_request_subscriptions", force: :cascade do |t| @@ -195,12 +200,14 @@ add_foreign_key "audit_retries", "github_users" add_foreign_key "check_suites", "check_suites", column: "cancelled_previous_check_suite_id" add_foreign_key "check_suites", "github_users" + add_foreign_key "check_suites", "plans" add_foreign_key "check_suites", "pull_requests" add_foreign_key "check_suites", "stages", column: "stopped_in_stage_id" add_foreign_key "ci_jobs", "check_suites" add_foreign_key "ci_jobs", "stages" add_foreign_key "github_users", "organizations" add_foreign_key "plans", "check_suites" + add_foreign_key "plans", "pull_requests" add_foreign_key "pull_request_subscriptions", "pull_requests" add_foreign_key "pull_requests", "github_users" add_foreign_key "stages", "check_suites" diff --git a/lib/bamboo_ci/api.rb b/lib/bamboo_ci/api.rb index aa89263..3cb7ec1 100644 --- a/lib/bamboo_ci/api.rb +++ b/lib/bamboo_ci/api.rb @@ -28,8 +28,8 @@ def get_status(id) get_request(URI("https://127.0.0.1/rest/api/latest/result/#{id}?expand=stages.stage.results,artifacts")) end - def submit_pr_to_ci(check_suite, ci_variables) - url = "https://127.0.0.1/rest/api/latest/queue/#{check_suite.pull_request.plan}" + def submit_pr_to_ci(check_suite, plan, ci_variables) + url = "https://127.0.0.1/rest/api/latest/queue/#{plan.bamboo_ci_plan_name}" url += custom_variables(check_suite) @@ -40,7 +40,7 @@ def submit_pr_to_ci(check_suite, ci_variables) logger(Logger::DEBUG, "Submission URL:\n #{url}") # Fetch Request - post_request(URI(url)) + post_request(URI(url.delete(' '))) end def custom_variables(check_suite) @@ -58,7 +58,7 @@ def add_comment_to_ci(key, comment) logger(Logger::DEBUG, "Comment Submission URL:\n #{url}") # Fetch Request - post_request(URI(url), body: "#{comment}") + post_request(URI(url.delete(' ')), body: "#{comment}") end def logger(severity, message) diff --git a/lib/bamboo_ci/plan_run.rb b/lib/bamboo_ci/plan_run.rb index 8c9bc9a..5bb0d30 100644 --- a/lib/bamboo_ci/plan_run.rb +++ b/lib/bamboo_ci/plan_run.rb @@ -22,7 +22,7 @@ class PlanRun attr_reader :ci_key attr_accessor :checks_run, :ci_variables - def initialize(check_suite, logger_level: Logger::INFO) + def initialize(check_suite, plan, logger_level: Logger::INFO) @logger_manager = [] @logger_level = logger_level @@ -32,14 +32,18 @@ def initialize(check_suite, logger_level: Logger::INFO) logger(Logger::INFO, "BambooCi::PlanRun - CheckSuite: #{check_suite.inspect}") @check_suite = check_suite + @plan = plan @ci_variables = [] end def start_plan - @response = submit_pr_to_ci(@check_suite, @ci_variables) + @refs = [] + @response = submit_pr_to_ci(@check_suite, @plan, @ci_variables) case @response&.code.to_i when 200, 201 + @check_suite.update(bamboo_ci_ref: JSON.parse(@response.body)['buildResultKey']) + success(@response) when 400..500 failed(@response) @@ -58,6 +62,10 @@ def bamboo_reference JSON.parse(@response.body)['buildResultKey'] end + def bamboo_references + @refs + end + private def success(response) diff --git a/lib/github/build/action.rb b/lib/github/build/action.rb index 1557ad5..28e8e80 100644 --- a/lib/github/build/action.rb +++ b/lib/github/build/action.rb @@ -26,15 +26,17 @@ class Action # Initializes the Action class with the given parameters. # # @param [CheckSuite] check_suite The CheckSuite to handle. - # @param [Github] github The Github instance to use. + # @param [Github::Check] github The Github::Check instance to use. # @param [Array] jobs The jobs to create for the CheckSuite. + # @param [String] Stage Plan name. # @param [Integer] logger_level The logging level to use (default: Logger::INFO). - def initialize(check_suite, github, jobs, logger_level: Logger::INFO) + def initialize(check_suite, github, jobs, name, logger_level: Logger::INFO) @check_suite = check_suite @github = github @jobs = jobs @loggers = [] - @stages = StageConfiguration.all + @stages_config = StageConfiguration.all + @name = name %w[github_app.log github_build_action.log].each do |filename| @loggers << GithubLogger.instance.create(filename, logger_level) @@ -49,11 +51,11 @@ def initialize(check_suite, github, jobs, logger_level: Logger::INFO) # # @param [Boolean] rerun Indicates if the jobs should be rerun (default: false). def create_summary(rerun: false) - logger(Logger::INFO, "SUMMARY #{@stages.inspect}") + logger(Logger::INFO, "SUMMARY #{@stages_config.inspect}") Github::Build::SkipOldTests.new(@check_suite).skip_old_tests - @stages.each do |stage_config| + @stages_config.each do |stage_config| create_check_run_stage(stage_config) end @@ -118,7 +120,7 @@ def create_ci_job(job) return if stage_config.nil? - stage = Stage.find_by(check_suite: @check_suite, name: stage_config.github_check_run_name) + stage = Stage.find_by(check_suite: @check_suite, name: "#{stage_config.github_check_run_name} - #{@name}") logger(Logger::INFO, "create_jobs - #{job.inspect} -> #{stage.inspect}") @@ -130,9 +132,8 @@ def create_ci_job(job) # # @param [StageConfiguration] stage_config The stage configuration. def create_check_run_stage(stage_config) - stage = Stage.find_by(name: stage_config.github_check_run_name, check_suite_id: @check_suite.id) - - logger(Logger::INFO, "STAGE #{stage_config.github_check_run_name} #{stage.inspect} - @#{@check_suite.inspect}") + logger(Logger::INFO, "create_check_run_stage - #{stage_config.github_check_run_name} - #{@name}") + stage = Stage.find_by(name: "#{stage_config.github_check_run_name} - #{@name}", check_suite_id: @check_suite.id) return create_stage(stage_config) if stage.nil? return unless stage.configuration.can_retry? @@ -148,7 +149,7 @@ def create_check_run_stage(stage_config) # @param [StageConfiguration] stage_config The stage configuration. # @return [Stage] The created stage. def create_stage(stage_config) - name = stage_config.github_check_run_name + name = "#{stage_config.github_check_run_name} - #{@name}" stage = Stage.create(check_suite: @check_suite, diff --git a/lib/github/build/plan_run.rb b/lib/github/build/plan_run.rb new file mode 100644 index 0000000..1242158 --- /dev/null +++ b/lib/github/build/plan_run.rb @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# plan_run.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +module Github + module Build + class PlanRun + TIMER = 5 # seconds + def initialize(pull_request, payload) + @pull_request = pull_request + @payload = payload + end + + def build + return [422, 'No Plans associated with this Pull Request'] if @pull_request.plans.empty? + + @pull_request.plans.each do |plan| + CreateExecutionByPlan + .delay(run_at: TIMER.seconds.from_now.utc, queue: 'create_execution_by_plan') + .create(@pull_request.id, @payload, plan) + end + + [200, 'Scheduled Plan Runs'] + end + end + end +end diff --git a/lib/github/build/skip_old_tests.rb b/lib/github/build/skip_old_tests.rb index 19efdc3..f90fa47 100644 --- a/lib/github/build/skip_old_tests.rb +++ b/lib/github/build/skip_old_tests.rb @@ -35,7 +35,6 @@ def skipping_old_test(check_run) return if check_run[:app][:name] != 'NetDEF CI Hook' or @stages.include?(check_run[:name]) @logger.info("Skipping old test suite: #{check_run[:name]}") - puts("Skipping old test suite: #{check_run[:name]}") message = 'Old test suite, skipping...' @github.skipped(check_run[:id], { title: "#{check_run[:name]} summary", summary: message }) diff --git a/lib/github/build_plan.rb b/lib/github/build_plan.rb index de4c262..d01338b 100644 --- a/lib/github/build_plan.rb +++ b/lib/github/build_plan.rb @@ -23,7 +23,6 @@ class BuildPlan def initialize(payload, logger_level: Logger::INFO) @logger = Logger.new($stdout) @logger.level = logger_level - @has_previous_exec = false @payload = payload @@ -43,28 +42,7 @@ def create @logger.info 'Fetching / Creating a pull request' fetch_pull_request - # Fetch last Check Suite - fetch_last_check_suite - - # Create a Check Suite - create_check_suite - - # Check if could save the Check Suite at database - unless @check_suite.persisted? - @logger.error "Failed to save CheckSuite: #{@check_suite.errors.inspect}" - return [422, 'Failed to save Check Suite'] - end - - # Stop a previous execution - Avoiding CI spam - stop_previous_execution - - # Starting a new CI run - status = start_new_execution - - return [status, 'Failed to create CI Plan'] if status != 200 - - # Creating CiJobs at database - ci_jobs + Github::Build::PlanRun.new(@pull_request, @payload).build end private @@ -74,9 +52,9 @@ def fetch_pull_request return create_pull_request if @pull_request.nil? - @logger.info "Updating plan: #{fetch_plan}" + @pull_request.update(branch_name: @payload.dig('pull_request', 'head', 'ref')) - @pull_request.update(plan: fetch_plan, branch_name: @payload.dig('pull_request', 'head', 'ref')) + add_plans(@pull_request) end def github_pr @@ -89,110 +67,18 @@ def create_pull_request author: @payload.dig('pull_request', 'user', 'login'), github_pr_id: github_pr, branch_name: @payload.dig('pull_request', 'head', 'ref'), - repository: @payload.dig('repository', 'full_name'), - plan: fetch_plan + repository: @payload.dig('repository', 'full_name') ) - Github::UserInfo.new(@payload.dig('pull_request', 'user', 'id'), pull_request: @pull_request) - end - - def start_new_execution - create_pull_request if @pull_request.nil? - - @check_suite.pull_request = @pull_request - - Github::UserInfo.new(@payload.dig('pull_request', 'user', 'id'), check_suite: @check_suite) + add_plans(@pull_request) - @bamboo_plan_run = BambooCi::PlanRun.new(@check_suite, logger_level: @logger.level) - @bamboo_plan_run.ci_variables = ci_vars - @bamboo_plan_run.start_plan - end - - def stop_previous_execution - return if @last_check_suite.nil? or @last_check_suite.finished? - - @logger.info 'Stopping previous execution' - @logger.info @last_check_suite.inspect - @logger.info @check_suite.inspect - - cancel_previous_ci_jobs - end - - def cancel_previous_ci_jobs - @last_check_suite.ci_jobs.where(status: %w[queued in_progress]).each do |ci_job| - @logger.warn("Cancelling Job #{ci_job.inspect}") - ci_job.cancelled(@github_check) - end - - @last_check_suite.update(stopped_in_stage: @last_check_suite.stages.where(status: :in_progress).last) - - @last_check_suite.stages.where(status: %w[queued in_progress]).each do |stage| - stage.cancelled(@github_check) - end - - @has_previous_exec = true - BambooCi::StopPlan.build(@last_check_suite.bamboo_ci_ref) - end - - def create_check_suite - @logger.info 'Creating a check suite' - @check_suite = - CheckSuite.create( - pull_request: @pull_request, - author: @payload.dig('pull_request', 'user', 'login'), - commit_sha_ref: @payload.dig('pull_request', 'head', 'sha'), - work_branch: @payload.dig('pull_request', 'head', 'ref'), - base_sha_ref: @payload.dig('pull_request', 'base', 'sha'), - merge_branch: @payload.dig('pull_request', 'base', 'ref') - ) - - @logger.info 'Creating GitHub Check API' - @github_check = Github::Check.new(@check_suite) - end - - def fetch_last_check_suite - @last_check_suite = @pull_request.check_suites.last - end - - def ci_jobs - @logger.info 'Creating GitHub Check' - - SlackBot.instance.execution_started_notification(@check_suite) - - @check_suite.update(bamboo_ci_ref: @bamboo_plan_run.bamboo_reference) - - jobs = BambooCi::RunningPlan.fetch(@bamboo_plan_run.bamboo_reference) - - return [422, 'Failed to fetch RunningPlan'] if jobs.nil? or jobs.empty? - - action = Github::Build::Action.new(@check_suite, @github_check, jobs) - action.create_summary - - @logger.info ">>> @has_previous_exec: #{@has_previous_exec}" - stop_execution_message if @has_previous_exec - - [200, 'Pull Request created'] - end - - def stop_execution_message - @check_suite.update(cancelled_previous_check_suite_id: @last_check_suite.id) - BambooCi::StopPlan.comment(@last_check_suite, @check_suite) - end - - def ci_vars - ci_vars = [] - ci_vars << { value: @github_check.signature, name: 'signature_secret' } - - ci_vars + Github::UserInfo.new(@payload.dig('pull_request', 'user', 'id'), pull_request: @pull_request) end - def fetch_plan - plan = Plan.find_by(github_repo_name: @payload.dig('repository', 'full_name')) - - return plan.bamboo_ci_plan_name unless plan.nil? - - # Default plan - 'TESTING-FRRCRAS' + def add_plans(pull_request) + plans = Plan.where(github_repo_name: @payload.dig('repository', 'full_name')) + pull_request.plans = plans + pull_request.save end end end diff --git a/lib/github/plan_execution/finished.rb b/lib/github/plan_execution/finished.rb index 733bfb8..7fbebf4 100644 --- a/lib/github/plan_execution/finished.rb +++ b/lib/github/plan_execution/finished.rb @@ -234,12 +234,16 @@ def check_stages @logger.info ">>> @result: #{@result.inspect}" return if @result.nil? or @result.empty? or @result['status-code']&.between?(400, 500) - @result.dig('stages', 'stage').each do |stage| - stage.dig('results', 'result').each do |result| - ci_job = CiJob.find_by(job_ref: result['buildResultKey'], check_suite_id: @check_suite.id) + @result.dig('stages', 'stage')&.each do |stage| + check_stage(stage, github_check) + end + end + + def check_stage(stage, github_check) + stage.dig('results', 'result')&.each do |result| + ci_job = CiJob.find_by(job_ref: result['buildResultKey'], check_suite_id: @check_suite.id) - update_stage_status(ci_job, result, github_check) - end + update_stage_status(ci_job, result, github_check) end end diff --git a/lib/github/re_run/base.rb b/lib/github/re_run/base.rb index 5339a58..d4d7608 100644 --- a/lib/github/re_run/base.rb +++ b/lib/github/re_run/base.rb @@ -32,23 +32,24 @@ def initialize(payload, logger_level: Logger::INFO) private - def fetch_run_ci_by_pr + def fetch_run_ci_by_pr(plan) CheckSuite - .joins(:pull_request) + .joins(pull_request: :plans) .joins(:ci_jobs) - .where(pull_request: { github_pr_id: pr_id, repository: repo }, ci_jobs: { status: 1 }) + .where(pull_request: { plans: { id: plan.id }, + github_pr_id: pr_id, repository: repo }, + ci_jobs: { status: 1 }) .uniq end - def stop_previous_execution - return if fetch_run_ci_by_pr.empty? + def stop_previous_execution(plan) + return if fetch_run_ci_by_pr(plan).empty? logger(Logger::INFO, 'Stopping previous execution') - logger(Logger::INFO, fetch_run_ci_by_pr.inspect) @last_check_suite = nil - fetch_run_ci_by_pr.each do |check_suite| + fetch_run_ci_by_pr(plan).each do |check_suite| stop_and_update_previous_execution(check_suite) end end @@ -74,30 +75,23 @@ def cancel_previous_jobs(check_suite) end end - def create_ci_jobs(bamboo_plan, check_suite) - jobs = BambooCi::RunningPlan.fetch(bamboo_plan.bamboo_reference) + def create_ci_jobs(check_suite, plan_name) + jobs = BambooCi::RunningPlan.fetch(check_suite.bamboo_ci_ref) - action = Github::Build::Action.new(check_suite, @github_check, jobs) + action = Github::Build::Action.new(check_suite, @github_check, jobs, plan_name) action.create_summary(rerun: true) end - def fetch_plan - plan = Plan.find_by_github_repo_name(@payload.dig('repository', 'full_name')) - - return plan.bamboo_ci_plan_name unless plan.nil? - - # Default plan - 'TESTING-FRRCRAS' - end - def logger(severity, message) @logger_manager.each do |logger_object| logger_object.add(severity, message) end end - def start_new_execution(check_suite) - bamboo_plan_run = BambooCi::PlanRun.new(check_suite, logger_level: @logger_level) + def start_new_execution(check_suite, plan) + cleanup(check_suite) + + bamboo_plan_run = BambooCi::PlanRun.new(check_suite, plan, logger_level: @logger_level) bamboo_plan_run.ci_variables = ci_vars bamboo_plan_run.start_plan @@ -109,8 +103,6 @@ def start_new_execution(check_suite) retry_type: 'full') Github::UserInfo.new(@payload.dig('sender', 'id'), check_suite: check_suite, audit_retry: audit_retry) - - bamboo_plan_run end def ci_vars @@ -120,20 +112,28 @@ def ci_vars ci_vars end - def ci_jobs(check_suite, bamboo_plan) + def ci_jobs(check_suite, plan) SlackBot.instance.execution_started_notification(check_suite) - check_suite.update(bamboo_ci_ref: bamboo_plan.bamboo_reference, re_run: true) - check_suite.update(cancelled_previous_check_suite: @last_check_suite) - create_ci_jobs(bamboo_plan, check_suite) + create_ci_jobs(check_suite, plan.name) + + update_unavailable_jobs(check_suite) + end + def update_unavailable_jobs(check_suite) CheckSuite.where(commit_sha_ref: check_suite.commit_sha_ref).each do |cs| Github::Build::UnavailableJobs.new(cs).update(new_check_suite: check_suite) end end + def cleanup(check_suite) + check_suite.pull_request.check_suites.each do |suite| + Delayed::Job.where('handler LIKE ?', "%method_name: :timeout\nargs:\n- #{suite.id}%") + end + end + def action @payload.dig('comment', 'body') end diff --git a/lib/github/re_run/command.rb b/lib/github/re_run/command.rb index 7487bc8..6cdc2ab 100644 --- a/lib/github/re_run/command.rb +++ b/lib/github/re_run/command.rb @@ -13,6 +13,8 @@ module Github module ReRun class Command < Base + TIMER = 1 # seconds + def initialize(payload, logger_level: Logger::INFO) super(payload, logger_level: logger_level) @@ -30,28 +32,19 @@ def start @github_check = Github::Check.new(check_suite) - stop_previous_execution - - check_suite = create_check_suite(check_suite) - - bamboo_plan = start_new_execution(check_suite) - ci_jobs(check_suite, bamboo_plan) + suite_by_plan(check_suite) - [201, 'Starting re-run (command)'] + [200, 'Scheduled Plan Runs'] end private - def create_check_suite(check_suite) - CheckSuite.create( - pull_request: check_suite.pull_request, - author: check_suite.author, - commit_sha_ref: check_suite.commit_sha_ref, - work_branch: check_suite.work_branch, - base_sha_ref: check_suite.base_sha_ref, - merge_branch: check_suite.merge_branch, - re_run: true - ) + def suite_by_plan(check_suite) + check_suite.pull_request.plans.each do |plan| + CreateExecutionByCommand + .delay(run_at: TIMER.seconds.from_now.utc, queue: 'create_execution_by_command') + .create(plan.id, check_suite.id, @payload) + end end def fetch_check_suite diff --git a/lib/github/re_run/comment.rb b/lib/github/re_run/comment.rb index daeba8a..670cd64 100644 --- a/lib/github/re_run/comment.rb +++ b/lib/github/re_run/comment.rb @@ -15,166 +15,51 @@ module Github module ReRun class Comment < Base + TIMER = 1 # seconds + def initialize(payload, logger_level: Logger::INFO) super(payload, logger_level: logger_level) @logger_manager << GithubLogger.instance.create('github_rerun_comment.log', logger_level) + @logger_manager << Logger.new($stdout) end def start return [422, 'Payload can not be blank'] if @payload.nil? or @payload.empty? return [404, 'Action not found'] unless action? - logger(Logger::DEBUG, ">>> Github::ReRun::Comment - sha256: #{sha256.inspect}, payload: #{@payload.inspect}") - - check_suite = sha256_or_comment? - - logger(Logger::DEBUG, ">>> Check suite: #{check_suite.inspect}") - - return [404, 'Failed to create a check suite'] if check_suite.nil? - - github_reaction_feedback(comment_id) - - stop_previous_execution - - bamboo_plan = start_new_execution(check_suite) + fetch_pull_request - ci_jobs(check_suite, bamboo_plan) - - [201, 'Starting re-run (comment)'] + confirm_and_start end private - def sha256_or_comment? - fetch_old_check_suite - - @old_check_suite.nil? ? comment_flow : sha256_flow - end - - def comment_flow - commit = fetch_last_commit_or_sha256 - github_check = fetch_github_check - pull_request_info = github_check.pull_request_info(pr_id, repo) - pull_request = fetch_or_create_pr(pull_request_info) - - fetch_old_check_suite(commit[:sha]) - check_suite = create_check_suite_by_commit(commit, pull_request, pull_request_info) - logger(Logger::INFO, "CheckSuite errors: #{check_suite.inspect}") - return nil unless check_suite.persisted? - - @github_check = Github::Check.new(check_suite) - - check_suite - end - - # Fetches the GitHub check associated with the pull request. - # - # This method finds the pull request by its GitHub PR ID and then retrieves - # the last check suite associated with that pull request. It then initializes - # a new `Github::Check` object with the last check suite. - # - # @return [Github::Check] the GitHub check associated with the pull request. - # - # @raise [ActiveRecord::RecordNotFound] if the pull request is not found. - def fetch_github_check - pull_request = PullRequest.find_by(github_pr_id: pr_id) - Github::Check.new(pull_request.check_suites.last) - end - - def create_check_suite_by_commit(commit, pull_request, pull_request_info) - CheckSuite.create( - pull_request: pull_request, - author: @payload.dig('comment', 'user', 'login'), - commit_sha_ref: commit[:sha], - work_branch: pull_request_info.dig(:head, :ref), - base_sha_ref: pull_request_info.dig(:base, :sha), - merge_branch: pull_request_info.dig(:base, :ref), - re_run: true - ) - end + def confirm_and_start + return [404, 'Pull Request not found'] if @pull_request.nil? + return [404, 'Can not rerun a new PullRequest'] if @pull_request.check_suites.empty? - def fetch_or_create_pr(pull_request_info) - last_check_suite = CheckSuite - .joins(:pull_request) - .where(pull_request: { github_pr_id: pr_id, repository: repo }) - .last - - return last_check_suite.pull_request unless last_check_suite.nil? - - pull_request = create_pull_request(pull_request_info) - - logger(Logger::DEBUG, ">>> Created a new pull request: #{pull_request}") - logger(Logger::ERROR, "Error: #{pull_request.errors.inspect}") unless pull_request.persisted? - - pull_request - end - - def create_pull_request(pull_request_info) - PullRequest.create( - author: @payload.dig('issue', 'user', 'login'), - github_pr_id: pr_id, - branch_name: pull_request_info.dig(:head, :ref), - repository: repo, - plan: fetch_plan - ) - end - - def sha256_flow - @github_check = Github::Check.new(@old_check_suite) - create_new_check_suite - end - - # The behaviour will be the following: It will fetch the last commit if it has - # received a comment and only fetch a commit if the command starts with ci:rerrun #. - # If there is any other character before the # it will be considered a comment. - def fetch_last_commit_or_sha256 - pull_request_commit = Github::Parsers::PullRequestCommit.new(repo, pr_id) - commit = pull_request_commit.find_by_sha(sha256) - - return commit if commit and action.match(/ci:rerun\s+#/i) + github_reaction_feedback(comment_id) - fetch_last_commit - end + @pull_request.plans.each do |plan| + CreateExecutionByComment + .delay(run_at: TIMER.seconds.from_now.utc, queue: 'create_execution_by_comment') + .create(@pull_request.id, @payload, plan) + end - def fetch_last_commit - Github::Parsers::PullRequestCommit.new(repo, pr_id).last_commit_in_pr + [200, 'Scheduled Plan Runs'] end def github_reaction_feedback(comment_id) return if comment_id.nil? - @github_check.comment_reaction_thumb_up(repo, comment_id) - end - - def fetch_old_check_suite(sha = sha256) - return if sha.nil? + github_check = Github::Check.new(@pull_request.check_suites.last) - logger(Logger::DEBUG, ">>> fetch_old_check_suite SHA: #{sha}") - - @old_check_suite = - CheckSuite - .joins(:pull_request) - .where('commit_sha_ref ILIKE ? AND pull_requests.repository = ?', "#{sha}%", repo) - .last - end - - def create_new_check_suite - CheckSuite.create( - pull_request: @old_check_suite.pull_request, - author: @old_check_suite.author, - commit_sha_ref: @old_check_suite.commit_sha_ref, - work_branch: @old_check_suite.work_branch, - base_sha_ref: @old_check_suite.base_sha_ref, - merge_branch: @old_check_suite.merge_branch, - re_run: true - ) + github_check.comment_reaction_thumb_up(repo, comment_id) end - def sha256 - return nil unless action.downcase.match? 'ci:rerun #' - - action.downcase.split('#').last + def fetch_pull_request + @pull_request = PullRequest.find_by(github_pr_id: pr_id) end def action? diff --git a/lib/github/update_status.rb b/lib/github/update_status.rb index 67a0803..1799677 100644 --- a/lib/github/update_status.rb +++ b/lib/github/update_status.rb @@ -50,6 +50,7 @@ def initialize(payload) # # @return [Array] An array containing the status code and message. def update + logger(Logger::INFO, "Updating status for job: #{@reference} with status: #{@status}") return job_not_found if @job.nil? return [304, 'Not Modified'] if @job.queued? and @status != 'in_progress' and @job.name != 'Checkout Code' return [304, 'Not Modified'] if @job.in_progress? and !%w[success failure].include? @status @@ -117,6 +118,8 @@ def update_status def insert_new_delayed_job queue = @job.check_suite.pull_request.github_pr_id % 10 + logger(Logger::INFO, "Inserting new delayed job for queue: #{queue} to update job #{@job.id}") + delete_and_create_delayed_job(queue) end @@ -127,9 +130,13 @@ def insert_new_delayed_job def delete_and_create_delayed_job(queue) fetch_delayed_job(queue).destroy_all + logger(Logger::INFO, + "Inserting new delayed job for queue: #{queue} to update job #{@job.id} " \ + "and bamboo_ci_ref: #{@check_suite.bamboo_ci_ref}") + CiJobStatus .delay(run_at: DELAYED_JOB_TIMER.seconds.from_now.utc, queue: queue) - .update(@job.check_suite.id, @job.id) + .update(@check_suite.bamboo_ci_ref, @job.id) end ## @@ -138,9 +145,12 @@ def delete_and_create_delayed_job(queue) # @param [Integer] queue The queue number for the delayed job. # @return [ActiveRecord::Relation] The relation containing the delayed jobs. def fetch_delayed_job(queue) + logger(Logger::INFO, + "Removing old delayed job for queue: #{queue} and bamboo_ci_ref: #{@check_suite.bamboo_ci_ref}") + Delayed::Job .where(queue: queue) - .where('handler LIKE ?', "%method_name: :update\nargs:\n- #{@check_suite.id}%") + .where('handler LIKE ?', "%method_name: :update\nargs:\n- #{@check_suite.bamboo_ci_ref}%") end ## @@ -179,6 +189,7 @@ def logger_initializer else GithubLogger.instance.create("pr#{@job.check_suite.pull_request.github_pr_id}.log", Logger::INFO) end + @loggers << Logger.new($stdout) end end end diff --git a/lib/github_ci_app.rb b/lib/github_ci_app.rb index e6479d2..e328326 100644 --- a/lib/github_ci_app.rb +++ b/lib/github_ci_app.rb @@ -30,6 +30,7 @@ require_relative 'github/user_info' require_relative 'github/build/skip_old_tests' require_relative 'github/topotest_failures/retrieve_error' +require_relative 'github/build/plan_run' # Helpers libs require_relative 'helpers/configuration' @@ -43,6 +44,9 @@ require_relative '../workers/timeout_execution' require_relative '../workers/ci_job_fetch_topotest_failures' require_relative '../workers/slack_username2_id' +require_relative '../workers/create_execution_by_plan' +require_relative '../workers/create_execution_by_comment' +require_relative '../workers/create_execution_by_command' # Slack libs require_relative 'slack/slack' diff --git a/lib/helpers/request.rb b/lib/helpers/request.rb index 55bb1e1..5fbba62 100644 --- a/lib/helpers/request.rb +++ b/lib/helpers/request.rb @@ -56,7 +56,7 @@ def delete_request(uri, machine: 'ci1.netdef.org') # Fetch Request resp = http.request(req) - logger(Logger::DEBUG, resp) + logger(Logger::INFO, resp) resp end diff --git a/lib/models/check_suite.rb b/lib/models/check_suite.rb index 667bc56..d7593f6 100644 --- a/lib/models/check_suite.rb +++ b/lib/models/check_suite.rb @@ -15,6 +15,7 @@ class CheckSuite < ActiveRecord::Base validates :commit_sha_ref, presence: true belongs_to :pull_request + belongs_to :plan belongs_to :stopped_in_stage, class_name: 'Stage', optional: true belongs_to :cancelled_previous_check_suite, class_name: 'CheckSuite', optional: true diff --git a/lib/models/plan.rb b/lib/models/plan.rb index 5a198bb..588610b 100644 --- a/lib/models/plan.rb +++ b/lib/models/plan.rb @@ -11,4 +11,7 @@ require 'otr-activerecord' class Plan < ActiveRecord::Base + has_many :check_suites + + belongs_to :pull_request end diff --git a/lib/models/pull_request.rb b/lib/models/pull_request.rb index 408ed51..48edcf1 100644 --- a/lib/models/pull_request.rb +++ b/lib/models/pull_request.rb @@ -18,20 +18,19 @@ class PullRequest < ActiveRecord::Base has_many :check_suites, dependent: :delete_all has_many :pull_request_subscriptions, dependent: :delete_all - + has_many :plans def finished? return true if check_suites.nil? or check_suites.empty? - current_execution.finished? + current_execution_by_plan(plan).finished? end def current_execution?(check_suite) - current_execution == check_suite + current_execution_by_plan(check_suite.plan) == check_suite end - # @return [CheckSuite] - def current_execution - check_suites.order(id: :asc).last + def current_execution_by_plan(plan_obj) + check_suites.where(plan: plan_obj).order(id: :asc).last end def self.unique_repository_names diff --git a/lib/models/stage.rb b/lib/models/stage.rb index 8384e73..6d31889 100644 --- a/lib/models/stage.rb +++ b/lib/models/stage.rb @@ -18,6 +18,17 @@ class Stage < ActiveRecord::Base default_scope -> { order(id: :asc) }, all_queries: true + scope :related_stages, lambda { |check_suite, suffix| + where('stages.name LIKE ?', "%#{suffix}").where(check_suite: check_suite) + } + + scope :next_stage, ->(current_position) { where(configuration: { position: current_position + 1 }) } + scope :next_stages, ->(current_position) { where(configuration: { position: [(current_position + 1)..] }) } + + def suffix + name.split(' - ').last + end + def update_execution_time started = audit_statuses.find_by(status: :in_progress) finished = audit_statuses.find_by(status: %i[success failure]) @@ -33,7 +44,14 @@ def running? def previous_stage position = configuration&.position.to_i - check_suite.stages.joins(:configuration).find_by(configuration: { position: position - 1 }) + + return nil unless suffix + + check_suite.stages + .joins(:configuration) + .where(configuration: { position: position - 1 }) + .where('stages.name LIKE ?', "%#{suffix}") + .first end def finished? @@ -117,13 +135,14 @@ def github_stage_full_name(name) end def output_in_progress - url = GitHubApp::Configuration.instance.ci_url in_progress = jobs.where(status: :in_progress) header = ":arrow_right: Jobs in progress: #{in_progress.size}/#{jobs.size}\n\n" - in_progress_jobs = mount_in_progress_jobs(jobs) + in_progress_jobs = in_progress.map do |job| + "- **#{job.name}** -> https://#{GitHubApp::Configuration.instance.ci_url}/browse/#{job.job_ref}\n" + end.join("\n") - url = "https://#{url}/browse/#{check_suite.bamboo_ci_ref}" + url = "https://#{GitHubApp::Configuration.instance.ci_url}/browse/#{check_suite.bamboo_ci_ref}" { title: "#{name} summary", summary: "#{header}#{in_progress_jobs}\nDetails at [#{url}](#{url})" } end diff --git a/spec/factories/check_suite.rb b/spec/factories/check_suite.rb index b368e2b..c52eff5 100644 --- a/spec/factories/check_suite.rb +++ b/spec/factories/check_suite.rb @@ -45,5 +45,11 @@ create(:stage, check_suite: check_suite) end end + + trait :with_stages_and_jobs do + after(:create) do |check_suite| + create(:stage, :with_job, check_suite: check_suite) + end + end end end diff --git a/spec/factories/plan.rb b/spec/factories/plan.rb index b892989..b12b687 100644 --- a/spec/factories/plan.rb +++ b/spec/factories/plan.rb @@ -10,6 +10,7 @@ FactoryBot.define do factory :plan do + name { Faker::App.name } bamboo_ci_plan_name { Faker::App.name } github_repo_name { Faker::App.name } end diff --git a/spec/factories/pull_request.rb b/spec/factories/pull_request.rb index 71e8777..98419a1 100644 --- a/spec/factories/pull_request.rb +++ b/spec/factories/pull_request.rb @@ -14,7 +14,7 @@ github_pr_id { 1 } branch_name { Faker::App.name } repository { 'Unit/Test' } - plan { Faker::Alphanumeric.alpha(number: 10) } + plans { [create(:plan)] } trait :with_check_suite do after(:create) do |pr| diff --git a/spec/factories/stage.rb b/spec/factories/stage.rb index ad46085..6b0fbaf 100644 --- a/spec/factories/stage.rb +++ b/spec/factories/stage.rb @@ -14,7 +14,7 @@ status { 0 } check_ref { Faker::Alphanumeric.alphanumeric(number: 18, min_alpha: 3, min_numeric: 3) } - configuration { create(:stage_configuration, github_check_run_name: name) } + configuration { create(:stage_configuration, github_check_run_name: name.split(' - ').first) } trait :failure do status { :failure } diff --git a/spec/lib/bamboo_ci/api_spec.rb b/spec/lib/bamboo_ci/api_spec.rb index 11e4f28..186cabc 100644 --- a/spec/lib/bamboo_ci/api_spec.rb +++ b/spec/lib/bamboo_ci/api_spec.rb @@ -74,9 +74,10 @@ def initialize let(:id) { 1 } let(:status) { 200 } let(:check_suite) { create(:check_suite) } + let(:plan) { check_suite.pull_request.plans.last } let(:url) do - "https://127.0.0.1/rest/api/latest/queue/#{check_suite.pull_request.plan}" \ + "https://127.0.0.1/rest/api/latest/queue/#{plan.bamboo_ci_plan_name}" \ "#{custom_variables}#{ci_variables_parsed}" end @@ -104,7 +105,7 @@ def initialize end it 'must returns success' do - expect(dummy.submit_pr_to_ci(check_suite, ci_variables).code.to_i).to eq(status) + expect(dummy.submit_pr_to_ci(check_suite, plan, ci_variables).code.to_i).to eq(status) end end diff --git a/spec/lib/bamboo_ci/plan_run_spec.rb b/spec/lib/bamboo_ci/plan_run_spec.rb index 5077844..afbdfb9 100644 --- a/spec/lib/bamboo_ci/plan_run_spec.rb +++ b/spec/lib/bamboo_ci/plan_run_spec.rb @@ -9,12 +9,13 @@ # frozen_string_literal: true describe BambooCi::PlanRun do - let(:plan_run) { described_class.new(check_suite) } + let(:plan) { check_suite.pull_request.plans.last } + let(:plan_run) { described_class.new(check_suite, plan) } before do allow(Netrc).to receive(:read).and_return({ 'ci1.netdef.org' => %w[user password] }) - stub_request(:post, "https://127.0.0.1/rest/api/latest/queue/#{check_suite.pull_request.plan}?" \ + stub_request(:post, "https://127.0.0.1/rest/api/latest/queue/#{plan.bamboo_ci_plan_name.delete(' ')}?" \ "bamboo.variable.github_base_sha=#{check_suite.base_sha_ref}" \ "&bamboo.variable.github_branch=#{check_suite.merge_branch}&" \ "bamboo.variable.github_merge_sha=#{check_suite.commit_sha_ref}&" \ diff --git a/spec/lib/github/build/action_spec.rb b/spec/lib/github/build/action_spec.rb index d4b412c..02dfc54 100644 --- a/spec/lib/github/build/action_spec.rb +++ b/spec/lib/github/build/action_spec.rb @@ -9,11 +9,11 @@ # frozen_string_literal: true describe Github::Build::Action do - let(:action) { described_class.new(check_suite, fake_github_check, jobs) } + let(:action) { described_class.new(check_suite, fake_github_check, jobs, 'Tato') } let(:fake_client) { Octokit::Client.new } let(:fake_github_check) { Github::Check.new(nil) } let(:check_suite) { create(:check_suite) } - let(:stage) { create(:stage, check_suite: check_suite) } + let(:stage) { create(:stage, name: 'Build - Tato', check_suite: check_suite) } let(:jobs) do [ { @@ -163,7 +163,8 @@ before do stage.configuration.update(start_in_progress: true) - described_class.new(check_suite_new, fake_github_check, jobs).create_summary(rerun: false) + described_class.new(check_suite_new, fake_github_check, jobs, + check_suite_new.pull_request.plans.last.name).create_summary(rerun: false) end it 'must not change' do @@ -177,11 +178,12 @@ before do stage.configuration.update(start_in_progress: true) - described_class.new(check_suite_new, fake_github_check, [ci_job]).create_summary(rerun: false) + described_class.new(check_suite_new, fake_github_check, [ci_job], + check_suite_new.pull_request.plans.last.name).create_summary(rerun: false) end it 'must not change' do - expect { described_class.new(check_suite_new, fake_github_check, [ci_job]).create_summary(rerun: false) } + expect { described_class.new(check_suite_new, fake_github_check, [ci_job], '').create_summary(rerun: false) } .not_to raise_error end end diff --git a/spec/lib/github/build/plan_run_spec.rb b/spec/lib/github/build/plan_run_spec.rb new file mode 100644 index 0000000..b6df448 --- /dev/null +++ b/spec/lib/github/build/plan_run_spec.rb @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# plan_run_spec.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +describe Github::Build::PlanRun do + let(:plan_run) { described_class.new(pull_request, payload) } + + context 'when receives an invalid pull request' do + let(:pull_request) { create(:pull_request, plans: []) } + let(:payload) { {} } + + it 'must return 422' do + expect(plan_run.build).to eq([422, 'No Plans associated with this Pull Request']) + end + end +end diff --git a/spec/lib/github/build/summary_spec.rb b/spec/lib/github/build/summary_spec.rb index 333e641..14c38c7 100644 --- a/spec/lib/github/build/summary_spec.rb +++ b/spec/lib/github/build/summary_spec.rb @@ -126,8 +126,12 @@ context 'when the tests stage finished unsuccessfully' do let(:first_stage_config) { create(:stage_configuration, position: 1) } let(:second_stage_config) { create(:stage_configuration, position: 2) } - let(:first_stage) { create(:stage, configuration: first_stage_config, check_suite: check_suite) } - let(:second_stage) { create(:stage, configuration: second_stage_config, check_suite: check_suite) } + let(:first_stage) do + create(:stage, name: 'Coding - Ajax', configuration: first_stage_config, check_suite: check_suite) + end + let(:second_stage) do + create(:stage, name: 'Tests - Ajax', configuration: second_stage_config, check_suite: check_suite) + end let(:ci_job2) { create(:ci_job, :success, check_suite: check_suite, stage: first_stage) } let(:ci_job) { create(:ci_job, :failure, check_suite: check_suite, stage: second_stage) } @@ -146,8 +150,12 @@ context 'when the tests stage finished unsuccessfully and build_message returns null' do let(:first_stage_config) { create(:stage_configuration, position: 1) } let(:second_stage_config) { create(:stage_configuration, position: 2) } - let(:first_stage) { create(:stage, configuration: first_stage_config, check_suite: check_suite) } - let(:second_stage) { create(:stage, name: 'Build', configuration: second_stage_config, check_suite: check_suite) } + let(:first_stage) do + create(:stage, name: 'Code - Tato', configuration: first_stage_config, check_suite: check_suite) + end + let(:second_stage) do + create(:stage, name: 'Build - Tato', configuration: second_stage_config, check_suite: check_suite) + end let(:ci_job2) { create(:ci_job, :success, check_suite: check_suite, stage: first_stage) } let(:ci_job) { create(:ci_job, :failure, name: 'Ubuntu Build', check_suite: check_suite, stage: second_stage) } @@ -159,6 +167,7 @@ end it 'must update stage' do + puts ci_job.inspect summary.build_summary expect(ci_job.stage.reload.status).to eq('failure') expect(ci_job2.stage.reload.status).to eq('success') @@ -168,8 +177,12 @@ context 'when the tests stage finished unsuccessfully and build_message returns errorlog' do let(:first_stage_config) { create(:stage_configuration, position: 1) } let(:second_stage_config) { create(:stage_configuration, position: 2) } - let(:first_stage) { create(:stage, configuration: first_stage_config, check_suite: check_suite) } - let(:second_stage) { create(:stage, name: 'Build', configuration: second_stage_config, check_suite: check_suite) } + let(:first_stage) do + create(:stage, name: 'Coding - Ajax', configuration: first_stage_config, check_suite: check_suite) + end + let(:second_stage) do + create(:stage, name: 'Build - Ajax', configuration: second_stage_config, check_suite: check_suite) + end let(:ci_job2) { create(:ci_job, :success, check_suite: check_suite, stage: first_stage) } let(:ci_job) { create(:ci_job, :failure, name: 'Ubuntu Build', check_suite: check_suite, stage: second_stage) } diff --git a/spec/lib/github/build_plan_spec.rb b/spec/lib/github/build_plan_spec.rb index d2425a1..de9b2b6 100644 --- a/spec/lib/github/build_plan_spec.rb +++ b/spec/lib/github/build_plan_spec.rb @@ -12,16 +12,20 @@ let(:build_plan) { described_class.new(payload) } let(:fake_client) { Octokit::Client.new } let(:fake_github_check) { Github::Check.new(nil) } - let(:fake_plan_run) { BambooCi::PlanRun.new(nil) } + let(:fake_plan_run) { BambooCi::PlanRun.new(nil, pull_request.plans.last) } let(:fake_check_run) { create(:check_suite) } before do allow(File).to receive(:read).and_return('') allow(OpenSSL::PKey::RSA).to receive(:new).and_return(OpenSSL::PKey::RSA.new(2048)) allow(TimeoutExecution).to receive_message_chain(:delay, :timeout).and_return(true) + allow(GitHubApp::Configuration).to receive(:new).and_return(GitHubApp::Configuration.instance) end describe 'Valid commands' do + let!(:plan) { create(:plan, github_repo_name: repo) } + + let(:pull_request) { create(:pull_request, github_pr_id: pr_number, repository: repo, author: author) } let(:pr_number) { rand(1_000_000) } let(:repo) { 'UnitTest/repo' } let(:fake_translation) { create(:stage_configuration) } @@ -68,7 +72,7 @@ allow(fake_github_check).to receive(:fetch_username).and_return({}) allow(fake_github_check).to receive(:check_runs_for_ref).and_return({}) - allow(BambooCi::RunningPlan).to receive(:fetch).with(fake_plan_run.bamboo_reference).and_return(ci_jobs) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return(ci_jobs) end context 'when action is opened' do @@ -82,68 +86,12 @@ end it 'must create a PR' do - expect(build_plan.create).to eq([200, 'Pull Request created']) - end - end - - context 'when action is opened and check created_objects' do - let(:action) { 'opened' } - let(:author) { 'Johnny Silverhand' } - let(:pull_request) { PullRequest.last } - let(:check_suite) { pull_request.check_suites.last } - let(:ci_job) { check_suite.ci_jobs.find_by(name: 'First Test') } - let(:ci_jobs) do - [{ name: 'First Test', job_ref: 'UNIT-TEST-FIRST-1', stage: fake_translation.bamboo_stage_name }] - end - let(:plan) { create(:plan, github_repo_name: repo) } - - before do - plan - build_plan.create - end - - it 'must create all objects' do - expect(pull_request.author).to eq(author) - expect(pull_request.github_pr_id.to_i).to eq(pr_number) - expect(pull_request.check_suites.to_a).not_to eq([]) - expect(check_suite.author).to eq(author) - expect(ci_job.name).to eq('First Test') - expect(ci_job.job_ref).to eq('UNIT-TEST-FIRST-1') - end - end - - context 'when commit and has a previous CI jobs' do - let(:action) { 'opened' } - let(:pull_request) { create(:pull_request, github_pr_id: pr_number, repository: repo) } - let(:previous_check_suite) { create(:check_suite, :with_running_ci_jobs, pull_request: pull_request) } - let(:previous_ci_job) { previous_check_suite.reload.ci_jobs.last } - let(:check_suite) { pull_request.reload.check_suites.last } - let(:author) { 'Johnny Silverhand' } - let(:ci_jobs) do - [{ name: 'First Test', job_ref: 'UNIT-TEST-FIRST-1', stage: fake_translation.bamboo_stage_name }] - end - let(:new_pull_request) { PullRequest.last } - - before do - previous_check_suite - - allow(BambooCi::StopPlan).to receive(:build) - allow(BambooCi::StopPlan).to receive(:comment) - allow(fake_github_check).to receive(:cancelled) - - build_plan.create - end - - it 'must create a new check_suite' do - expect(pull_request.author).to eq(new_pull_request.author) - expect(check_suite.author).to eq(author) - expect(previous_ci_job.status).to eq('cancelled') + expect(build_plan.create).to eq([200, 'Scheduled Plan Runs']) end end context 'when commit and has a previous CI jobs running' do let(:action) { 'opened' } - let(:pull_request) { create(:pull_request, github_pr_id: pr_number, repository: repo) } let(:previous_check_suite) { create(:check_suite, :with_running_success_ci_jobs, pull_request: pull_request) } let(:previous_ci_job) { previous_check_suite.reload.ci_jobs.last } let(:check_suite) { pull_request.reload.check_suites.last } @@ -188,6 +136,8 @@ end describe 'Invalid commands' do + let!(:plan) { create(:plan, github_repo_name: repo) } + let(:pr_number) { 0 } let(:repo) { 'unit-test/xxx' } let(:payload) do @@ -234,38 +184,11 @@ end end - context 'when check suite does not persisted at database' do - let(:author) { nil } - let(:action) { 'synchronize' } - let(:check_suite) { create(:check_suite) } - - before do - allow(Octokit::Client).to receive(:new).and_return(fake_client) - allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) - allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) - - allow(BambooCi::PlanRun).to receive(:new).and_return(fake_plan_run) - allow(fake_plan_run).to receive(:start_plan).and_return(200) - allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-1') - - allow(Github::Check).to receive(:new).and_return(fake_github_check) - allow(fake_github_check).to receive(:create).and_return(fake_check_run) - allow(fake_github_check).to receive(:in_progress).and_return(fake_check_run) - allow(fake_github_check).to receive(:queued).and_return(fake_check_run) - allow(fake_github_check).to receive(:fetch_username).and_return({}) - - allow(CheckSuite).to receive(:create).and_return(check_suite) - allow(check_suite).to receive(:persisted?).and_return(false) - end - - it 'must returns an error' do - expect(build_plan.create).to eq([422, 'Failed to save Check Suite']) - end - end - context 'when failed to start CI' do let(:author) { 'Jonny Rocket' } let(:action) { 'synchronize' } + let(:check_suite) { create(:check_suite, pull_request: pull_request) } + let(:pull_request) { create(:pull_request) } before do allow(Octokit::Client).to receive(:new).and_return(fake_client) @@ -284,13 +207,15 @@ end it 'must returns an error' do - expect(build_plan.create).to eq([400, 'Failed to create CI Plan']) + expect(build_plan.create).to eq([200, 'Scheduled Plan Runs']) end end context 'when failed to fetch the running plan' do let(:author) { 'Jonny Rocket' } let(:action) { 'synchronize' } + let(:check_suite) { create(:check_suite, pull_request: pull_request) } + let(:pull_request) { create(:pull_request, author: author) } before do allow(Octokit::Client).to receive(:new).and_return(fake_client) @@ -307,11 +232,11 @@ allow(fake_github_check).to receive(:queued).and_return(fake_check_run) allow(fake_github_check).to receive(:fetch_username).and_return({}) - allow(BambooCi::RunningPlan).to receive(:fetch).with(fake_plan_run.bamboo_reference).and_return([]) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return([]) end it 'must returns an error' do - expect(build_plan.create).to eq([422, 'Failed to fetch RunningPlan']) + expect(build_plan.create).to eq([200, 'Scheduled Plan Runs']) end end end diff --git a/spec/lib/github/plan_execution/finished_spec.rb b/spec/lib/github/plan_execution/finished_spec.rb index 12aad0a..66c09d7 100644 --- a/spec/lib/github/plan_execution/finished_spec.rb +++ b/spec/lib/github/plan_execution/finished_spec.rb @@ -319,5 +319,40 @@ expect(pla_exec.finished).to eq([200, 'Still running']) end end + + context 'when ci_job.job_ref is nil and not current execution' do + let(:status) { 200 } + let(:ci_job) { check_suite.ci_jobs.last } + let(:check_suite) { create(:check_suite, :with_stages_and_jobs, :with_running_ci_jobs) } + let(:payload) { { 'bamboo_ref' => check_suite.bamboo_ci_ref } } + let(:summary) { double(build_summary: nil) } + let(:body) do + { + 'stages' => { + 'stage' => [ + { + 'results' => { + 'result' => [ci_job] + } + } + ] + } + } + end + + before do + allow(ci_job).to receive(:enqueue) + allow(check_suite).to receive(:ci_jobs).and_return([ci_job]) + allow(Github::Check).to receive(:new).and_return(fake_github_check) + allow(check_suite.pull_request).to receive(:current_execution?).and_return(false) + allow(Github::Build::Summary).to receive(:new).and_return(summary) + allow(summary).to receive(:build_summary) + allow_any_instance_of(PullRequest).to receive(:current_execution?).and_return(false) + end + + it 'enqueues the ci_job and returns false' do + expect(pla_exec.finished).to eq([200, 'Finished']) + end + end end end diff --git a/spec/lib/github/re_run/command_spec.rb b/spec/lib/github/re_run/command_spec.rb index d5f2748..a05476a 100644 --- a/spec/lib/github/re_run/command_spec.rb +++ b/spec/lib/github/re_run/command_spec.rb @@ -9,10 +9,11 @@ # frozen_string_literal: true describe Github::ReRun::Command do + let(:pull_request) { create(:pull_request) } let(:rerun) { described_class.new(payload) } let(:fake_client) { Octokit::Client.new } let(:fake_github_check) { Github::Check.new(nil) } - let(:fake_plan_run) { BambooCi::PlanRun.new(nil) } + let(:fake_plan_run) { BambooCi::PlanRun.new(nil, pull_request.plans.last) } before do allow(File).to receive(:read).and_return('') @@ -83,7 +84,7 @@ end it 'must returns success' do - expect(rerun.start).to eq([201, 'Starting re-run (command)']) + expect(rerun.start).to eq([200, 'Scheduled Plan Runs']) expect(check_suites.size).to eq(2) expect(check_suites.first.re_run).to be_falsey expect(check_suites.last.re_run).to be_truthy diff --git a/spec/lib/github/re_run/comment_spec.rb b/spec/lib/github/re_run/comment_spec.rb index 044b057..1ebb0f3 100644 --- a/spec/lib/github/re_run/comment_spec.rb +++ b/spec/lib/github/re_run/comment_spec.rb @@ -12,7 +12,7 @@ let(:rerun) { described_class.new(payload) } let(:fake_client) { Octokit::Client.new } let(:fake_github_check) { Github::Check.new(nil) } - let(:fake_plan_run) { BambooCi::PlanRun.new(nil) } + let(:fake_plan_run) { BambooCi::PlanRun.new(nil, pull_request.plans.last) } let(:fake_unavailable) { Github::Build::UnavailableJobs.new(nil) } let!(:pull_request) { create(:pull_request, :with_check_suite, id: 1) } @@ -50,7 +50,9 @@ let(:fake_translation) { create(:stage_configuration) } context 'when receives a valid command' do - let(:check_suite) { create(:check_suite, :with_running_ci_jobs) } + let(:pull_request) { create(:pull_request, github_pr_id: 22, repository: 'test') } + let(:check_suite) { create(:check_suite, :with_running_ci_jobs, pull_request: pull_request) } + let(:previous_check_suite) { create(:check_suite, :with_running_ci_jobs, pull_request: pull_request) } let(:ci_jobs) do [ { name: 'First Test', job_ref: 'UNIT-TEST-FIRST-1', stage: fake_translation.bamboo_stage_name }, @@ -68,6 +70,9 @@ let(:check_suites) { CheckSuite.where(commit_sha_ref: check_suite.commit_sha_ref) } before do + previous_check_suite + check_suite + allow(Octokit::Client).to receive(:new).and_return(fake_client) allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) @@ -88,11 +93,11 @@ allow(fake_plan_run).to receive(:bamboo_reference).and_return('CHK-01') allow(BambooCi::StopPlan).to receive(:build) - allow(BambooCi::RunningPlan).to receive(:fetch).with(fake_plan_run.bamboo_reference).and_return(ci_jobs) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return(ci_jobs) end it 'must returns success' do - expect(rerun.start).to eq([201, 'Starting re-run (comment)']) + expect(rerun.start).to eq([200, 'Scheduled Plan Runs']) expect(check_suites.size).to eq(2) end end @@ -138,11 +143,11 @@ allow(fake_plan_run).to receive(:bamboo_reference).and_return('CHK-01') allow(BambooCi::StopPlan).to receive(:build) - allow(BambooCi::RunningPlan).to receive(:fetch).with(fake_plan_run.bamboo_reference).and_return(ci_jobs) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return(ci_jobs) end it 'must returns success' do - expect(rerun.start).to eq([201, 'Starting re-run (comment)']) + expect(rerun.start).to eq([200, 'Scheduled Plan Runs']) expect(check_suites.size).to eq(2) end end @@ -207,13 +212,13 @@ allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-1') allow(BambooCi::StopPlan).to receive(:build) - allow(BambooCi::RunningPlan).to receive(:fetch).with(fake_plan_run.bamboo_reference).and_return(ci_jobs) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return(ci_jobs) another_check_suite end it 'must returns success' do - expect(rerun.start).to eq([201, 'Starting re-run (comment)']) + expect(rerun.start).to eq([200, 'Scheduled Plan Runs']) expect(check_suite_rerun).not_to be_nil end @@ -229,7 +234,8 @@ end context 'when you receive an comment' do - let(:check_suite) { create(:check_suite, :with_running_ci_jobs) } + let(:pull_request) { create(:pull_request, github_pr_id: 12, repository: 'test') } + let(:check_suite) { create(:check_suite, :with_running_ci_jobs, pull_request: pull_request) } let(:check_suite_rerun) { CheckSuite.find_by(commit_sha_ref: check_suite.commit_sha_ref, re_run: true) } let(:ci_jobs) do @@ -288,56 +294,90 @@ allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-1') allow(BambooCi::StopPlan).to receive(:build) - allow(BambooCi::RunningPlan).to receive(:fetch).with(fake_plan_run.bamboo_reference).and_return(ci_jobs) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return(ci_jobs) end it 'must returns success' do - expect(rerun.start).to eq([201, 'Starting re-run (comment)']) + expect(rerun.start).to eq([200, 'Scheduled Plan Runs']) expect(check_suite_rerun).not_to be_nil end end - end - describe 'alternative scenarios' do - let(:fake_client) { Octokit::Client.new } - let(:fake_github_check) { Github::Check.new(nil) } - let(:fake_translation) { create(:stage_configuration) } + context 'when receives an invalid pull request' do + let(:pull_request) { create(:pull_request, github_pr_id: 12, repository: 'test') } + let(:check_suite) { create(:check_suite, :with_running_ci_jobs, pull_request: pull_request) } + let(:check_suite_rerun) { CheckSuite.find_by(commit_sha_ref: check_suite.commit_sha_ref, re_run: true) } - before do - allow(Octokit::Client).to receive(:new).and_return(fake_client) - allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) - allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) - allow(fake_client).to receive(:pull_request_commits).and_return(pull_request_commits, []) + let(:ci_jobs) do + [ + { name: 'First Test', job_ref: 'UNIT-TEST-FIRST-1', stage: fake_translation.bamboo_stage_name } + ] + end - allow(Github::Check).to receive(:new).and_return(fake_github_check) - allow(fake_github_check).to receive(:create).and_return(fake_check_suite) - allow(fake_github_check).to receive(:add_comment) - allow(fake_github_check).to receive(:cancelled) - allow(fake_github_check).to receive(:queued) - allow(fake_github_check).to receive(:pull_request_info).and_return(pull_request_info) - allow(fake_github_check).to receive(:fetch_username).and_return({}) - allow(fake_github_check).to receive(:check_runs_for_ref).and_return({}) + let(:payload) do + { + 'action' => 'created', + 'comment' => { + 'body' => 'CI:rerun', + 'user' => { 'login' => 'John' } + }, + 'repository' => { 'full_name' => check_suite.pull_request.repository }, + 'issue' => { 'number' => check_suite.pull_request.github_pr_id } + } + end - allow(BambooCi::PlanRun).to receive(:new).and_return(fake_plan_run) - allow(fake_plan_run).to receive(:start_plan).and_return(200) - allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-1') + let(:pull_request_info) do + { + head: { + ref: 'master' + }, + base: { + ref: 'test', + sha: check_suite.base_sha_ref + } + } + end - allow(BambooCi::StopPlan).to receive(:build) - allow(BambooCi::RunningPlan).to receive(:fetch).with(fake_plan_run.bamboo_reference).and_return(bamboo_jobs) + let(:pull_request_commits) do + [ + { sha: check_suite.commit_sha_ref, date: Time.now } + ] + end + + before do + allow(Octokit::Client).to receive(:new).and_return(fake_client) + allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) + allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) + allow(fake_client).to receive(:pull_request_commits).and_return(pull_request_commits, []) + + allow(PullRequest).to receive(:find_by).and_return(nil) + end + + it 'must returns failure' do + expect(rerun.start).to eq([404, 'Pull Request not found']) + end end - context 'when you receive an comment and does not exist a PR' do - let(:commit_sha) { Faker::Internet.uuid } + context 'when receives a valid pull request but without check_suites' do + let(:pull_request) { create(:pull_request, github_pr_id: 12, repository: 'test') } + let(:check_suite) { create(:check_suite, :with_running_ci_jobs, pull_request: pull_request) } + let(:check_suite_rerun) { CheckSuite.find_by(commit_sha_ref: check_suite.commit_sha_ref, re_run: true) } + + let(:ci_jobs) do + [ + { name: 'First Test', job_ref: 'UNIT-TEST-FIRST-1', stage: fake_translation.bamboo_stage_name } + ] + end let(:payload) do { 'action' => 'created', 'comment' => { - 'body' => 'CI:rerun 000000', + 'body' => 'CI:rerun', 'user' => { 'login' => 'John' } }, - 'repository' => { 'full_name' => 'unit_test' }, - 'issue' => { 'number' => pull_request.github_pr_id } + 'repository' => { 'full_name' => check_suite.pull_request.repository }, + 'issue' => { 'number' => check_suite.pull_request.github_pr_id } } end @@ -348,40 +388,142 @@ }, base: { ref: 'test', - sha: commit_sha + sha: check_suite.base_sha_ref } } end let(:pull_request_commits) do [ - { sha: commit_sha, date: Time.now } + { sha: check_suite.commit_sha_ref, date: Time.now } ] end - let(:bamboo_jobs) do + before do + allow(Octokit::Client).to receive(:new).and_return(fake_client) + allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) + allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) + allow(fake_client).to receive(:pull_request_commits).and_return(pull_request_commits, []) + + allow(PullRequest).to receive(:find_by).and_return(pull_request) + allow(pull_request).to receive(:check_suites).and_return([]) + end + + it 'must returns failure' do + expect(rerun.start).to eq([404, 'Can not rerun a new PullRequest']) + end + end + + context 'when you receive an comment' do + let(:pull_request) { create(:pull_request, github_pr_id: 12, repository: 'test') } + let(:check_suite) { create(:check_suite, :with_running_ci_jobs, pull_request: pull_request) } + let(:check_suite_rerun) { CheckSuite.find_by(commit_sha_ref: check_suite.commit_sha_ref, re_run: true) } + + let(:ci_jobs) do [ - { name: 'test', job_ref: 'checkout-01', stage: fake_translation.bamboo_stage_name } + { name: 'First Test', job_ref: 'UNIT-TEST-FIRST-1', stage: fake_translation.bamboo_stage_name } ] end - let(:fake_check_suite) { create(:check_suite, pull_request: pull_request) } - let(:check_suite_rerun) { CheckSuite.find_by(commit_sha_ref: commit_sha, re_run: true) } + let(:payload) do + { + 'action' => 'created', + 'comment' => { + 'body' => "CI:rerun 000000 ##{check_suite.commit_sha_ref}", + 'user' => { 'login' => 'John' } + }, + 'repository' => { 'full_name' => check_suite.pull_request.repository }, + 'issue' => { 'number' => check_suite.pull_request.github_pr_id } + } + end + + let(:pull_request_info) do + { + head: { + ref: 'master' + }, + base: { + ref: 'test', + sha: check_suite.base_sha_ref + } + } + end + + let(:pull_request_commits) do + [ + { sha: check_suite.commit_sha_ref, date: Time.now } + ] + end + + before do + allow(Octokit::Client).to receive(:new).and_return(fake_client) + allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) + allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) + allow(fake_client).to receive(:pull_request_commits).and_return(pull_request_commits, []) + + allow(Github::Check).to receive(:new).and_return(fake_github_check) + allow(fake_github_check).to receive(:create).and_return(check_suite) + allow(fake_github_check).to receive(:add_comment) + allow(fake_github_check).to receive(:cancelled) + allow(fake_github_check).to receive(:queued) + allow(fake_github_check).to receive(:pull_request_info).and_return(pull_request_info) + allow(fake_github_check).to receive(:fetch_username).and_return({}) + allow(fake_github_check).to receive(:check_runs_for_ref).and_return({}) + + allow(BambooCi::PlanRun).to receive(:new).and_return(fake_plan_run) + allow(fake_plan_run).to receive(:start_plan).and_return(200) + allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-1') + + allow(BambooCi::StopPlan).to receive(:build) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return(ci_jobs) + + allow(CheckSuite).to receive(:create).and_return(check_suite) + allow(check_suite).to receive(:persisted?).and_return(false) + end it 'must returns success' do - expect(rerun.start).to eq([201, 'Starting re-run (comment)']) - expect(check_suite_rerun).not_to be_nil + expect(rerun.start).to eq([200, 'Scheduled Plan Runs']) end end + end + + describe 'alternative scenarios' do + let(:fake_client) { Octokit::Client.new } + let(:fake_github_check) { Github::Check.new(nil) } + let(:fake_translation) { create(:stage_configuration) } + + before do + allow(Octokit::Client).to receive(:new).and_return(fake_client) + allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) + allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) + allow(fake_client).to receive(:pull_request_commits).and_return(pull_request_commits, []) - context 'when can not save check_suite' do + allow(Github::Check).to receive(:new).and_return(fake_github_check) + allow(fake_github_check).to receive(:create).and_return(fake_check_suite) + allow(fake_github_check).to receive(:add_comment) + allow(fake_github_check).to receive(:cancelled) + allow(fake_github_check).to receive(:queued) + allow(fake_github_check).to receive(:pull_request_info).and_return(pull_request_info) + allow(fake_github_check).to receive(:fetch_username).and_return({}) + allow(fake_github_check).to receive(:check_runs_for_ref).and_return({}) + + allow(BambooCi::PlanRun).to receive(:new).and_return(fake_plan_run) + allow(fake_plan_run).to receive(:start_plan).and_return(200) + allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-1') + + allow(BambooCi::StopPlan).to receive(:build) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return(bamboo_jobs) + end + + context 'when you receive an comment and does not exist a PR' do let(:commit_sha) { Faker::Internet.uuid } let(:payload) do { 'action' => 'created', 'comment' => { - 'body' => 'CI:rerun 000000' + 'body' => 'CI:rerun 000000', + 'user' => { 'login' => 'John' } }, 'repository' => { 'full_name' => 'unit_test' }, 'issue' => { 'number' => pull_request.github_pr_id } @@ -413,13 +555,11 @@ end let(:fake_check_suite) { create(:check_suite, pull_request: pull_request) } - - before do - create(:plan, github_repo_name: 'unit_test') - end + let(:check_suite_rerun) { CheckSuite.find_by(commit_sha_ref: commit_sha, re_run: true) } it 'must returns success' do - expect(rerun.start).to eq([404, 'Failed to create a check suite']) + expect(rerun.start).to eq([200, 'Scheduled Plan Runs']) + expect(check_suite_rerun).not_to be_nil end end end diff --git a/spec/lib/github/retry/comment_spec.rb b/spec/lib/github/retry/comment_spec.rb index 2347551..5f2f348 100644 --- a/spec/lib/github/retry/comment_spec.rb +++ b/spec/lib/github/retry/comment_spec.rb @@ -12,7 +12,7 @@ let(:github_retry) { described_class.new(payload) } let(:fake_client) { Octokit::Client.new } let(:fake_github_check) { Github::Check.new(nil) } - let(:fake_plan_run) { BambooCi::PlanRun.new(nil) } + let(:fake_plan_run) { BambooCi::PlanRun.new(nil, check_suite.pull_request.plans.last) } let(:fake_unavailable) { Github::Build::UnavailableJobs.new(nil) } before do diff --git a/spec/workers/create_execution_by_command_spec.rb b/spec/workers/create_execution_by_command_spec.rb new file mode 100644 index 0000000..e28a0fb --- /dev/null +++ b/spec/workers/create_execution_by_command_spec.rb @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# create_execution_by_command_spec.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +describe CreateExecutionByCommand do + let(:plan) { create(:plan) } + let(:pull_request) { create(:pull_request, plan: plan) } + let(:check_suite) { create(:check_suite, pull_request: pull_request) } + let(:payload) do + { + 'sender' => { 'login' => 'user', 'id' => 123, 'type' => 'User' } + } + end + + before do + allow(Plan).to receive(:find).with(plan.id).and_return(plan) + allow(GithubLogger).to receive_message_chain(:instance, :create).and_return(Logger.new($stdout)) + allow(Logger).to receive(:new).and_return(Logger.new($stdout)) + allow(Github::Check).to receive(:new) + allow_any_instance_of(CreateExecutionByCommand).to receive(:stop_previous_execution) + allow_any_instance_of(CreateExecutionByCommand).to receive(:ci_jobs) + allow_any_instance_of(CreateExecutionByCommand).to receive(:cleanup) + bamboo_plan_run_double = double('BambooCi::PlanRun') + allow(bamboo_plan_run_double).to receive(:ci_variables=) + allow(bamboo_plan_run_double).to receive(:start_plan) + allow(BambooCi::PlanRun).to receive(:new).and_return(bamboo_plan_run_double) + allow(AuditRetry).to receive(:create) + allow(Github::UserInfo).to receive(:new) + end + + describe '.create' do + it 'returns [404, "Failed to fetch a check suite"] if check_suite is nil' do + allow(CheckSuite).to receive(:find).with(999).and_return(nil) + expect(described_class.create(plan.id, 999, payload)).to eq([404, 'Failed to fetch a check suite']) + end + end +end diff --git a/spec/workers/create_execution_by_comment_spec.rb b/spec/workers/create_execution_by_comment_spec.rb new file mode 100644 index 0000000..805a418 --- /dev/null +++ b/spec/workers/create_execution_by_comment_spec.rb @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# create_execution_by_comment_spec.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +describe CreateExecutionByComment do + let(:pull_request) { create(:pull_request) } + let(:plan) { create(:plan) } + let(:payload) do + { + 'comment' => { 'body' => 'ci:rerun #123456', 'user' => { 'login' => 'user' } }, + 'action' => 'created' + } + end + + let(:fake_client) { Octokit::Client.new } + let(:fake_github_check) { Github::Check.new(nil) } + let(:fake_plan_run) { BambooCi::PlanRun.new(nil, pull_request.plans.last) } + let(:fake_check_run) { create(:check_suite) } + let(:fake_action) { double('Github::Build::Action') } + + before do + allow(File).to receive(:read).and_return('') + allow(OpenSSL::PKey::RSA).to receive(:new).and_return(OpenSSL::PKey::RSA.new(2048)) + allow(TimeoutExecution).to receive_message_chain(:delay, :timeout).and_return(true) + allow(GitHubApp::Configuration).to receive(:new).and_return(GitHubApp::Configuration.instance) + + allow(Octokit::Client).to receive(:new).and_return(fake_client) + allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) + allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) + + allow(BambooCi::PlanRun).to receive(:new).and_return(fake_plan_run) + allow(fake_plan_run).to receive(:start_plan).and_return(200) + allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-FIRST-1') + allow(fake_plan_run).to receive(:bamboo_reference).and_return('CHECKOUT-1') + + allow(Github::Check).to receive(:new).and_return(fake_github_check) + allow(fake_github_check).to receive(:create).and_return(fake_check_run) + allow(fake_github_check).to receive(:in_progress).and_return(fake_check_run) + allow(fake_github_check).to receive(:queued).and_return(fake_check_run) + allow(fake_github_check).to receive(:fetch_username).and_return({}) + allow(fake_github_check).to receive(:fetch_username).and_return({}) + allow(fake_github_check).to receive(:check_runs_for_ref).and_return({}) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return({ job: '1' }) + allow(Github::Build::Action).to receive(:new).and_return(fake_action) + allow(fake_action).to receive(:create_summary) + + allow(GithubLogger).to receive_message_chain(:instance, :create).and_return(Logger.new($stdout)) + allow(Logger).to receive(:new).and_return(Logger.new($stdout)) + allow(PullRequest).to receive(:find).and_return(pull_request) + allow_any_instance_of(CreateExecutionByComment).to receive(:run_by_plan).and_return([201, + 'Starting re-run (comment)']) + end + + describe '.create' do + it 'returns [422, "Plan not found"] if plan is nil' do + expect(described_class.create(pull_request.id, payload, nil)).to eq([422, 'Plan not found']) + end + end + + describe '#fetch_last_commit_or_sha256' do + it 'returns commit if commit exists and action matches ci:rerun # pattern' do + instance = described_class.allocate + # Corrige erro de @payload nil + instance.instance_variable_set(:@payload, { 'repository' => { 'full_name' => 'repo/name' } }) + allow(instance).to receive(:action).and_return('ci:rerun #123456') + commit = double('commit') + allow(Github::Parsers::PullRequestCommit).to receive_message_chain(:new, :find_by_sha).and_return(commit) + expect(instance.send(:fetch_last_commit_or_sha256)).to eq(commit) + end + end + + describe '#action?' do + it 'returns true when action matches ci:rerun and payload action is created' do + instance = described_class.allocate + instance.instance_variable_set(:@payload, { 'action' => 'created' }) + allow(instance).to receive(:action).and_return('ci:rerun') + expect(instance.send(:action?)).to be true + end + + it 'returns false when action does not match ci:rerun' do + instance = described_class.allocate + instance.instance_variable_set(:@payload, { 'action' => 'created' }) + allow(instance).to receive(:action).and_return('other') + expect(instance.send(:action?)).to be false + end + + it 'returns false when payload action is not created' do + instance = described_class.allocate + instance.instance_variable_set(:@payload, { 'action' => 'edited' }) + allow(instance).to receive(:action).and_return('ci:rerun') + expect(instance.send(:action?)).to be false + end + end +end diff --git a/spec/workers/create_execution_by_plan_spec.rb b/spec/workers/create_execution_by_plan_spec.rb new file mode 100644 index 0000000..144e40f --- /dev/null +++ b/spec/workers/create_execution_by_plan_spec.rb @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# create_execution_by_plan_spec.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +describe CreateExecutionByPlan do + let(:pull_request) { create(:pull_request, id: 25) } + let(:payload) do + { + 'pull_request' => { + 'user' => { 'login' => 'user', 'id' => 123 }, + 'head' => { 'sha' => 'abc123', 'ref' => 'feature' }, + 'base' => { 'sha' => 'def456', 'ref' => 'main' } + } + } + end + + let(:fake_client) { Octokit::Client.new } + let(:fake_github_check) { Github::Check.new(nil) } + let(:fake_plan_run) { BambooCi::PlanRun.new(nil, pull_request.plans.last) } + let(:fake_check_run) { create(:check_suite) } + let(:fake_action) { double('Github::Build::Action') } + + before do + allow(File).to receive(:read).and_return('') + allow(OpenSSL::PKey::RSA).to receive(:new).and_return(OpenSSL::PKey::RSA.new(2048)) + allow(TimeoutExecution).to receive_message_chain(:delay, :timeout).and_return(true) + allow(GitHubApp::Configuration).to receive(:new).and_return(GitHubApp::Configuration.instance) + + allow(Octokit::Client).to receive(:new).and_return(fake_client) + allow(fake_client).to receive(:find_app_installations).and_return([{ 'id' => 1 }]) + allow(fake_client).to receive(:create_app_installation_access_token).and_return({ 'token' => 1 }) + + allow(BambooCi::PlanRun).to receive(:new).and_return(fake_plan_run) + allow(fake_plan_run).to receive(:start_plan).and_return(200) + allow(fake_plan_run).to receive(:bamboo_reference).and_return('UNIT-TEST-FIRST-1') + allow(fake_plan_run).to receive(:bamboo_reference).and_return('CHECKOUT-1') + + allow(Github::Check).to receive(:new).and_return(fake_github_check) + allow(fake_github_check).to receive(:create).and_return(fake_check_run) + allow(fake_github_check).to receive(:in_progress).and_return(fake_check_run) + allow(fake_github_check).to receive(:queued).and_return(fake_check_run) + allow(fake_github_check).to receive(:fetch_username).and_return({}) + allow(fake_github_check).to receive(:fetch_username).and_return({}) + allow(fake_github_check).to receive(:check_runs_for_ref).and_return({}) + allow(BambooCi::RunningPlan).to receive(:fetch).and_return({ job: '1' }) + allow(Github::Build::Action).to receive(:new).and_return(fake_action) + allow(fake_action).to receive(:create_summary) + end + + describe '.create' do + let(:fake_suite) { create(:check_suite) } + it 'returns [422, "Plan not found"]' do + allow(Plan).to receive(:find_by).and_return(nil) + expect(described_class.create(pull_request.id, payload, 999)).to eq([422, 'Plan not found']) + end + + it 'returns [422, "Failed to save Check Suite"]' do + allow(CheckSuite).to receive(:create).and_return(fake_suite) + allow(fake_suite).to receive(:persisted?).and_return(false) + + expect(described_class.create(pull_request.id, payload, + pull_request.plans.last.id)).to eq([422, 'Failed to save Check Suite']) + end + + it 'must create the execution' do + result = described_class.create(pull_request.id, payload, pull_request.plans.last.id) + expect(result).to eq([200, 'Pull Request created']) + end + + context 'when plan does not exists' do + it 'returns [422, "Plan not found"]' do + allow(Plan).to receive(:find_by).and_return(nil) + expect(described_class.create(pull_request.id, payload, 999)).to eq([422, 'Plan not found']) + end + end + end +end diff --git a/workers/ci_job_status.rb b/workers/ci_job_status.rb index 970244c..76d01aa 100644 --- a/workers/ci_job_status.rb +++ b/workers/ci_job_status.rb @@ -11,9 +11,9 @@ require_relative '../config/setup' class CiJobStatus - def self.update(check_suite_id, ci_job_id) + def self.update(bamboo_ci_ref, ci_job_id) @logger = GithubLogger.instance.create('ci_job_status.log', Logger::INFO) - @logger.info("CiJobStatus::Update: Checksuite #{check_suite_id} -> '#{ci_job_id}'") + @logger.info("CiJobStatus::Update: Checksuite #{bamboo_ci_ref} -> '#{ci_job_id}'") job = CiJob.find(ci_job_id) @@ -22,9 +22,9 @@ def self.update(check_suite_id, ci_job_id) return unless job.finished? - @logger.info("Github::PlanExecution::Finished: '#{job.check_suite.bamboo_ci_ref}'") + @logger.info("Github::PlanExecution::Finished: '#{bamboo_ci_ref}'") - finished = Github::PlanExecution::Finished.new({ 'bamboo_ref' => job.check_suite.bamboo_ci_ref }) + finished = Github::PlanExecution::Finished.new({ 'bamboo_ref' => bamboo_ci_ref }) finished.finished end end diff --git a/workers/create_execution_by_command.rb b/workers/create_execution_by_command.rb new file mode 100644 index 0000000..b3addaa --- /dev/null +++ b/workers/create_execution_by_command.rb @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# create_execution_by_command.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +class CreateExecutionByCommand < Github::ReRun::Base + def self.create(plan_id, check_suite_id, payload) + check_suite = CheckSuite.find(check_suite_id) + plan = Plan.find(plan_id) + + return [404, 'Failed to fetch a check suite'] if check_suite.nil? + + instance = new(plan, check_suite, payload) + + instance.status + end + + attr_reader :status + + def initialize(plan, check_suite, payload) + super(payload, logger_level: Logger::INFO) + + @logger_manager << GithubLogger.instance.create('github_rerun_command.log', Logger::INFO) + @logger_manager << Logger.new($stdout) + + @github_check = Github::Check.new(check_suite) + + stop_previous_execution(plan) + + check_suite = create_check_suite(check_suite) + + start_new_execution(check_suite, plan) + ci_jobs(check_suite, plan) + end + + def create_check_suite(check_suite) + CheckSuite.create( + pull_request: check_suite.pull_request, + author: check_suite.author, + commit_sha_ref: check_suite.commit_sha_ref, + work_branch: check_suite.work_branch, + base_sha_ref: check_suite.base_sha_ref, + merge_branch: check_suite.merge_branch, + re_run: true + ) + end + + def start_new_execution(check_suite, plan) + cleanup(check_suite) + + bamboo_plan_run = BambooCi::PlanRun.new(check_suite, plan, logger_level: @logger_level) + bamboo_plan_run.ci_variables = ci_vars + bamboo_plan_run.start_plan + + audit_retry = + AuditRetry.create(check_suite: check_suite, + github_username: @payload.dig('sender', 'login'), + github_id: @payload.dig('sender', 'id'), + github_type: @payload.dig('sender', 'type'), + retry_type: 'full') + + Github::UserInfo.new(@payload.dig('sender', 'id'), check_suite: check_suite, audit_retry: audit_retry) + end +end diff --git a/workers/create_execution_by_comment.rb b/workers/create_execution_by_comment.rb new file mode 100644 index 0000000..9caaca3 --- /dev/null +++ b/workers/create_execution_by_comment.rb @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# create_execution_by_comment.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +class CreateExecutionByComment < Github::ReRun::Base + def self.create(pull_request_id, payload, plan_id) + logger = GithubLogger.instance.create('github_app.log', Logger::INFO) + plan = Plan.find_by(id: plan_id) + + return [422, 'Plan not found'] if plan.nil? + + instance = new(pull_request_id, payload, plan) + + logger.info "CreateExecutionByComment: Plan '#{plan.name}' for Pull Request ID: #{pull_request_id} with " \ + "status: #{instance.status.inspect}" + + instance.status + end + + attr_reader :status + + def initialize(pull_request_id, payload, plan) + super(payload, logger_level: Logger::INFO) + + @logger_manager << GithubLogger.instance.create('github_rerun_comment.log', Logger::INFO) + @logger_manager << Logger.new($stdout) + + @pull_request = PullRequest.find(pull_request_id) + @status = [] + + run_by_plan(plan) + end + + private + + def run_by_plan(plan) + check_suite = sha256_or_comment? + logger(Logger::DEBUG, ">>> Check suite: #{check_suite.inspect}") + + return [404, 'Failed to create a check suite'] if check_suite.nil? + + check_suite.update(plan: plan) + + stop_previous_execution(plan) + + start_new_execution(check_suite, plan) + + ci_jobs(check_suite, plan) + + [201, 'Starting re-run (comment)'] + end + + def sha256_or_comment? + fetch_old_check_suite + + @old_check_suite.nil? ? comment_flow : sha256_flow + end + + def comment_flow + commit = fetch_last_commit_or_sha256 + github_check = fetch_github_check + pull_request_info = github_check.pull_request_info(pr_id, repo) + + fetch_old_check_suite(commit[:sha]) + check_suite = create_check_suite_by_commit(commit, @pull_request, pull_request_info) + logger(Logger::INFO, "CheckSuite errors: #{check_suite.inspect}") + return nil unless check_suite.persisted? + + @github_check = Github::Check.new(check_suite) + + check_suite + end + + # Fetches the GitHub check associated with the pull request. + # + # This method finds the pull request by its GitHub PR ID and then retrieves + # the last check suite associated with that pull request. It then initializes + # a new `Github::Check` object with the last check suite. + # + # @return [Github::Check] the GitHub check associated with the pull request. + # + # @raise [ActiveRecord::RecordNotFound] if the pull request is not found. + def fetch_github_check + pull_request = PullRequest.find_by(github_pr_id: pr_id) + Github::Check.new(pull_request.check_suites.last) + end + + def create_check_suite_by_commit(commit, pull_request, pull_request_info) + CheckSuite.create( + pull_request: pull_request, + author: @payload.dig('comment', 'user', 'login'), + commit_sha_ref: commit[:sha], + work_branch: pull_request_info.dig(:head, :ref), + base_sha_ref: pull_request_info.dig(:base, :sha), + merge_branch: pull_request_info.dig(:base, :ref), + re_run: true + ) + end + + def sha256_flow + @github_check = Github::Check.new(@old_check_suite) + create_new_check_suite + end + + # The behaviour will be the following: It will fetch the last commit if it has + # received a comment and only fetch a commit if the command starts with ci:rerrun #. + # If there is any other character before the # it will be considered a comment. + def fetch_last_commit_or_sha256 + pull_request_commit = Github::Parsers::PullRequestCommit.new(repo, pr_id) + commit = pull_request_commit.find_by_sha(sha256) + + return commit if commit and action.match(/ci:rerun\s+#/i) + + fetch_last_commit + end + + def fetch_last_commit + Github::Parsers::PullRequestCommit.new(repo, pr_id).last_commit_in_pr + end + + def fetch_old_check_suite(sha = sha256) + return if sha.nil? + + logger(Logger::DEBUG, ">>> fetch_old_check_suite SHA: #{sha}") + + @old_check_suite = + CheckSuite + .joins(:pull_request) + .where('commit_sha_ref ILIKE ? AND pull_requests.repository = ?', "#{sha}%", repo) + .last + end + + def create_new_check_suite + CheckSuite.create( + pull_request: @pull_request, + author: @old_check_suite.author, + commit_sha_ref: @old_check_suite.commit_sha_ref, + work_branch: @old_check_suite.work_branch, + base_sha_ref: @old_check_suite.base_sha_ref, + merge_branch: @old_check_suite.merge_branch, + re_run: true + ) + end + + def sha256 + return nil unless action.downcase.match? 'ci:rerun #' + + action.downcase.split('#').last + end + + def action? + action.to_s.downcase.match? 'ci:rerun' and @payload['action'] == 'created' + end +end diff --git a/workers/create_execution_by_plan.rb b/workers/create_execution_by_plan.rb new file mode 100644 index 0000000..09f7a1d --- /dev/null +++ b/workers/create_execution_by_plan.rb @@ -0,0 +1,177 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# create_execution_by_plan.rb +# Part of NetDEF CI System +# +# Copyright (c) 2025 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# frozen_string_literal: true + +class CreateExecutionByPlan + def self.create(pull_request_id, payload, plan_id) + logger = GithubLogger.instance.create('github_app.log', Logger::INFO) + plan = Plan.find_by(id: plan_id) + + return [422, 'Plan not found'] if plan.nil? + + instance = new(pull_request_id, payload, plan_id) + + logger.info "CreateExecutionByPlan: Plan '#{plan.name}' for Pull Request ID: #{pull_request_id} with " \ + "status: #{instance.status.inspect}" + + instance.status + end + + attr_reader :status + + def initialize(pull_request_id, payload, plan_id) + @logger = Logger.new($stdout) + @logger.level = Logger::INFO + + @pull_request = PullRequest.find(pull_request_id) + @payload = payload + @status = [] + + create_execution_by_plan(Plan.find_by(id: plan_id)) + end + + private + + def create_execution_by_plan(plan) + @has_previous_exec = false + + @logger.info "Starting Plan: #{plan.name}" + + fetch_last_check_suite(plan) + + create_check_suite + + unless @check_suite.persisted? + @status = [422, 'Failed to save Check Suite'] + + return + end + + @check_suite.update(plan: plan) + + @logger.info "Check Suite created: #{@check_suite.inspect}" + + # Stop a previous execution - Avoiding CI spam + stop_previous_execution + + @logger.info "Starting a new execution for Pull Request: #{@pull_request.inspect}" + # Starting a new CI run + status = start_new_execution(plan) + + @logger.info "New execution started with status: #{status}" + + if status != 200 + @status = [status, 'Failed to create CI Plan'] + + return + end + + @status = ci_jobs(plan) + end + + def ci_jobs(plan) + @logger.info 'Creating GitHub Check' + + SlackBot.instance.execution_started_notification(@check_suite) + + jobs = BambooCi::RunningPlan.fetch(@check_suite.bamboo_ci_ref) + + return [422, 'Failed to fetch RunningPlan'] if jobs.nil? or jobs.empty? + + action = Github::Build::Action.new(@check_suite, @github_check, jobs, plan.name) + action.create_summary + + @logger.info ">>> @has_previous_exec: #{@has_previous_exec}" + stop_execution_message if @has_previous_exec + + [200, 'Pull Request created'] + end + + def start_new_execution(plan) + @check_suite.pull_request = @pull_request + + Github::UserInfo.new(@payload.dig('pull_request', 'user', 'id'), check_suite: @check_suite) + + @logger.info 'Starting a new plan' + @bamboo_plan_run = BambooCi::PlanRun.new(@check_suite, plan, logger_level: @logger.level) + @bamboo_plan_run.ci_variables = ci_vars + @bamboo_plan_run.start_plan + end + + def stop_previous_execution + return if @last_check_suite.nil? or @last_check_suite.finished? + + @logger.info 'Stopping previous execution' + @logger.info @last_check_suite.inspect + @logger.info @check_suite.inspect + + cancel_previous_ci_jobs + end + + def cancel_previous_ci_jobs + mark_as_cancelled_jobs + + @last_check_suite.update(stopped_in_stage: @last_check_suite.stages.where(status: :in_progress).last) + + mark_as_cancelled_stages + + @has_previous_exec = true + + BambooCi::StopPlan.build(@last_check_suite.bamboo_ci_ref) + end + + def mark_as_cancelled_jobs + @last_check_suite.ci_jobs.where(status: %w[queued in_progress]).each do |ci_job| + @logger.warn("Cancelling Job #{ci_job.inspect}") + ci_job.cancelled(@github_check) + end + end + + def mark_as_cancelled_stages + @last_check_suite.stages.where(status: %w[queued in_progress]).each do |stage| + stage.cancelled(@github_check) + end + end + + def fetch_last_check_suite(plan) + @last_check_suite = + CheckSuite + .joins(pull_request: :plans) + .where(pull_request: { id: @pull_request.id, plans: { name: plan.name } }) + .last + end + + def create_check_suite + @logger.info 'Creating a check suite' + @check_suite = + CheckSuite.create( + pull_request: @pull_request, + author: @payload.dig('pull_request', 'user', 'login'), + commit_sha_ref: @payload.dig('pull_request', 'head', 'sha'), + work_branch: @payload.dig('pull_request', 'head', 'ref'), + base_sha_ref: @payload.dig('pull_request', 'base', 'sha'), + merge_branch: @payload.dig('pull_request', 'base', 'ref') + ) + + @logger.info 'Creating GitHub Check API' + @github_check = Github::Check.new(@check_suite) + end + + def ci_vars + ci_vars = [] + ci_vars << { value: @github_check.signature, name: 'signature_secret' } + + ci_vars + end + + def stop_execution_message + @check_suite.update(cancelled_previous_check_suite_id: @last_check_suite.id) + BambooCi::StopPlan.comment(@last_check_suite, @check_suite) + end +end