diff --git a/app/assets/stylesheets/page_bundles/ci_status.scss b/app/assets/stylesheets/page_bundles/ci_status.scss index f2129aa6841695450f549a97a478416546444c6a..6d2849345c0fd457f30f4f53e10fd386be045694 100644 --- a/app/assets/stylesheets/page_bundles/ci_status.scss +++ b/app/assets/stylesheets/page_bundles/ci_status.scss @@ -15,17 +15,16 @@ } &.ci-failed { - @include status-color( - var(--red-100, $red-100), + @include status-color(var(--red-100, $red-100), var(--red-500, $red-500), - var(--red-600, $red-600) - ); + var(--red-600, $red-600)); } &.ci-success { @include green-status-color; } + &.ci-canceling, &.ci-canceled, &.ci-disabled, &.ci-scheduled, @@ -39,11 +38,9 @@ } &.ci-preparing { - @include status-color( - var(--gray-100, $gray-100), + @include status-color(var(--gray-100, $gray-100), var(--gray-300, $gray-300), - var(--gray-400, $gray-400) - ); + var(--gray-400, $gray-400)); } &.ci-pending, @@ -51,20 +48,16 @@ &.ci-waiting-for-callback, &.ci-failed-with-warnings, &.ci-success-with-warnings { - @include status-color( - var(--orange-50, $orange-50), + @include status-color(var(--orange-50, $orange-50), var(--orange-500, $orange-500), - var(--orange-700, $orange-700) - ); + var(--orange-700, $orange-700)); } &.ci-info, &.ci-running { - @include status-color( - var(--blue-100, $blue-100), + @include status-color(var(--blue-100, $blue-100), var(--blue-500, $blue-500), - var(--blue-600, $blue-600) - ); + var(--blue-600, $blue-600)); } &.ci-created, @@ -76,4 +69,4 @@ background-color: rgba($gl-text-color-secondary, 0.07); } } -} +} \ No newline at end of file diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb index 17cf48bb5cf95cb1a7413d14590cb592495ec0d4..5fa90cf243cee3822df805f7e513a353ce78c737 100644 --- a/app/graphql/types/ci/pipeline_status_enum.rb +++ b/app/graphql/types/ci/pipeline_status_enum.rb @@ -12,6 +12,7 @@ class PipelineStatusEnum < BaseEnum running: 'Pipeline is running.', failed: 'At least one stage of the pipeline failed.', success: 'Pipeline completed successfully.', + canceling: 'Pipeline is in the process of canceling.', canceled: 'Pipeline was canceled before completion.', skipped: 'Pipeline was skipped.', manual: 'Pipeline needs to be manually started.', diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index d62bdfa4c8701f93236d6e0f9b68f06a1650a7af..0c4b5bd087401c59b9b2e3a0f81af9cade507514 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -37,6 +37,10 @@ class Bridge < Ci::Processable end end + event :canceling do + transition CANCELABLE_STATUSES.map(&:to_sym) => :canceling + end + event :pending do transition all => :pending end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 515e7966cc3c4af35ec5653354928fc9a42c0403..16d3318b676b0a576369fcc58bec81ee831733c5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -395,6 +395,12 @@ def self.ids_in_merge_request(merge_request_ids) in_merge_request(merge_request_ids).pluck(:id) end + # A Ci::Bridge may transition to `canceling` as a result of strategy: :depend + # but only a Ci::Build will transition to `canceling`` via `.cancel` + def supports_canceling? + Feature.enabled?(:ci_canceling_status, project, type: :wip) && cancel_gracefully? + end + def build_matcher strong_memoize(:build_matcher) do Gitlab::Ci::Matching::BuildMatcher.new({ @@ -472,7 +478,7 @@ def play(current_user, job_variables_attributes = nil) # rubocop: enable CodeReuse/ServiceClass def cancelable? - active? || created? + (active? || created?) && !canceling? end def retries_count diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 1fe6af8c595ca3cf14c706dee685a2000d5cf738..73616c4043ad3467563978bde843fb4bc7c92329 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -64,7 +64,7 @@ def update_timeout_state end def set_cancel_gracefully - runtime_runner_features.merge!({ cancel_gracefully: true }) + runtime_runner_features[:cancel_gracefully] = true end def cancel_gracefully? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2802072d937020443bb3dbedcc4251fbfaee79fa..057a2725aa68e843300c93bdded626160e7d5ccc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -187,7 +187,7 @@ class Pipeline < Ci::ApplicationRecord state_machine :status, initial: :created do event :enqueue do transition [:created, :manual, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending - transition [:success, :failed, :canceled] => :running + transition [:success, :failed, :canceling, :canceled] => :running # this is needed to ensure tests to be covered transition [:running] => :running @@ -226,6 +226,10 @@ class Pipeline < Ci::ApplicationRecord transition any => :success end + event :canceling do + transition any - [:canceling, :canceled] => :canceling + end + event :cancel do transition any - [:canceled] => :canceled end @@ -866,6 +870,7 @@ def notes project.notes.for_commit_id(sha) end + # rubocop: disable Metrics/CyclomaticComplexity -- breaking apart hurts readability def set_status(new_status) retry_optimistic_lock(self, name: 'ci_pipeline_set_status') do case new_status @@ -877,6 +882,7 @@ def set_status(new_status) when 'running' then run when 'success' then succeed when 'failed' then drop + when 'canceling' then canceling when 'canceled' then cancel when 'skipped' then skip when 'manual' then block @@ -886,6 +892,7 @@ def set_status(new_status) end end end + # rubocop: enable Metrics/CyclomaticComplexity def protected_ref? strong_memoize(:protected_ref) { project.protected_for?(git_ref) } diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 989d6337ab7600569895c9ef34a836db73af735c..8e4784e3f38f53eaeacca388116b5f0a9549b22f 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -137,7 +137,7 @@ def clone(current_user:, new_job_variables_attributes: []) def retryable? return false if retried? || archived? || deployment_rejected? - success? || failed? || canceled? + success? || failed? || canceled? || canceling? end def aggregated_needs_names diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 251a3089667b68f6455f6a84719dbbeac3ae0f74..8743ed5b00929a8aad2b1e50ed32938397399182 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -117,6 +117,10 @@ class Stage < Ci::ApplicationRecord transition any - [:success] => :success end + event :canceling do + transition any - [:canceling, :canceled] => :canceling + end + event :cancel do transition any - [:canceled] => :canceled end @@ -134,6 +138,7 @@ def self.use_partition_id_filter? Ci::Pipeline.use_partition_id_filter? end + # rubocop: disable Metrics/CyclomaticComplexity -- breaking apart hurts readability, consider refactoring issue #439268 def set_status(new_status) retry_optimistic_lock(self, name: 'ci_stage_set_status') do case new_status @@ -145,6 +150,7 @@ def set_status(new_status) when 'running' then run when 'success' then succeed when 'failed' then drop + when 'canceling' then canceling when 'canceled' then cancel when 'manual' then block when 'scheduled' then delay @@ -154,6 +160,7 @@ def set_status(new_status) end end end + # rubocop: enable Metrics/CyclomaticComplexity # This will be removed with ci_remove_ensure_stage_service def update_legacy_status diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 7ec3210f703ebad06366e8875ed85cbc9a648699..386ff3a45849704af0898eaaf08130abd10e2273 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -165,15 +165,18 @@ class CommitStatus < Ci::ApplicationRecord end event :drop do + transition canceling: :canceled # runner returns success/failed transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :failed end event :success do + transition canceling: :canceled # runner returns success/failed transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running] => :success end event :cancel do - transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :canceled + transition running: :canceling, if: :supports_canceling? + transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :manual, :scheduled, :running] => :canceled end before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status| @@ -258,6 +261,10 @@ def group_name name.to_s.sub(regex, '').strip end + def supports_canceling? + false + end + # Time spent running. def duration calculate_duration(started_at, finished_at) diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb index cf7442d186362cfc2e420a5b992a6d3585d385bd..468e66e4245b1aab2bf08475cc1e5d3311d05044 100644 --- a/app/models/concerns/ci/has_status.rb +++ b/app/models/concerns/ci/has_status.rb @@ -6,20 +6,20 @@ module HasStatus DEFAULT_STATUS = 'created' BLOCKED_STATUS = %w[manual scheduled].freeze - AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceled skipped manual scheduled].freeze + AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceling canceled skipped manual scheduled].freeze STARTED_STATUSES = %w[running success failed].freeze ACTIVE_STATUSES = %w[waiting_for_resource preparing waiting_for_callback pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze STOPPED_STATUSES = COMPLETED_STATUSES + BLOCKED_STATUS - ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceled success skipped created].freeze + ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceling canceled success skipped created].freeze PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze IGNORED_STATUSES = %w[manual].to_set.freeze - ALIVE_STATUSES = (ACTIVE_STATUSES + ['created']).freeze - CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled']).freeze + ALIVE_STATUSES = ORDERED_STATUSES - COMPLETED_STATUSES - BLOCKED_STATUS + CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled'] - ['canceling']).freeze STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, failed: 4, canceled: 5, skipped: 6, manual: 7, scheduled: 8, preparing: 9, waiting_for_resource: 10, - waiting_for_callback: 11 }.freeze + waiting_for_callback: 11, canceling: 12 }.freeze UnknownStatusError = Class.new(StandardError) @@ -69,6 +69,7 @@ def stopped_statuses state :failed, value: 'failed' state :success, value: 'success' state :canceled, value: 'canceled' + state :canceling, value: 'canceling' state :skipped, value: 'skipped' state :manual, value: 'manual' state :scheduled, value: 'scheduled' @@ -83,6 +84,7 @@ def stopped_statuses scope :pending, -> { with_status(:pending) } scope :success, -> { with_status(:success) } scope :failed, -> { with_status(:failed) } + scope :canceling, -> { with_status(:canceling) } scope :canceled, -> { with_status(:canceled) } scope :skipped, -> { with_status(:skipped) } scope :manual, -> { with_status(:manual) } @@ -92,7 +94,7 @@ def stopped_statuses scope :created_or_pending, -> { with_status(:created, :pending) } scope :running_or_pending, -> { with_status(:running, :pending) } scope :finished, -> { with_status(:success, :failed, :canceled) } - scope :failed_or_canceled, -> { with_status(:failed, :canceled) } + scope :failed_or_canceled, -> { with_status(:failed, :canceled, :canceling) } scope :complete, -> { with_status(completed_statuses) } scope :incomplete, -> { without_statuses(completed_statuses) } scope :waiting_for_resource_or_upcoming, -> { with_status(:created, :scheduled, :waiting_for_resource) } diff --git a/config/feature_flags/wip/ci_canceling_status.yml b/config/feature_flags/wip/ci_canceling_status.yml new file mode 100644 index 0000000000000000000000000000000000000000..41e0c95b5e566af031329c74185ebcca7695409d --- /dev/null +++ b/config/feature_flags/wip/ci_canceling_status.yml @@ -0,0 +1,9 @@ +--- +name: ci_canceling_status +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/399215 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140522 +rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17520 +milestone: '16.10' +group: group::pipeline execution +type: wip +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index dae84988792e5a00a2af83f0943297498adeeb98..b4c29f41922d493deee38303d552908ba2d7435d 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -24776,7 +24776,7 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction). | <a id="pipelinesourcejob"></a>`sourceJob` | [`CiJob`](#cijob) | Job where pipeline was triggered from. | | <a id="pipelinestages"></a>`stages` | [`CiStageConnection`](#cistageconnection) | Stages of the pipeline. (see [Connections](#connections)) | | <a id="pipelinestartedat"></a>`startedAt` | [`Time`](#time) | Timestamp when the pipeline was started. | -| <a id="pipelinestatus"></a>`status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, WAITING_FOR_CALLBACK, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED). | +| <a id="pipelinestatus"></a>`status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, WAITING_FOR_CALLBACK, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, CANCELING, SKIPPED, MANUAL, SCHEDULED). | | <a id="pipelinestuck"></a>`stuck` | [`Boolean!`](#boolean) | If the pipeline is stuck. | | <a id="pipelinetestreportsummary"></a>`testReportSummary` | [`TestReportSummary!`](#testreportsummary) | Summary of the test report generated by the pipeline. | | <a id="pipelinetotaljobs"></a>`totalJobs` | [`Int!`](#int) | The total number of jobs in the pipeline. | @@ -30725,6 +30725,7 @@ Values for sorting inherited variables. | Value | Description | | ----- | ----------- | | <a id="cijobstatuscanceled"></a>`CANCELED` | A job that is canceled. | +| <a id="cijobstatuscanceling"></a>`CANCELING` | A job that is canceling. | | <a id="cijobstatuscreated"></a>`CREATED` | A job that is created. | | <a id="cijobstatusfailed"></a>`FAILED` | A job that is failed. | | <a id="cijobstatusmanual"></a>`MANUAL` | A job that is manual. | @@ -32402,6 +32403,7 @@ Pipeline security report finding sort values. | Value | Description | | ----- | ----------- | | <a id="pipelinestatusenumcanceled"></a>`CANCELED` | Pipeline was canceled before completion. | +| <a id="pipelinestatusenumcanceling"></a>`CANCELING` | Pipeline is in the process of canceling. | | <a id="pipelinestatusenumcreated"></a>`CREATED` | Pipeline has been created. | | <a id="pipelinestatusenumfailed"></a>`FAILED` | At least one stage of the pipeline failed. | | <a id="pipelinestatusenummanual"></a>`MANUAL` | Pipeline needs to be manually started. | diff --git a/ee/app/models/ee/ci/bridge.rb b/ee/app/models/ee/ci/bridge.rb index 6d85b5f5576c2d51866cb41cc3e9b70b92328f36..8bc88610fba2a569bd7ed4d51686922f64005e72 100644 --- a/ee/app/models/ee/ci/bridge.rb +++ b/ee/app/models/ee/ci/bridge.rb @@ -36,6 +36,8 @@ def inherit_status_from_upstream! self.success! when 'failed' self.drop! + when 'canceling' + self.canceling! when 'canceled' self.cancel! when 'skipped' diff --git a/ee/spec/models/ci/build_spec.rb b/ee/spec/models/ci/build_spec.rb index 8a26075b84979b1241f4950dd8e96d3ac774719f..3b546ebf144ac6f070eb452b3cc3a0ef35cf9c16 100644 --- a/ee/spec/models/ci/build_spec.rb +++ b/ee/spec/models/ci/build_spec.rb @@ -96,12 +96,31 @@ describe 'updates pipeline minutes' do let(:job) { create(:ci_build, :running, pipeline: pipeline) } - %w[success drop cancel].each do |event| - it "for event #{event}" do - expect(Ci::Minutes::UpdateBuildMinutesService) - .to receive(:new).and_call_original + context 'when ci_canceling_status is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + %w[success drop cancel].each do |event| + it "for event #{event}" do + expect(Ci::Minutes::UpdateBuildMinutesService) + .to receive(:new).and_call_original - job.public_send(event) + job.public_send(event) + end + end + end + + # TODO: ensure minutes are still tracked when set to + # canceled but not when transitioning to canceling + context 'when ci_canceling_status is enabled' do + %w[success drop].each do |event| + it "for event #{event}" do + expect(Ci::Minutes::UpdateBuildMinutesService) + .to receive(:new).and_call_original + + job.public_send(event) + end end end end diff --git a/ee/spec/models/merge_trains/car_spec.rb b/ee/spec/models/merge_trains/car_spec.rb index 2b08866a2dbe3c04ec42ea6deee61a6658bfb9fd..50646b483d2da9df135ea04979130b2be2c4df84 100644 --- a/ee/spec/models/merge_trains/car_spec.rb +++ b/ee/spec/models/merge_trains/car_spec.rb @@ -450,10 +450,20 @@ context 'when merge train has a pipeline' do let(:train_car) { create(:merge_train_car, pipeline: pipeline) } let(:pipeline) { create(:ci_pipeline, :running) } - let(:build) { create(:ci_build, :running, pipeline: pipeline) } + let(:job) { create(:ci_build, :running, pipeline: pipeline) } - it 'cancels the jobs in the pipeline' do - expect { subject }.to change { build.reload.status }.from('running').to('canceled') + context 'when canceling is not supported' do + it 'cancels the jobs in the pipeline' do + expect { subject }.to change { job.reload.status }.from('running').to('canceled') + end + end + + context 'when canceling is supported' do + include_context 'when canceling support' + + it 'cancels the jobs in the pipeline' do + expect { subject }.to change { job.reload.status }.from('running').to('canceling') + end end end end diff --git a/ee/spec/services/auto_merge/merge_train_service_spec.rb b/ee/spec/services/auto_merge/merge_train_service_spec.rb index bd9d3dc356ffd3bf6aef68b555f48ca16586551f..0f2182104176554fa3b88bd311475522ca43962e 100644 --- a/ee/spec/services/auto_merge/merge_train_service_spec.rb +++ b/ee/spec/services/auto_merge/merge_train_service_spec.rb @@ -171,10 +171,28 @@ end let(:pipeline) { create(:ci_pipeline) } - let(:build) { create(:ci_build, :running, pipeline: pipeline) } + let(:job) { create(:ci_build, :running, pipeline: pipeline) } - it 'cancels the jobs in the pipeline' do - expect { subject }.to change { build.reload.status }.from('running').to('canceled') + context 'when ci_canceling_status is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'cancels the jobs in the pipeline' do + expect { subject }.to change { job.reload.status }.from('running').to('canceled') + end + end + + it 'sets the job to a canceled status' do + expect { subject }.to change { job.reload.status }.from('running').to('canceled') + end + + context 'when canceling is supported' do + include_context 'when canceling support' + + it 'sets the job to a canceling status' do + expect { subject }.to change { job.reload.status }.from('running').to('canceling') + end end end diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb index 02a0e6bd7221608af12641ead8ae8bd1df9c20ef..0255a457223c91c8c3caef065fd93a4ecca04c2e 100644 --- a/lib/api/ci/helpers/runner.rb +++ b/lib/api/ci/helpers/runner.rb @@ -82,7 +82,7 @@ def authenticate_job!(heartbeat_runner: false) forbidden!('Project has been deleted!') if job.project.nil? || job.project.pending_delete? forbidden!('Job has been erased!') if job.erased? - job_forbidden!(job, 'Job is not running') unless job.running? + job_forbidden!(job, 'Job is not processing on runner') unless processing_on_runner?(job) # Only some requests (like updating the job or patching the trace) should trigger # runner heartbeat. Operations like artifacts uploading are executed in context of @@ -152,6 +152,10 @@ def check_if_backoff_required! private + def processing_on_runner?(job) + job.running? || job.canceling? + end + def get_runner_config_from_request { config: attributes_for_keys(%w[gpus], params.dig('info', 'config')) } end diff --git a/lib/gitlab/ci/status/build/canceling.rb b/lib/gitlab/ci/status/build/canceling.rb new file mode 100644 index 0000000000000000000000000000000000000000..4936445cf55308a1bf3be487315ec95e9e1d8b2c --- /dev/null +++ b/lib/gitlab/ci/status/build/canceling.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + module Build + class Canceling < Status::Extended + def illustration + { + image: 'illustrations/canceled-job_empty.svg', + size: '', + title: _('This job is in the process of canceling') + } + end + + def self.matches?(build, _user) + build.canceling? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 54f6784b847b6003df646ad74a8baf8d281cfab3..5272421a97939865d93bc02c4164427ed441421d 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -9,6 +9,7 @@ def self.extended_statuses [[Status::Build::Erased, Status::Build::Scheduled, Status::Build::Manual, + Status::Build::Canceling, Status::Build::Canceled, Status::Build::Created, Status::Build::Preparing, diff --git a/lib/gitlab/ci/status/canceling.rb b/lib/gitlab/ci/status/canceling.rb new file mode 100644 index 0000000000000000000000000000000000000000..586bac0310ea0bb89f59343f8f40e669d0dd707b --- /dev/null +++ b/lib/gitlab/ci/status/canceling.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + class Canceling < Status::Core + def text + s_('CiStatusText|Canceling') + end + + def label + s_('CiStatusLabel|canceling') + end + + def icon + 'status_canceled' + end + + def favicon + 'favicon_status_canceled' + end + + def details_path + nil + end + end + end + end +end diff --git a/lib/gitlab/ci/status/composite.rb b/lib/gitlab/ci/status/composite.rb index fe4f6db95490834db57c050a203d2e695e69c4bd..c3882c69cc8b5050234dec3aae98e8c4e85e7135 100644 --- a/lib/gitlab/ci/status/composite.rb +++ b/lib/gitlab/ci/status/composite.rb @@ -71,6 +71,8 @@ def status 'preparing' elsif any_of?(:created) 'running' + elsif any_of?(:canceling) + 'canceling' else 'failed' end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5433d9dc4808d37ad8331216da661e363fa4e30f..f3d3220f70ba6221bfa4fe7cbefa50f35bbf6de5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10605,6 +10605,9 @@ msgstr "" msgid "CiStatusLabel|canceled" msgstr "" +msgid "CiStatusLabel|canceling" +msgstr "" + msgid "CiStatusLabel|created" msgstr "" @@ -10653,6 +10656,9 @@ msgstr "" msgid "CiStatusText|Canceled" msgstr "" +msgid "CiStatusText|Canceling" +msgstr "" + msgid "CiStatusText|Created" msgstr "" @@ -51195,6 +51201,9 @@ msgstr "" msgid "This job is in pending state and is waiting to be picked by a runner" msgstr "" +msgid "This job is in the process of canceling" +msgstr "" + msgid "This job is performing tasks that must complete before it can start" msgstr "" diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 82c1aa3e18c7d39d37a92af3a23c19413264f7e3..cbc854c7856b833d649afede9fce74c431ff6dea 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -1014,24 +1014,55 @@ def post_request describe 'POST cancel.json' do let!(:pipeline) { create(:ci_pipeline, project: project) } - let!(:build) { create(:ci_build, :running, pipeline: pipeline) } + let!(:job) { create(:ci_build, :running, pipeline: pipeline) } - before do + subject do post :cancel, params: { namespace_id: project.namespace, project_id: project, id: pipeline.id }, format: :json end - it 'cancels a pipeline without returning any content', :sidekiq_might_not_need_inline do - expect(response).to have_gitlab_http_status(:no_content) - expect(pipeline.reload).to be_canceled + context 'when supports canceling is true' do + include_context 'when canceling support' + + it 'sets a pipeline status to canceling', :sidekiq_inline do + subject + + expect(pipeline.reload).to be_canceling + end + + it 'returns a no content http status' do + subject + + expect(response).to have_gitlab_http_status(:no_content) + end end - context 'when builds are disabled' do - let(:feature) { ProjectFeature::DISABLED } + context 'when supports canceling is false' do + before do + allow(job).to receive(:supports_canceling?).and_return(false) + end - it 'fails to retry pipeline' do - expect(response).to have_gitlab_http_status(:not_found) + it 'sets a pipeline status to canceled', :sidekiq_inline do + subject + + expect(pipeline.reload).to be_canceled + end + + it 'returns a no content http status' do + subject + + expect(response).to have_gitlab_http_status(:no_content) + end + + context 'when builds are disabled' do + let(:feature) { ProjectFeature::DISABLED } + + it 'fails to retry pipeline' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 3bfd15b56e02f9ded1c88204e31778014a48d625..0556b42aa97e21fd3fc6f132e35286c6165b527d 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -102,6 +102,11 @@ status { 'failed' } end + trait :canceling do + started + status { 'canceling' } + end + trait :canceled do finished status { 'canceled' } diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb index 545bfee4910db42eb6259477924084f54680dcb7..bfa845906d8c10918a5ba213021e949985847fce 100644 --- a/spec/features/projects/jobs/user_browses_jobs_spec.rb +++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb @@ -69,13 +69,32 @@ def visit_jobs_page visit_jobs_page end - it 'cancels a job successfully' do - find_by_testid('cancel-button').click + context 'when supports canceling is true' do + include_context 'when canceling support' - wait_for_requests + it 'cancels a job successfully' do + find_by_testid('cancel-button').click + + wait_for_requests + + expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceling') + expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]') + end + end - expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled') - expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]') + context 'when supports canceling is false' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'cancels a job successfully' do + find_by_testid('cancel-button').click + + wait_for_requests + + expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled') + expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]') + end end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 7de9b3ef7dba46547334e1fd4bffc5865d2fa1fa..4e41fff733d3492355262630569ac4df311640c1 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -193,11 +193,25 @@ end end - it 'cancels the preparing build and shows retry button', :sidekiq_might_not_need_inline do + it 'does not show the retry button' do find('#ci-badge-deploy .ci-action-icon-container').click page.within('#ci-badge-deploy') do - expect(page).to have_css('.js-icon-retry') + expect(page).not_to have_css('.js-icon-retry') + end + end + + context 'when ci_canceling_status is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'shows retry button', :sidekiq_inline do + find('#ci-badge-deploy .ci-action-icon-container').click + + page.within('#ci-badge-deploy') do + expect(page).to have_css('.js-icon-retry') + end end end end @@ -371,15 +385,25 @@ expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]') end - context 'when canceling', :sidekiq_inline do + context 'when cancel button clicked', :sidekiq_inline do before do find('button[aria-label="Cancel downstream pipeline"]').click - wait_for_requests end - it 'shows the pipeline as canceled with the retry action' do - expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]') + it 'shows the pipeline as canceling with the retry action' do expect(page).to have_selector('[data-testid="status_canceled_borderless-icon"]') + expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]') + end + + context 'when ci_canceling_status is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'shows the pipeline as canceled with the retry action' do + expect(page).to have_selector('[data-testid="status_canceled_borderless-icon"]') + expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]') + end end end end @@ -387,19 +411,25 @@ context 'with a failed downstream' do let(:status) { :failed } - it 'indicates that pipeline can be retried' do - expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]') - end - - context 'when retrying' do + context 'when ci_canceling_status is disabled' do before do - find('button[aria-label="Retry downstream pipeline"]').click - wait_for_requests + stub_feature_flags(ci_canceling_status: false) end - it 'shows running pipeline with the cancel action' do - expect(page).to have_selector('[data-testid="status_running_borderless-icon"]') - expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]') + it 'indicates that pipeline can be retried' do + expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]') + end + + context 'when retrying' do + before do + find('button[aria-label="Retry downstream pipeline"]').click + wait_for_requests + end + + it 'shows running pipeline with the cancel action' do + expect(page).to have_selector('[data-testid="status_running_borderless-icon"]') + expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]') + end end end end @@ -546,16 +576,19 @@ context 'canceling jobs' do before do visit_pipeline + click_on 'Cancel pipeline' end - it { expect(page).not_to have_selector('.ci-canceled') } + it 'does not show a "Cancel pipeline" button', :sidekiq_inline do + expect(page).not_to have_content('Cancel pipeline') + end - context 'when canceling' do + context 'when ci_canceling_status disabled' do before do - click_on 'Cancel pipeline' + stub_feature_flags(ci_canceling_status: false) end - it 'does not show a "Cancel pipeline" button', :sidekiq_might_not_need_inline do + it 'does not show a "Cancel pipeline" button', :sidekiq_inline do expect(page).not_to have_content('Cancel pipeline') end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 629185e3ba180a63e2500e5f9fac3ed802354eeb..b9c43eb8445035e5052c73f3ec5e45b26a09f72c 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -104,30 +104,58 @@ end context 'when pipeline is cancelable' do - let!(:build) do + let!(:job) do create(:ci_build, pipeline: pipeline, stage: 'test') end before do - build.run + job.run visit_project_pipelines end - it 'indicates that pipeline can be canceled' do - expect(page).to have_selector('.js-pipelines-cancel-button') - expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running') + context 'when canceling support is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'indicates that pipeline can be canceled' do + expect(page).to have_selector('.js-pipelines-cancel-button') + expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running') + end + + context 'when canceling' do + before do + find('.js-pipelines-cancel-button').click + click_button 'Stop pipeline' + wait_for_requests + end + + it 'indicates that pipelines was canceled', :sidekiq_inline do + expect(page).not_to have_selector('.js-pipelines-cancel-button') + expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled') + end + end end - context 'when canceling' do - before do - find('.js-pipelines-cancel-button').click - click_button 'Stop pipeline' - wait_for_requests + context 'when canceling support is enabled' do + include_context 'when canceling support' + + it 'indicates that pipeline can be canceled' do + expect(page).to have_selector('.js-pipelines-cancel-button') + expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running') end - it 'indicated that pipelines was canceled', :sidekiq_might_not_need_inline do - expect(page).not_to have_selector('.js-pipelines-cancel-button') - expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceled') + context 'when canceling' do + before do + find('.js-pipelines-cancel-button').click + click_button 'Stop pipeline' + wait_for_requests + end + + it 'indicates that pipeline is canceling', :sidekiq_inline do + expect(page).not_to have_selector('.js-pipelines-cancel-button') + expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Canceling') + end end end end diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb index 1394f536c7264e9776244d97f3774b8b093bf4c2..315df38bba703ae6151a78829a8d1717819b24f3 100644 --- a/spec/helpers/ci/jobs_helper_spec.rb +++ b/spec/helpers/ci/jobs_helper_spec.rb @@ -34,6 +34,7 @@ it 'returns job statuses' do expect(helper.job_statuses).to eq({ + "canceling" => "CANCELING", "canceled" => "CANCELED", "created" => "CREATED", "failed" => "FAILED", diff --git a/spec/lib/gitlab/ci/status/build/canceled_spec.rb b/spec/lib/gitlab/ci/status/build/canceled_spec.rb index 519b970ca5e9212a25d1777db6685122b0e74631..30ef586db8dd96e24af9da8044067c6b334c4d6b 100644 --- a/spec/lib/gitlab/ci/status/build/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/build/canceled_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Build::Canceled do - let(:user) { create(:user) } +RSpec.describe Gitlab::Ci::Status::Build::Canceled, feature_category: :continuous_integration do + let(:user) { build_stubbed(:user) } subject do described_class.new(double('subject')) @@ -17,7 +17,7 @@ subject { described_class.matches?(build, user) } context 'when build is canceled' do - let(:build) { create(:ci_build, :canceled) } + let(:build) { build_stubbed(:ci_build, :canceled) } it 'is a correct match' do expect(subject).to be true @@ -25,7 +25,7 @@ end context 'when build is not canceled' do - let(:build) { create(:ci_build) } + let(:build) { build_stubbed(:ci_build) } it 'does not match' do expect(subject).to be false diff --git a/spec/lib/gitlab/ci/status/build/canceling_spec.rb b/spec/lib/gitlab/ci/status/build/canceling_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..617a1a0ccd0396c64aca79fd369fd9021abe46db --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/canceling_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Status::Build::Canceling, feature_category: :continuous_integration do + let(:user) { build_stubbed(:user) } + + subject(:status_instance) do + described_class.new(double) + end + + describe '#illustration' do + it { expect(status_instance.illustration).to include(:image, :size, :title) } + end + + describe '.matches?' do + subject(:matches?) { described_class.matches?(build, user) } + + context 'when build is canceled' do + let(:build) { build_stubbed(:ci_build, :canceling) } + + it 'is a correct match' do + expect(matches?).to be true + end + end + + context 'when build is not canceled' do + let(:build) { build_stubbed(:ci_build) } + + it 'does not match' do + expect(matches?).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index ddb8b7ecff96f559391f6386edf13cfde3636679..868260ab6fa1722ca346cee19b36edb031d43df7 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Canceled do +RSpec.describe Gitlab::Ci::Status::Canceled, feature_category: :continuous_integration do subject do described_class.new(double('subject'), double('user')) end diff --git a/spec/lib/gitlab/ci/status/canceling_spec.rb b/spec/lib/gitlab/ci/status/canceling_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..577ec72cfa0f67196fd1bf11e659d3781513a7e8 --- /dev/null +++ b/spec/lib/gitlab/ci/status/canceling_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Status::Canceling, feature_category: :continuous_integration do + subject(:status) do + described_class.new(double, double) + end + + describe '#text' do + it { expect(status.text).to eq 'Canceling' } + end + + describe '#label' do + it { expect(status.label).to eq 'canceling' } + end + + describe '#icon' do + it { expect(status.icon).to eq 'status_canceled' } + end + + describe '#favicon' do + it { expect(status.favicon).to eq 'favicon_status_canceled' } + end + + describe '#group' do + it { expect(status.group).to eq 'canceling' } + end + + describe '#details_path' do + it { expect(status.details_path).to be_nil } + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7d1199dfc258e957a87e9b474594f1d2b2733e39..f560b7e5a9d86dac8abebe0bb6cb3f4ec591a161 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -5492,6 +5492,42 @@ def run_job_without_exception end end + describe '#supports_canceling?' do + let(:job) { create(:ci_build, :running, project: project) } + + context 'when the builds runner does not support canceling' do + specify { expect(job.supports_canceling?).to be false } + + context 'when the ci_canceling_status flag is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'returns false' do + expect(job.supports_canceling?).to be false + end + end + end + + context 'when the builds runner supports canceling' do + include_context 'when canceling support' + + it 'returns true' do + expect(job.supports_canceling?).to be true + end + + context 'when the ci_canceling_status flag is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'returns false' do + expect(job.supports_canceling?).to be false + end + end + end + end + describe '#runtime_runner_features' do subject do build.save! diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5b1754d8946637c1c14ca565906a4db0b5394def..d310ff6c2ef9335dde4db353410ac9c0eaf44d8d 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -283,6 +283,12 @@ describe '#set_status' do let(:pipeline) { build(:ci_empty_pipeline, :created) } + let(:not_transitionable) do + [ + { from_status: :canceled, to_status: :canceling } + ] + end + where(:from_status, :to_status) do from_status_names = described_class.state_machines[:status].states.map(&:name) to_status_names = from_status_names - [:created] # we never want to transition into created @@ -294,12 +300,12 @@ it do pipeline.status = from_status.to_s - if from_status != to_status || success_to_success? + if (from_status != to_status || success_to_success?) && transitionable?(from_status, to_status) expect(pipeline.set_status(to_status.to_s)) .to eq(true) else expect(pipeline.set_status(to_status.to_s)) - .to eq(false), "loopback transitions are not allowed" + .to eq(false), 'loopback transitions are not allowed' end end @@ -308,6 +314,14 @@ def success_to_success? from_status == :success && to_status == :success end + + def transitionable?(from, to) + not_transitionable.each do |exclusion| + return false if from.to_sym == exclusion[:from_status].to_sym && to.to_sym == exclusion[:to_status].to_sym + end + + true + end end end @@ -1470,6 +1484,12 @@ def create_build(name, status) let(:build_b) { create_build('build2', queued_at: 0) } let(:build_c) { create_build('build3', queued_at: 0) } + describe '#canceling' do + it 'transitions to canceling' do + expect { pipeline.canceling }.to change { pipeline.status }.from('created').to('canceling') + end + end + %w[succeed! drop! cancel! skip! block! delay!].each do |action| context "when the pipeline received #{action} event" do it 'deletes a persistent ref asynchronously' do @@ -2940,7 +2960,7 @@ def create_pipeline(status, ref, sha) subject { described_class.bridgeable_statuses } it { is_expected.to be_an(Array) } - it { is_expected.to contain_exactly('running', 'success', 'failed', 'canceled', 'skipped', 'manual', 'scheduled') } + it { is_expected.to contain_exactly('running', 'success', 'failed', 'canceling', 'canceled', 'skipped', 'manual', 'scheduled') } end describe '#status', :sidekiq_inline do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 4951f57fe6fb8616591620e30d6d14515ca0a7e2..c214d6a88c3d4d2ac7c8173c81dd9b0be8955472 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -89,11 +89,17 @@ from_status_names.product(to_status_names) end + let(:not_transitionable) do + [ + { from_status: :canceled, to_status: :canceling } + ] + end + with_them do it do stage.status = from_status.to_s - if from_status != to_status + if from_status != to_status && transitionable?(from_status, to_status) expect(stage.set_status(to_status.to_s)) .to eq(true) else @@ -102,6 +108,24 @@ end end end + + def transitionable?(from, to) + not_transitionable.each do |exclusion| + return false if from.to_sym == exclusion[:from_status].to_sym && to.to_sym == exclusion[:to_status].to_sym + end + + true + end + end + + describe '#canceling' do + it 'transitions to canceling' do + stage = create(:ci_stage, pipeline: pipeline, project: pipeline.project, status: 'running') + create(:ci_build, :success, stage_id: stage.id) + create(:ci_build, :running, stage_id: stage.id) + + expect { stage.canceling }.to change { stage.status }.from('running').to('canceling') + end end describe '#update_status' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 7c4917596a0d03fd0108c00f0d0edef0b6c8512f..f8573d4a56291fbbe6a751cee5de2b26769837e6 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -54,6 +54,24 @@ def create_status(**opts) it { is_expected.to eq(commit_status.user) } end + describe '#success' do + it 'transitions canceling to canceled' do + commit_status = create_status(stage: 'test', status: 'canceling') + + expect { commit_status.success! }.to change { commit_status.status }.from('canceling').to('canceled') + end + + context 'when status is one that transitions to success' do + [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running].each do |status| + it 'transitions to success' do + commit_status = create_status(stage: 'test', status: status.to_s) + + expect { commit_status.success! }.to change { commit_status.status }.from(status.to_s).to('success') + end + end + end + end + describe 'status state machine' do let!(:commit_status) { create(:commit_status, :running, project: project) } @@ -795,6 +813,23 @@ def create_status(**opts) end end end + + it 'transitions canceling to canceled' do + commit_status = create_status(stage: 'test', status: 'canceling') + + expect { commit_status.drop! }.to change { commit_status.status }.from('canceling').to('canceled') + end + + context 'when status is one that transitions to success' do + [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, +:scheduled].each do |status| + it 'transitions to success' do + commit_status = create_status(stage: 'test', status: status.to_s) + + expect { commit_status.drop! }.to change { commit_status.status }.from(status.to_s).to('failed') + end + end + end end describe 'ensure stage assignment' do diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index ef169dbe872406bf35c07fbad0d1dc95b7011e31..ee39a9eb2e6bef18a57efab4d5eadc64b45aff7d 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -1256,14 +1256,31 @@ def expect_variables(variables, expected_variables) create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) end - let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) } + let_it_be(:job) { create(:ci_build, :running, pipeline: pipeline) } context 'authorized user', :aggregate_failures do - it 'retries failed builds', :sidekiq_might_not_need_inline do - post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) + context 'when supports canceling is true' do + include_context 'when canceling support' - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['status']).to eq('canceled') + it 'cancels builds', :sidekiq_inline do + post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['status']).to eq('canceling') + end + + context 'when ci_canceling_status is disabled' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'cancels builds', :sidekiq_inline do + post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['status']).to eq('canceled') + end + end end end diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb index 8c596d2338f0256ea6bda9e7a3e4a9cfd4bc6c97..5c9166f944e970548d8ed4fb53c297f1f3c6f678 100644 --- a/spec/requests/api/ci/runner/jobs_trace_spec.rb +++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb @@ -142,17 +142,35 @@ expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' end - context 'when job is cancelled' do + context 'when canceling is supported' do + include_context 'when canceling support' + + context 'when job is cancelled' do + before do + job.cancel + end + + it 'patching the trace is allowed' do + patch_the_trace + + expect(response).to have_gitlab_http_status(:accepted) + end + end + end + + context 'when canceling is not supported' do before do - job.cancel + stub_feature_flags(ci_canceling_status: false) end - context 'when trace is patched' do + context 'when job is canceled' do before do - patch_the_trace + job.cancel end - it 'returns Forbidden' do + it 'patching the trace returns forbidden' do + patch_the_trace + expect(response).to have_gitlab_http_status(:forbidden) end end @@ -203,13 +221,26 @@ end end - context 'when the job is canceled' do - before do + context 'when canceling is supported' do + include_context 'when canceling support' + + it 'receives status in header' do job.cancel patch_the_trace + + expect(response.header['Job-Status']).to eq 'canceling' + end + end + + context 'when canceling is not supported' do + before do + stub_feature_flags(ci_canceling_status: false) end it 'receives status in header' do + job.cancel + patch_the_trace + expect(response.header['Job-Status']).to eq 'canceled' end end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb index 8c1359384ed5a703e8099d630b068b071bdd05eb..be619394b6f79fc10b5c1d2a11f83beaab219859 100644 --- a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb @@ -40,13 +40,33 @@ expect(build).not_to be_canceled end - it 'cancels all cancelable builds from a pipeline', :sidekiq_inline do - build = create(:ci_build, :running, pipeline: pipeline) + context 'when running build' do + let!(:job) { create(:ci_build, :running, pipeline: pipeline) } - post_graphql_mutation(mutation, current_user: user) + context 'when supports canceling is true' do + include_context 'when canceling support' + + it 'transitions all running jobs to canceling', :sidekiq_inline do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(job.reload).to be_canceling + expect(pipeline.reload).to be_canceling + end + end + + context 'when supports canceling is false' do + before do + stub_feature_flags(ci_canceling_status: false) + end + + it 'cancels all running jobs to canceled', :sidekiq_inline do + post_graphql_mutation(mutation, current_user: user) - expect(response).to have_gitlab_http_status(:success) - expect(build.reload).to be_canceled - expect(pipeline.reload).to be_canceled + expect(response).to have_gitlab_http_status(:success) + expect(job.reload).to be_canceled + expect(pipeline.reload).to be_canceled + end + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 8e4e90ae9625d3298233c86e382456044fdef0c2..33c4e6a88d5550321e147320bbb2738a92dd2385 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -306,7 +306,7 @@ def execute_service( context 'when only interruptible builds are running' do context 'when build marked explicitly by interruptible is running' do - it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do + it 'cancels running outdated pipelines', :sidekiq_inline do pipeline_on_previous_commit .builds .find_by_name('build_1_2') @@ -320,7 +320,7 @@ def execute_service( end context 'when build that is not marked as interruptible is running' do - it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do + it 'cancels running outdated pipelines', :sidekiq_inline do build_2_1 = pipeline_on_previous_commit .builds.find_by_name('build_2_1') diff --git a/spec/support/shared_examples/ci/jobs_shared_examples.rb b/spec/support/shared_examples/ci/jobs_shared_examples.rb index d952d4a98eb7fcf261bce86eb7a21622195f5840..a9affa772852efbb53254e5293bf012f208a8064 100644 --- a/spec/support/shared_examples/ci/jobs_shared_examples.rb +++ b/spec/support/shared_examples/ci/jobs_shared_examples.rb @@ -25,3 +25,10 @@ end end end + +RSpec.shared_context 'when canceling support' do + before do + job.metadata.set_cancel_gracefully + job.save! + end +end diff --git a/spec/workers/ci/cancel_pipeline_worker_spec.rb b/spec/workers/ci/cancel_pipeline_worker_spec.rb index 8e8f9a78132939c17d68c85d93d3b1d2547a54c7..8ffbd8913bbdfa0735dbdf17340daf59dd884627 100644 --- a/spec/workers/ci/cancel_pipeline_worker_spec.rb +++ b/spec/workers/ci/cancel_pipeline_worker_spec.rb @@ -54,7 +54,9 @@ end describe 'with builds and state transition side effects', :sidekiq_inline do - let!(:build) { create(:ci_build, :running, pipeline: pipeline) } + let!(:job) { create(:ci_build, :running, pipeline: pipeline) } + + include_context 'when canceling support' it_behaves_like 'an idempotent worker', :sidekiq_inline do let(:job_args) { [pipeline.id, pipeline.id] } @@ -64,8 +66,8 @@ pipeline.reload - expect(pipeline).to be_canceled - expect(pipeline.builds.first).to be_canceled + expect(pipeline).to be_canceling + expect(pipeline.builds.first).to be_canceling expect(pipeline.builds.first.auto_canceled_by_id).to eq pipeline.id expect(pipeline.auto_canceled_by_id).to eq pipeline.id end