From 151dc7d7180aea44202154ecaadc5f14a790aef6 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Mon, 11 Dec 2023 11:32:24 +0300 Subject: [PATCH 1/4] Add auto_cancel:on_new_commit to canceling redundant pipelines This is the fourth step of full implementation of workflow:auto_cancel:on_new_commit and using it in the canceling redundant pipelines feature. In this change, we are using auto_cancel_on_new_commit in CancelRedundantPipelinesService and adding safe_cancellation to CancelPipelineService. These changes are behind the FF ci_workflow_auto_cancel_on_new_commit. --- app/assets/javascripts/editor/schema/ci.json | 12 +- app/models/ci/pipeline.rb | 4 +- app/models/ci/processable.rb | 4 + app/services/ci/cancel_pipeline_service.rb | 27 +++- .../cancel_redundant_pipelines_service.rb | 72 ++++++--- .../ci_workflow_auto_cancel_on_new_commit.yml | 8 + doc/ci/yaml/index.md | 43 ++++++ .../editor/schema/ci/ci_schema_spec.js | 14 +- .../auto_cancel/on_job_failure.yml} | 1 - .../workflow/auto_cancel/on_new_commit.yml | 3 + .../on_job_failure/none.yml | 4 - .../auto_cancel/on_job_failure.yml} | 1 - .../workflow/auto_cancel/on_new_commit.yml | 3 + .../ci/cancel_pipeline_service_spec.rb | 76 ++++++++-- ...cancel_redundant_pipelines_service_spec.rb | 137 +++++++++++++++++- 15 files changed, 356 insertions(+), 53 deletions(-) create mode 100644 config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml rename spec/frontend/editor/schema/ci/yaml_tests/negative_tests/{auto_cancel_pipeline.yml => workflow/auto_cancel/on_job_failure.yml} (57%) create mode 100644 spec/frontend/editor/schema/ci/yaml_tests/negative_tests/workflow/auto_cancel/on_new_commit.yml delete mode 100644 spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml rename spec/frontend/editor/schema/ci/yaml_tests/positive_tests/{auto_cancel_pipeline/on_job_failure/all.yml => workflow/auto_cancel/on_job_failure.yml} (52%) create mode 100644 spec/frontend/editor/schema/ci/yaml_tests/positive_tests/workflow/auto_cancel/on_new_commit.yml diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index 1fb68394912b8c..b8cb7797ad9403 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -944,7 +944,8 @@ }, "workflowAutoCancel": { "type": "object", - "markdownDescription": "Define the rules for when pipeline should be automatically cancelled.", + "description": "Define the rules for when pipeline should be automatically cancelled.", + "additionalProperties": false, "properties": { "on_job_failure": { "markdownDescription": "Define which jobs to stop after a job fails.", @@ -954,6 +955,15 @@ "none", "all" ] + }, + "on_new_commit": { + "markdownDescription": "Configure the behavior of the auto-cancel redundant pipelines feature. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#workflowauto_cancelon_new_commit)", + "type": "string", + "enum": [ + "conservative", + "interruptible", + "disabled" + ] } } }, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f8ccbbe63bd995..911e78c807fd3d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -151,7 +151,7 @@ class Pipeline < Ci::ApplicationRecord accepts_nested_attributes_for :variables, reject_if: :persisted? delegate :full_path, to: :project, prefix: true - delegate :name, :auto_cancel_on_job_failure, to: :pipeline_metadata, allow_nil: true + delegate :name, :auto_cancel_on_job_failure, :auto_cancel_on_new_commit, to: :pipeline_metadata, allow_nil: true validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } @@ -439,7 +439,7 @@ class Pipeline < Ci::ApplicationRecord where_exists(Ci::Build.latest.scoped_pipeline.with_artifacts(reports_scope)) end - scope :with_only_interruptible_builds, -> do + scope :conservative_interruptible, -> do where_not_exists( Ci::Build.scoped_pipeline.with_status(STARTED_STATUSES).not_interruptible ) diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 414d36da7c3171..989d6337ab7600 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -33,6 +33,10 @@ class Processable < ::CommitStatus where('NOT EXISTS (?)', needs) end + scope :interruptible, -> do + joins(:metadata).merge(Ci::BuildMetadata.with_interruptible) + end + scope :not_interruptible, -> do joins(:metadata).where.not( Ci::BuildMetadata.table_name => { id: Ci::BuildMetadata.scoped_build.with_interruptible.select(:id) } diff --git a/app/services/ci/cancel_pipeline_service.rb b/app/services/ci/cancel_pipeline_service.rb index 38053b1392110f..92eead3fdd14c4 100644 --- a/app/services/ci/cancel_pipeline_service.rb +++ b/app/services/ci/cancel_pipeline_service.rb @@ -10,17 +10,20 @@ class CancelPipelineService # @cascade_to_children - if true cancels all related child pipelines for parent child pipelines # @auto_canceled_by_pipeline - store the pipeline_id of the pipeline that triggered cancellation # @execute_async - if true cancel the children asyncronously + # @safe_cancellation - if true only cancel interruptible:true jobs def initialize( pipeline:, current_user:, cascade_to_children: true, auto_canceled_by_pipeline: nil, - execute_async: true) + execute_async: true, + safe_cancellation: false) @pipeline = pipeline @current_user = current_user @cascade_to_children = cascade_to_children @auto_canceled_by_pipeline = auto_canceled_by_pipeline @execute_async = execute_async + @safe_cancellation = safe_cancellation end def execute @@ -42,13 +45,16 @@ def force_execute log_pipeline_being_canceled pipeline.update_column(:auto_canceled_by_id, @auto_canceled_by_pipeline.id) if @auto_canceled_by_pipeline - cancel_jobs(pipeline.cancelable_statuses) - return ServiceResponse.success unless cascade_to_children? + if @safe_cancellation + # Only build and bridge (trigger) jobs can be interruptible. + # We do not cancel GenericCommitStatuses because they can't have the `interruptible` attribute. + cancel_jobs(pipeline.processables.cancelable.interruptible) + else + cancel_jobs(pipeline.cancelable_statuses) + end - # cancel any bridges that could spin up new child pipelines - cancel_jobs(pipeline.bridges_in_self_and_project_descendants.cancelable) - cancel_children + cancel_children if cascade_to_children? ServiceResponse.success end @@ -106,8 +112,15 @@ def permission_error_response ) end - # For parent child-pipelines only (not multi-project) + # We don't handle the case when `cascade_to_children` is `true` and `safe_cancellation` is `true` + # because `safe_cancellation` is passed as `true` only when `cascade_to_children` is `false` + # from `CancelRedundantPipelinesService`. + # In the future, when "safe cancellation" is implemented as a regular cancellation feature, + # we need to handle this case. def cancel_children + cancel_jobs(pipeline.bridges_in_self_and_project_descendants.cancelable) + + # For parent child-pipelines only (not multi-project) pipeline.all_child_pipelines.each do |child_pipeline| if execute_async? ::Ci::CancelPipelineWorker.perform_async( diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb index 224b2d96205362..97e9a45cd5076d 100644 --- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb +++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb @@ -23,7 +23,7 @@ def execute pipelines = parent_and_child_pipelines(ids) Gitlab::OptimisticLocking.retry_lock(pipelines, name: 'cancel_pending_pipelines') do |cancelables| - auto_cancel_interruptible_pipelines(cancelables.ids) + auto_cancel_pipelines(cancelables.ids) end end end @@ -69,31 +69,65 @@ def parent_and_child_pipelines(ids) .base_and_descendants .alive_or_scheduled end - # rubocop: enable CodeReuse/ActiveRecord - def auto_cancel_interruptible_pipelines(pipeline_ids) + def legacy_auto_cancel_pipelines(pipeline_ids) ::Ci::Pipeline .id_in(pipeline_ids) - .with_only_interruptible_builds + .conservative_interruptible .each do |cancelable_pipeline| - Gitlab::AppLogger.info( - class: self.class.name, - message: "Pipeline #{pipeline.id} auto-canceling pipeline #{cancelable_pipeline.id}", - canceled_pipeline_id: cancelable_pipeline.id, - canceled_by_pipeline_id: pipeline.id, - canceled_by_pipeline_source: pipeline.source - ) - - # cascade_to_children not needed because we iterate through descendants here - ::Ci::CancelPipelineService.new( - pipeline: cancelable_pipeline, - current_user: nil, - auto_canceled_by_pipeline: pipeline, - cascade_to_children: false - ).force_execute + log_info(cancelable_pipeline) + cancel_pipeline(cancelable_pipeline, safe_cancellation: false) end end + def auto_cancel_pipelines(pipeline_ids) + if Feature.disabled?(:ci_workflow_auto_cancel_on_new_commit, project) + return legacy_auto_cancel_pipelines(pipeline_ids) + end + + conservative_cancellable_pipeline_ids = ::Ci::Pipeline.id_in(pipeline_ids).conservative_interruptible.ids + + ::Ci::Pipeline + .id_in(pipeline_ids) + .each do |cancelable_pipeline| + case cancelable_pipeline.auto_cancel_on_new_commit + when 'conservative', nil + # 'conservative' and `nil` are the same because `auto_cancel_on_new_commit` returns `nil` + # when the pipeline does not have `pipeline_metadata`. + + next unless conservative_cancellable_pipeline_ids.include?(cancelable_pipeline.id) + + log_info(cancelable_pipeline) + cancel_pipeline(cancelable_pipeline, safe_cancellation: false) + when 'interruptible' + log_info(cancelable_pipeline) + cancel_pipeline(cancelable_pipeline, safe_cancellation: true) + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def log_info(cancelable_pipeline) + Gitlab::AppLogger.info( + class: self.class.name, + message: "Pipeline #{pipeline.id} auto-canceling pipeline #{cancelable_pipeline.id}", + canceled_pipeline_id: cancelable_pipeline.id, + canceled_by_pipeline_id: pipeline.id, + canceled_by_pipeline_source: pipeline.source + ) + end + + def cancel_pipeline(cancelable_pipeline, safe_cancellation:) + # cascade_to_children not needed because we iterate through descendants here + ::Ci::CancelPipelineService.new( + pipeline: cancelable_pipeline, + current_user: nil, + auto_canceled_by_pipeline: pipeline, + cascade_to_children: false, + safe_cancellation: safe_cancellation + ).force_execute + end + def pipelines_created_after 3.days.ago end diff --git a/config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml b/config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml new file mode 100644 index 00000000000000..74b6ad8911e7df --- /dev/null +++ b/config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml @@ -0,0 +1,8 @@ +--- +name: ci_workflow_auto_cancel_on_new_commit +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139358 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434676 +milestone: '16.7' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 2761658a7194a1..30f3980eff11e0 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -480,6 +480,46 @@ You can use some [predefined CI/CD variables](../variables/predefined_variables. - [`workflow: rules` examples](workflow.md#workflow-rules-examples) - [Switch between branch pipelines and merge request pipelines](workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines) +#### `workflow:auto_cancel:on_new_commit` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412473) in GitLab 16.7 [with a flag](../../administration/feature_flags.md) named `ci_workflow_auto_cancel_on_new_commit`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available per project or +for your entire instance, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `ci_workflow_auto_cancel_on_new_commit`. +On GitLab.com, this feature is not available. +The feature is not ready for production use. + +Use `workflow:auto_cancel:on_new_commit` to configure the behavior of +the [auto-cancel redundant pipelines](../pipelines/settings.md#auto-cancel-redundant-pipelines) feature. + +**Possible inputs**: + +- `conservative`: Current (legacy) behavior. +- `interruptible`: Cancel only `interruptible` jobs. +- `disabled`: Disable auto-cancel. + +**Example of `workflow:auto_cancel:on_new_commit`**: + +```yaml +workflow: + auto_cancel: + on_new_commit: interruptible + +job1: + interruptible: true + script: sleep 60 + +job2: + interruptible: false # Default when not defined. + script: sleep 60 +``` + +In this example: + +- When a new commit is pushed to a branch, GitLab creates a new pipeline and `job1` and `job2` start. +- If a new commit is pushed to the branch before the jobs complete, only `job1` is canceled. + #### `workflow:name` > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/372538) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `pipeline_name`. Disabled by default. @@ -2687,6 +2727,9 @@ In this example, a new pipeline causes a running pipeline to be: cancelled. After a user starts the job, the pipeline cannot be canceled by the **Auto-cancel redundant pipelines** feature. +TODO: add information for trigger jobs; https://gitlab.com/gitlab-org/gitlab/-/issues/412473#note_1685775974 +TODO: restructure here with workflow:auto_cancel:on_new_commit + ### `needs` > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/47063) in GitLab 12.2. diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index 7986509074e78b..949cf1367ff0f2 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -38,8 +38,8 @@ import SecretsYaml from './yaml_tests/positive_tests/secrets.yml'; import ServicesYaml from './yaml_tests/positive_tests/services.yml'; import NeedsParallelMatrixYaml from './yaml_tests/positive_tests/needs_parallel_matrix.yml'; import ScriptYaml from './yaml_tests/positive_tests/script.yml'; -import AutoCancelPipelineOnJobFailureAllYaml from './yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml'; -import AutoCancelPipelineOnJobFailureNoneYaml from './yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml'; +import WorkflowAutoCancelOnJobFailureYaml from './yaml_tests/positive_tests/workflow/auto_cancel/on_job_failure.yml'; +import WorkflowAutoCancelOnNewCommitYaml from './yaml_tests/positive_tests/workflow/auto_cancel/on_new_commit.yml'; // YAML NEGATIVE TEST import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml'; @@ -66,7 +66,8 @@ import NeedsParallelMatrixNumericYaml from './yaml_tests/negative_tests/needs/pa import NeedsParallelMatrixWrongParallelValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_parallel_value.yml'; import NeedsParallelMatrixWrongMatrixValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_matrix_value.yml'; import ScriptNegativeYaml from './yaml_tests/negative_tests/script.yml'; -import AutoCancelPipelineNegativeYaml from './yaml_tests/negative_tests/auto_cancel_pipeline.yml'; +import WorkflowAutoCancelOnJobFailureNegativeYaml from './yaml_tests/negative_tests/workflow/auto_cancel/on_job_failure.yml'; +import WorkflowAutoCancelOnNewCommitNegativeYaml from './yaml_tests/negative_tests/workflow/auto_cancel/on_new_commit.yml'; const ajv = new Ajv({ strictTypes: false, @@ -110,8 +111,8 @@ describe('positive tests', () => { SecretsYaml, NeedsParallelMatrixYaml, ScriptYaml, - AutoCancelPipelineOnJobFailureAllYaml, - AutoCancelPipelineOnJobFailureNoneYaml, + WorkflowAutoCancelOnJobFailureYaml, + WorkflowAutoCancelOnNewCommitYaml, }), )('schema validates %s', (_, input) => { // We construct a new "JSON" from each main key that is inside a @@ -157,7 +158,8 @@ describe('negative tests', () => { NeedsParallelMatrixWrongParallelValueYaml, NeedsParallelMatrixWrongMatrixValueYaml, ScriptNegativeYaml, - AutoCancelPipelineNegativeYaml, + WorkflowAutoCancelOnJobFailureNegativeYaml, + WorkflowAutoCancelOnNewCommitNegativeYaml, }), )('schema validates %s', (_, input) => { // We construct a new "JSON" from each main key that is inside a diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/auto_cancel_pipeline.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/workflow/auto_cancel/on_job_failure.yml similarity index 57% rename from spec/frontend/editor/schema/ci/yaml_tests/negative_tests/auto_cancel_pipeline.yml rename to spec/frontend/editor/schema/ci/yaml_tests/negative_tests/workflow/auto_cancel/on_job_failure.yml index 0ba3e5632e3eb8..2bf9effe1bef39 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/auto_cancel_pipeline.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/workflow/auto_cancel/on_job_failure.yml @@ -1,4 +1,3 @@ -# invalid workflow:auto-cancel:on-job-failure workflow: auto_cancel: on_job_failure: unexpected_value diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/workflow/auto_cancel/on_new_commit.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/workflow/auto_cancel/on_new_commit.yml new file mode 100644 index 00000000000000..371662efd24e48 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/workflow/auto_cancel/on_new_commit.yml @@ -0,0 +1,3 @@ +workflow: + auto_cancel: + on_new_commit: unexpected_value diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml deleted file mode 100644 index b99eb50e962c38..00000000000000 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/none.yml +++ /dev/null @@ -1,4 +0,0 @@ -# valid workflow:auto-cancel:on-job-failure -workflow: - auto_cancel: - on_job_failure: none diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/workflow/auto_cancel/on_job_failure.yml similarity index 52% rename from spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml rename to spec/frontend/editor/schema/ci/yaml_tests/positive_tests/workflow/auto_cancel/on_job_failure.yml index bf84ff16f42dca..79d18f40721107 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/auto_cancel_pipeline/on_job_failure/all.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/workflow/auto_cancel/on_job_failure.yml @@ -1,4 +1,3 @@ -# valid workflow:auto-cancel:on-job-failure workflow: auto_cancel: on_job_failure: all diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/workflow/auto_cancel/on_new_commit.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/workflow/auto_cancel/on_new_commit.yml new file mode 100644 index 00000000000000..a1641878e4d989 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/workflow/auto_cancel/on_new_commit.yml @@ -0,0 +1,3 @@ +workflow: + auto_cancel: + on_new_commit: conservative diff --git a/spec/services/ci/cancel_pipeline_service_spec.rb b/spec/services/ci/cancel_pipeline_service_spec.rb index 256d2db1ed2560..6051485c4df8c8 100644 --- a/spec/services/ci/cancel_pipeline_service_spec.rb +++ b/spec/services/ci/cancel_pipeline_service_spec.rb @@ -13,12 +13,14 @@ current_user: current_user, cascade_to_children: cascade_to_children, auto_canceled_by_pipeline: auto_canceled_by_pipeline, - execute_async: execute_async) + execute_async: execute_async, + safe_cancellation: safe_cancellation) end let(:cascade_to_children) { true } let(:auto_canceled_by_pipeline) { nil } let(:execute_async) { true } + let(:safe_cancellation) { false } shared_examples 'force_execute' do context 'when pipeline is not cancelable' do @@ -30,9 +32,14 @@ context 'when pipeline is cancelable' do before do - create(:ci_build, :running, pipeline: pipeline) - create(:ci_build, :created, pipeline: pipeline) - create(:ci_build, :success, pipeline: pipeline) + create(:ci_build, :running, pipeline: pipeline, name: 'build1') + create(:ci_build, :created, pipeline: pipeline, name: 'build2') + create(:ci_build, :success, pipeline: pipeline, name: 'build3') + create(:ci_build, :pending, :interruptible, pipeline: pipeline, name: 'build4') + + create(:ci_bridge, :running, pipeline: pipeline, name: 'bridge1') + create(:ci_bridge, :running, :interruptible, pipeline: pipeline, name: 'bridge2') + create(:ci_bridge, :success, :interruptible, pipeline: pipeline, name: 'bridge3') end it 'logs the event' do @@ -55,7 +62,15 @@ it 'cancels all cancelable jobs' do expect(response).to be_success - expect(pipeline.all_jobs.pluck(:status)).to match_array(%w[canceled canceled success]) + expect(pipeline.all_jobs.pluck(:name, :status)).to match_array([ + %w[build1 canceled], + %w[build2 canceled], + %w[build3 success], + %w[build4 canceled], + %w[bridge1 canceled], + %w[bridge2 canceled], + %w[bridge3 success] + ]) end context 'when auto_canceled_by_pipeline is provided' do @@ -74,6 +89,28 @@ end end + context 'when cascade_to_children: false and safe_cancellation: true' do + # We are testing the `safe_cancellation: true`` case with only `cascade_to_children: false` + # because `safe_cancellation` is passed as `true` only when `cascade_to_children` is `false` + # from `CancelRedundantPipelinesService`. + + let(:cascade_to_children) { false } + let(:safe_cancellation) { true } + + it 'cancels only interruptible jobs' do + expect(response).to be_success + expect(pipeline.all_jobs.pluck(:name, :status)).to match_array([ + %w[build1 running], + %w[build2 created], + %w[build3 success], + %w[build4 canceled], + %w[bridge1 running], + %w[bridge2 canceled], + %w[bridge3 success] + ]) + end + end + context 'when pipeline has child pipelines' do let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) } let!(:child_job) { create(:ci_build, :running, pipeline: child_pipeline) } @@ -81,8 +118,8 @@ let!(:grandchild_job) { create(:ci_build, :running, pipeline: grandchild_pipeline) } before do - child_pipeline.source_bridge.update!(status: :running) - grandchild_pipeline.source_bridge.update!(status: :running) + child_pipeline.source_bridge.update!(name: 'child_pipeline_bridge', status: :running) + grandchild_pipeline.source_bridge.update!(name: 'grandchild_pipeline_bridge', status: :running) end context 'when execute_async: false' do @@ -91,8 +128,15 @@ it 'cancels the bridge jobs and child jobs' do expect(response).to be_success - expect(pipeline.bridges.pluck(:status)).to be_all('canceled') - expect(child_pipeline.bridges.pluck(:status)).to be_all('canceled') + expect(pipeline.bridges.pluck(:name, :status)).to match_array([ + %w[bridge1 canceled], + %w[bridge2 canceled], + %w[bridge3 success], + %w[child_pipeline_bridge canceled] + ]) + expect(child_pipeline.bridges.pluck(:name, :status)).to match_array([ + %w[grandchild_pipeline_bridge canceled] + ]) expect(child_job.reload).to be_canceled expect(grandchild_job.reload).to be_canceled end @@ -110,7 +154,12 @@ expect(response).to be_success - expect(pipeline.bridges.pluck(:status)).to be_all('canceled') + expect(pipeline.bridges.pluck(:name, :status)).to match_array([ + %w[bridge1 canceled], + %w[bridge2 canceled], + %w[bridge3 success], + %w[child_pipeline_bridge canceled] + ]) end end @@ -124,7 +173,12 @@ expect(response).to be_success - expect(pipeline.bridges.pluck(:status)).to be_all('canceled') + expect(pipeline.bridges.pluck(:name, :status)).to match_array([ + %w[bridge1 canceled], + %w[bridge2 canceled], + %w[bridge3 success], + %w[child_pipeline_bridge canceled] + ]) expect(child_job.reload).to be_running end end diff --git a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb index 0d83187f9e4ba3..b39c1b2a69ceb3 100644 --- a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb +++ b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb @@ -53,7 +53,7 @@ project.update!(auto_cancel_pending_pipelines: 'enabled') end - it 'cancels only previous interruptible builds' do + it 'cancels only previous non started builds' do execute expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled') @@ -153,6 +153,36 @@ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') end + + context 'when the child pipeline auto_cancel_on_new_commit is `interruptible`' do + before do + child_pipeline.create_pipeline_metadata!( + project: child_pipeline.project, auto_cancel_on_new_commit: 'interruptible' + ) + end + + it 'cancels interruptible child pipeline builds' do + expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'success') + end + + context 'when the FF ci_workflow_auto_cancel_on_new_commit is disabled' do + before do + stub_feature_flags(ci_workflow_auto_cancel_on_new_commit: false) + end + + it 'does not cancel any child pipeline builds' do + expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') + end + end + end end context 'when the child pipeline has non-interruptible non-started job' do @@ -227,6 +257,37 @@ end end + context 'when there are non-interruptible completed jobs in the pipeline' do + before do + create(:ci_build, :failed, pipeline: prev_pipeline) + create(:ci_build, :success, pipeline: prev_pipeline) + end + + it 'does not cancel any job' do + execute + + expect(job_statuses(prev_pipeline)).to contain_exactly( + 'running', 'success', 'created', 'failed', 'success' + ) + expect(job_statuses(pipeline)).to contain_exactly('pending') + end + + context 'when the FF ci_workflow_auto_cancel_on_new_commit is disabled' do + before do + stub_feature_flags(ci_workflow_auto_cancel_on_new_commit: false) + end + + it 'does not cancel any job' do + execute + + expect(job_statuses(prev_pipeline)).to contain_exactly( + 'running', 'success', 'created', 'failed', 'success' + ) + expect(job_statuses(pipeline)).to contain_exactly('pending') + end + end + end + context 'when there are trigger jobs' do before do create(:ci_bridge, :created, pipeline: prev_pipeline) @@ -246,6 +307,80 @@ end end + context 'when auto_cancel_on_new_commit is `interruptible`' do + before do + prev_pipeline.create_pipeline_metadata!( + project: prev_pipeline.project, auto_cancel_on_new_commit: 'interruptible' + ) + end + + it 'cancels only interruptible jobs' do + execute + + expect(job_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'created') + expect(job_statuses(pipeline)).to contain_exactly('pending') + end + + context 'when the FF ci_workflow_auto_cancel_on_new_commit is disabled' do + before do + stub_feature_flags(ci_workflow_auto_cancel_on_new_commit: false) + end + + it 'cancels non started builds' do + execute + + expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled') + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + end + + context 'when there are non-interruptible completed jobs in the pipeline' do + before do + create(:ci_build, :failed, pipeline: prev_pipeline) + create(:ci_build, :success, pipeline: prev_pipeline) + end + + it 'still cancels only interruptible jobs' do + execute + + expect(job_statuses(prev_pipeline)).to contain_exactly( + 'canceled', 'success', 'created', 'failed', 'success' + ) + expect(job_statuses(pipeline)).to contain_exactly('pending') + end + + context 'when the FF ci_workflow_auto_cancel_on_new_commit is disabled' do + before do + stub_feature_flags(ci_workflow_auto_cancel_on_new_commit: false) + end + + it 'does not cancel any job' do + execute + + expect(build_statuses(prev_pipeline)).to contain_exactly( + 'created', 'success', 'running', 'failed', 'success' + ) + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + end + end + end + + context 'when auto_cancel_on_new_commit is `disabled`' do + before do + prev_pipeline.create_pipeline_metadata!( + project: prev_pipeline.project, auto_cancel_on_new_commit: 'disabled' + ) + end + + it 'does not cancel any job' do + execute + + expect(job_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + expect(job_statuses(pipeline)).to contain_exactly('pending') + end + end + it 'does not cancel future pipelines' do expect(prev_pipeline.id).to be < pipeline.id expect(build_statuses(pipeline)).to contain_exactly('pending') -- GitLab From d3202a324b891a012d26b5fd2a1d5758f5d72472 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Mon, 18 Dec 2023 11:44:11 +0100 Subject: [PATCH 2/4] Apply documentation suggestions --- .../ci_workflow_auto_cancel_on_new_commit.yml | 2 +- doc/ci/yaml/index.md | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml b/config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml index 74b6ad8911e7df..3b8c7b1e4896bd 100644 --- a/config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml +++ b/config/feature_flags/development/ci_workflow_auto_cancel_on_new_commit.yml @@ -2,7 +2,7 @@ name: ci_workflow_auto_cancel_on_new_commit introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139358 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434676 -milestone: '16.7' +milestone: '16.8' type: development group: group::pipeline authoring default_enabled: false diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 30f3980eff11e0..b71dbdedb4addb 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -482,7 +482,7 @@ You can use some [predefined CI/CD variables](../variables/predefined_variables. #### `workflow:auto_cancel:on_new_commit` -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412473) in GitLab 16.7 [with a flag](../../administration/feature_flags.md) named `ci_workflow_auto_cancel_on_new_commit`. Disabled by default. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412473) in GitLab 16.8 [with a flag](../../administration/feature_flags.md) named `ci_workflow_auto_cancel_on_new_commit`. Disabled by default. FLAG: On self-managed GitLab, by default this feature is not available. To make it available per project or @@ -495,9 +495,9 @@ the [auto-cancel redundant pipelines](../pipelines/settings.md#auto-cancel-redun **Possible inputs**: -- `conservative`: Current (legacy) behavior. -- `interruptible`: Cancel only `interruptible` jobs. -- `disabled`: Disable auto-cancel. +- `conservative`: Cancel the pipeline, but only if no jobs with `interruptible: false` have started yet. Default when not defined. +- `interruptible`: Cancel only jobs with `interruptible: true`. +- `disabled`: Do not auto-cancel any jobs. **Example of `workflow:auto_cancel:on_new_commit`**: @@ -2661,7 +2661,8 @@ job2: ### `interruptible` -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32022) in GitLab 12.3. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32022) in GitLab 12.3. +> - Support for `trigger` jobs [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412473) in GitLab 16.8 [with a flag](../../administration/feature_flags.md) named `ci_workflow_auto_cancel_on_new_commit`. Disabled by default. Use `interruptible` to configure the [auto-cancel redundant pipelines](../pipelines/settings.md#auto-cancel-redundant-pipelines) feature to cancel a job before it completes if a new pipeline on the same ref starts for a newer commit. If the feature @@ -2726,9 +2727,12 @@ In this example, a new pipeline causes a running pipeline to be: a pipeline to allow users to manually prevent a pipeline from being automatically cancelled. After a user starts the job, the pipeline cannot be canceled by the **Auto-cancel redundant pipelines** feature. - -TODO: add information for trigger jobs; https://gitlab.com/gitlab-org/gitlab/-/issues/412473#note_1685775974 -TODO: restructure here with workflow:auto_cancel:on_new_commit +- When using `interruptible` with a [trigger job](#trigger): + - The triggered downstream pipeline is never affected by the trigger job's `interruptible` configuration. + - If [`workflow:auto_cancel`](#workflowauto_cancelon_new_commit) is set to `conservative`, + the trigger job's `interruptible` configuration has no effect. + - If [`workflow:auto_cancel`](#workflowauto_cancelon_new_commit) is set to `interruptible`, + a trigger job with `interruptible: true` can be automatically cancelled. ### `needs` -- GitLab From 2ad4ac67e809b7d8f0cd35baafe5e875189e656d Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Thu, 21 Dec 2023 15:30:41 +0100 Subject: [PATCH 3/4] Change from disabled to none --- app/assets/javascripts/editor/schema/ci.json | 2 +- app/models/ci/pipeline_metadata.rb | 2 +- doc/ci/yaml/index.md | 2 +- spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb | 2 +- spec/models/ci/pipeline_metadata_spec.rb | 2 +- .../ci/create_pipeline_service/workflow_auto_cancel_spec.rb | 2 +- .../cancel_redundant_pipelines_service_spec.rb | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index b8cb7797ad9403..63c1ccb36dab34 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -962,7 +962,7 @@ "enum": [ "conservative", "interruptible", - "disabled" + "none" ] } } diff --git a/app/models/ci/pipeline_metadata.rb b/app/models/ci/pipeline_metadata.rb index 37fa3e32ad8e58..a41cdf61b71cf7 100644 --- a/app/models/ci/pipeline_metadata.rb +++ b/app/models/ci/pipeline_metadata.rb @@ -7,7 +7,7 @@ class PipelineMetadata < Ci::ApplicationRecord enum auto_cancel_on_new_commit: { conservative: 0, interruptible: 1, - disabled: 2 + none: 2 }, _prefix: true enum auto_cancel_on_job_failure: { diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index b71dbdedb4addb..93c38237993b6e 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -497,7 +497,7 @@ the [auto-cancel redundant pipelines](../pipelines/settings.md#auto-cancel-redun - `conservative`: Cancel the pipeline, but only if no jobs with `interruptible: false` have started yet. Default when not defined. - `interruptible`: Cancel only jobs with `interruptible: true`. -- `disabled`: Do not auto-cancel any jobs. +- `none`: Do not auto-cancel any jobs. **Example of `workflow:auto_cancel:on_new_commit`**: diff --git a/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb b/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb index bdd66cc00a1000..764908ee0402bf 100644 --- a/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb @@ -25,7 +25,7 @@ it 'returns errors' do expect(config.errors) - .to include('auto cancel on new commit must be one of: conservative, interruptible, disabled') + .to include('auto cancel on new commit must be one of: conservative, interruptible, none') end end end diff --git a/spec/models/ci/pipeline_metadata_spec.rb b/spec/models/ci/pipeline_metadata_spec.rb index 1a42611806363b..c114c0e945ecd4 100644 --- a/spec/models/ci/pipeline_metadata_spec.rb +++ b/spec/models/ci/pipeline_metadata_spec.rb @@ -15,7 +15,7 @@ is_expected.to define_enum_for( :auto_cancel_on_new_commit ).with_values( - conservative: 0, interruptible: 1, disabled: 2 + conservative: 0, interruptible: 1, none: 2 ).with_prefix end diff --git a/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb b/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb index 851c6f8fbea7ad..3ad6164bd01155 100644 --- a/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb +++ b/spec/services/ci/create_pipeline_service/workflow_auto_cancel_spec.rb @@ -57,7 +57,7 @@ it 'creates a pipeline with errors' do expect(pipeline).to be_persisted expect(pipeline.errors.full_messages).to include( - 'workflow:auto_cancel on new commit must be one of: conservative, interruptible, disabled') + 'workflow:auto_cancel on new commit must be one of: conservative, interruptible, none') end end end diff --git a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb index b39c1b2a69ceb3..b28c85dde9497f 100644 --- a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb +++ b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb @@ -366,10 +366,10 @@ end end - context 'when auto_cancel_on_new_commit is `disabled`' do + context 'when auto_cancel_on_new_commit is `none`' do before do prev_pipeline.create_pipeline_metadata!( - project: prev_pipeline.project, auto_cancel_on_new_commit: 'disabled' + project: prev_pipeline.project, auto_cancel_on_new_commit: 'none' ) end -- GitLab From 48a5bb9f5a577c039056fd8d524b3e308e667c74 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Wed, 3 Jan 2024 12:04:57 +0100 Subject: [PATCH 4/4] Apply reviewer suggestions --- app/models/ci/pipeline.rb | 6 +- .../cancel_redundant_pipelines_service.rb | 27 +++---- spec/models/ci/pipeline_spec.rb | 32 +++++++++ ...cancel_redundant_pipelines_service_spec.rb | 72 +++++++++++++++++++ 4 files changed, 123 insertions(+), 14 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 911e78c807fd3d..ef10cdc133abdc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -151,7 +151,7 @@ class Pipeline < Ci::ApplicationRecord accepts_nested_attributes_for :variables, reject_if: :persisted? delegate :full_path, to: :project, prefix: true - delegate :name, :auto_cancel_on_job_failure, :auto_cancel_on_new_commit, to: :pipeline_metadata, allow_nil: true + delegate :name, :auto_cancel_on_job_failure, to: :pipeline_metadata, allow_nil: true validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } @@ -1394,6 +1394,10 @@ def merge_request_diff merge_request.merge_request_diff_for(merge_request_diff_sha) end + def auto_cancel_on_new_commit + pipeline_metadata&.auto_cancel_on_new_commit || 'conservative' + end + private def add_message(severity, content) diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb index 97e9a45cd5076d..98469e82af3081 100644 --- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb +++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb @@ -75,7 +75,6 @@ def legacy_auto_cancel_pipelines(pipeline_ids) .id_in(pipeline_ids) .conservative_interruptible .each do |cancelable_pipeline| - log_info(cancelable_pipeline) cancel_pipeline(cancelable_pipeline, safe_cancellation: false) end end @@ -85,29 +84,33 @@ def auto_cancel_pipelines(pipeline_ids) return legacy_auto_cancel_pipelines(pipeline_ids) end - conservative_cancellable_pipeline_ids = ::Ci::Pipeline.id_in(pipeline_ids).conservative_interruptible.ids - ::Ci::Pipeline .id_in(pipeline_ids) .each do |cancelable_pipeline| case cancelable_pipeline.auto_cancel_on_new_commit - when 'conservative', nil - # 'conservative' and `nil` are the same because `auto_cancel_on_new_commit` returns `nil` - # when the pipeline does not have `pipeline_metadata`. - - next unless conservative_cancellable_pipeline_ids.include?(cancelable_pipeline.id) + when 'none' + # no-op + when 'conservative' + next unless conservative_cancellable_pipeline_ids(pipeline_ids).include?(cancelable_pipeline.id) - log_info(cancelable_pipeline) cancel_pipeline(cancelable_pipeline, safe_cancellation: false) when 'interruptible' - log_info(cancelable_pipeline) cancel_pipeline(cancelable_pipeline, safe_cancellation: true) + else + raise ArgumentError, + "Unknown auto_cancel_on_new_commit value: #{cancelable_pipeline.auto_cancel_on_new_commit}" end end end + + def conservative_cancellable_pipeline_ids(pipeline_ids) + strong_memoize_with(:conservative_cancellable_pipeline_ids, pipeline_ids) do + ::Ci::Pipeline.id_in(pipeline_ids).conservative_interruptible.ids + end + end # rubocop: enable CodeReuse/ActiveRecord - def log_info(cancelable_pipeline) + def cancel_pipeline(cancelable_pipeline, safe_cancellation:) Gitlab::AppLogger.info( class: self.class.name, message: "Pipeline #{pipeline.id} auto-canceling pipeline #{cancelable_pipeline.id}", @@ -115,9 +118,7 @@ def log_info(cancelable_pipeline) canceled_by_pipeline_id: pipeline.id, canceled_by_pipeline_source: pipeline.source ) - end - def cancel_pipeline(cancelable_pipeline, safe_cancellation:) # cascade_to_children not needed because we iterate through descendants here ::Ci::CancelPipelineService.new( pipeline: cancelable_pipeline, diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index e075ea232d359b..52c3792ac93a74 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -5727,4 +5727,36 @@ def create_bridge(upstream:, downstream:, depends: false) end end end + + describe '#auto_cancel_on_new_commit' do + let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) } + + subject(:auto_cancel_on_new_commit) { pipeline.auto_cancel_on_new_commit } + + context 'when pipeline_metadata is not present' do + it { is_expected.to eq('conservative') } + end + + context 'when pipeline_metadata is present' do + before_all do + create(:ci_pipeline_metadata, project: pipeline.project, pipeline: pipeline) + end + + context 'when auto_cancel_on_new_commit is nil' do + before do + pipeline.pipeline_metadata.auto_cancel_on_new_commit = nil + end + + it { is_expected.to eq('conservative') } + end + + context 'when auto_cancel_on_new_commit is a valid value' do + before do + pipeline.pipeline_metadata.auto_cancel_on_new_commit = 'interruptible' + end + + it { is_expected.to eq('interruptible') } + end + end + end end diff --git a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb index b28c85dde9497f..7b5eef92f53f23 100644 --- a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb +++ b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb @@ -381,6 +381,78 @@ end end + context 'when auto_cancel_on_new_commit is `conservative`' do + before do + prev_pipeline.create_pipeline_metadata!( + project: prev_pipeline.project, auto_cancel_on_new_commit: 'conservative' + ) + end + + it 'cancels only previous non started builds' do + execute + + expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled') + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + + context 'when the FF ci_workflow_auto_cancel_on_new_commit is disabled' do + before do + stub_feature_flags(ci_workflow_auto_cancel_on_new_commit: false) + end + + it 'cancels only previous non started builds' do + execute + + expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled') + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + end + + context 'when there are non-interruptible completed jobs in the pipeline' do + before do + create(:ci_build, :failed, pipeline: prev_pipeline) + create(:ci_build, :success, pipeline: prev_pipeline) + end + + it 'does not cancel any job' do + execute + + expect(job_statuses(prev_pipeline)).to contain_exactly( + 'running', 'success', 'created', 'failed', 'success' + ) + expect(job_statuses(pipeline)).to contain_exactly('pending') + end + + context 'when the FF ci_workflow_auto_cancel_on_new_commit is disabled' do + before do + stub_feature_flags(ci_workflow_auto_cancel_on_new_commit: false) + end + + it 'does not cancel any job' do + execute + + expect(job_statuses(prev_pipeline)).to contain_exactly( + 'running', 'success', 'created', 'failed', 'success' + ) + expect(job_statuses(pipeline)).to contain_exactly('pending') + end + end + end + end + + context 'when auto_cancel_on_new_commit is an invalid value' do + before do + allow(prev_pipeline).to receive(:auto_cancel_on_new_commit).and_return('invalid') + relation = Ci::Pipeline.id_in(prev_pipeline.id) + allow(relation).to receive(:each).and_yield(prev_pipeline) + allow(Ci::Pipeline).to receive(:id_in).and_return(relation) + end + + it 'raises an error' do + expect { execute }.to raise_error(ArgumentError, 'Unknown auto_cancel_on_new_commit value: invalid') + end + end + it 'does not cancel future pipelines' do expect(prev_pipeline.id).to be < pipeline.id expect(build_statuses(pipeline)).to contain_exactly('pending') -- GitLab