From c256a37d99e325303296baef6faf7ade65322528 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Thu, 28 Aug 2025 15:18:49 +0200 Subject: [PATCH 01/22] Add allowed time window for scheduled pipelines --- app/models/ci/pipeline.rb | 8 ++++++ .../atomic_processing_service.rb | 4 +++ .../atomic_processing_service_spec.rb | 27 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 72d4097a945a10..b6f2e369ecf5b0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -897,6 +897,14 @@ def needs_processing? .exists? end + def within_scheduled_window_after_creation? + # TODO: get this window from the associated pipeline schedule + # Make it configurable for the customer + window = 2.days + + (created_at + window).future? + end + def has_yaml_errors? yaml_errors.present? end diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index 9484c5b0ffe818..e70b00910b2c88 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -19,6 +19,10 @@ def initialize(pipeline) def execute return unless pipeline.needs_processing? + if pipeline.scheduled? && !pipeline.within_scheduled_window_after_creation? + pipeline.skip + return + end # Run the process only if we can obtain an exclusive lease; returns nil if lease is unavailable success = try_obtain_lease { process! } diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index fef18cd2cdb7cb..2b681ca1d2cd33 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -505,6 +505,33 @@ def event_on_pipeline(event) expect(pipeline.reload.status).to eq 'pending' end end + + context 'when a delayed job is allowed to be automatically skipped' do + before do + create_build('delayed', **delayed_options, allow_failure: true, stage_idx: 0) + create_build('job', stage_idx: 1) + + allow(Ci::BuildScheduleWorker).to receive(:perform_at) + end + + it 'skips the pipeline when it is out of the window' do + travel_to 3.days.from_now do + expect(process_pipeline).to be_truthy + expect(builds_names_and_statuses).to eq({ delayed: 'scheduled' }) + enqueue_scheduled('delayed') + end + expect(pipeline.reload.status).to eq 'skipped' + end + + it 'does not skip the pipeline when it is out of the window' do + travel_to 1.day.from_now do + expect(process_pipeline).to be_truthy + expect(builds_names_and_statuses).to eq({ delayed: 'scheduled' }) + enqueue_scheduled('delayed') + end + expect(pipeline.reload.status).to eq 'pending' + end + end end context 'when an exception is raised during a persistent ref creation' do -- GitLab From d9b14758b4623bc9b4848ae771194883640d19fc Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Thu, 28 Aug 2025 19:14:30 +0200 Subject: [PATCH 02/22] Extend and fix prevention of enqueuing for pipelines outside of window --- app/models/ci/pipeline.rb | 8 +++---- app/models/ci/pipeline_schedule.rb | 7 ++++++ app/workers/run_pipeline_schedule_worker.rb | 10 +++++++++ ...ne_skip_window_to_ci_pipeline_schedules.rb | 9 ++++++++ db/schema_migrations/20250828154201 | 1 + db/structure.sql | 1 + .../atomic_processing_service_spec.rb | 3 +++ .../run_pipeline_schedule_worker_spec.rb | 22 +++++++++++++++++++ 8 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb create mode 100644 db/schema_migrations/20250828154201 diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index b6f2e369ecf5b0..a8c1dcc33ea902 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -898,11 +898,11 @@ def needs_processing? end def within_scheduled_window_after_creation? - # TODO: get this window from the associated pipeline schedule - # Make it configurable for the customer - window = 2.days + return true unless pipeline_schedule + return true unless scheduled? + return true if pipeline_schedule.scheduled_pipeline_skip_window.to_i == 0 - (created_at + window).future? + (created_at + pipeline_schedule.scheduled_pipeline_skip_window).future? end def has_yaml_errors? diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index e35b6e290bf1c3..d3b7196fe5ed48 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -142,6 +142,13 @@ def last_pipeline pipelines.last end + def within_window?(old_next_run_at) + return true if scheduled_pipeline_skip_window.to_i == 0 + + scheduled_pipeline_skip_window.to_i > 0 && + (old_next_run_at + scheduled_pipeline_skip_window.to_i).past? + end + private def ambiguous_ref? diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb index f0bd5a262cd77d..c76d718e228595 100644 --- a/app/workers/run_pipeline_schedule_worker.rb +++ b/app/workers/run_pipeline_schedule_worker.rb @@ -19,8 +19,18 @@ def perform(schedule_id, user_id, options = {}) return unless schedule_valid?(schedule, schedule_id, user, options) + old_next_run_at = schedule.next_run_at + update_next_run_at_for(schedule) if options['scheduling'] + unless schedule.within_window?(old_next_run_at) + message = 'Pipeline schedule runnable window exceeded.' + response = ServiceResponse.error(message: message) + log_error(schedule.id, message) + + return response + end + response = run_pipeline_schedule(schedule, user) log_error(schedule.id, response.message) if response&.error? diff --git a/db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb b/db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb new file mode 100644 index 00000000000000..6b7eaa1fd428c7 --- /dev/null +++ b/db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddScheduledPipelineSkipWindowToCiPipelineSchedules < Gitlab::Database::Migration[2.3] + milestone '18.4' + + def change + add_column :ci_pipeline_schedules, :scheduled_pipeline_skip_window, :integer, default: 0 + end +end diff --git a/db/schema_migrations/20250828154201 b/db/schema_migrations/20250828154201 new file mode 100644 index 00000000000000..217d28f9ed3328 --- /dev/null +++ b/db/schema_migrations/20250828154201 @@ -0,0 +1 @@ +ce599a8032f3d6ae743365703df3b41e1d120679b54c34ef0f24a88b26db4991 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f81c3477eade48..27d55a42c1ed72 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13251,6 +13251,7 @@ CREATE TABLE ci_pipeline_schedules ( active boolean DEFAULT true, created_at timestamp without time zone, updated_at timestamp without time zone, + scheduled_pipeline_skip_window integer DEFAULT 0, CONSTRAINT check_4a0f7b994d CHECK ((project_id IS NOT NULL)) ); diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index 2b681ca1d2cd33..0d33a87d100db5 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -507,7 +507,10 @@ def event_on_pipeline(event) end context 'when a delayed job is allowed to be automatically skipped' do + let!(:project_pipeline_schedule) { create(:ci_pipeline_schedule, project: project, scheduled_pipeline_skip_window: 2.days) } + before do + pipeline.update!(pipeline_schedule: project_pipeline_schedule) create_build('delayed', **delayed_options, allow_failure: true, stage_idx: 0) create_build('job', stage_idx: 1) diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb index cbf837ac5dda34..5f57fff7e5e7c0 100644 --- a/spec/workers/run_pipeline_schedule_worker_spec.rb +++ b/spec/workers/run_pipeline_schedule_worker_spec.rb @@ -136,6 +136,28 @@ end end + context "when pipeline is outside of allowed scheduled window" do + let(:error_response) do + instance_double(ServiceResponse, + error?: true, + success?: false, + message: 'Pipeline schedule runnable window exceeded.' + ) + end + + before do + pipeline_schedule.update!(next_run_at: 3.days.ago, scheduled_pipeline_skip_window: 1) + allow(ServiceResponse).to receive(:error) + .with(message: 'Pipeline schedule runnable window exceeded.') + .and_return(error_response) + end + + it "returns the a timeout response" do + expect(worker.perform(pipeline_schedule.id, + user.id)).to eq(error_response) + end + end + describe "#run_pipeline_schedule" do let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService, execute: service_response) } let(:service_response) { instance_double(ServiceResponse, payload: pipeline, error?: false) } -- GitLab From 82c3c8dbc2d3070ac167af2e2a1245d1d5b9b9a0 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Tue, 2 Sep 2025 14:39:07 +0200 Subject: [PATCH 03/22] Add validations for scheduled_pipeline_skip_window --- app/models/ci/pipeline_schedule.rb | 1 + spec/models/ci/pipeline_schedule_spec.rb | 25 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index d3b7196fe5ed48..e930dea9f0738a 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -44,6 +44,7 @@ class PipelineSchedule < Ci::ApplicationRecord validates :variables, nested_attributes_duplicates: true validates :inputs, nested_attributes_duplicates: { child_attributes: %i[name] } validates :project, presence: true + validates :scheduled_pipeline_skip_window, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true validates :inputs, length: { maximum: Ci::Pipeline::INPUTS_LIMIT, diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 878146fc40bdc2..a279b90c70cdc7 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -10,6 +10,7 @@ it { is_expected.to belong_to(:project) } it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_numericality_of(:scheduled_pipeline_skip_window) } it { is_expected.to belong_to(:owner) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } @@ -32,6 +33,30 @@ end describe 'validations' do + it 'does allow nil value scheduled_pipeline_skip_window' do + pipeline_schedule = build(:ci_pipeline_schedule, scheduled_pipeline_skip_window: nil, project: project) + + expect(pipeline_schedule).to be_valid + end + + it 'does allow value greater 0 for scheduled_pipeline_skip_window' do + pipeline_schedule = build(:ci_pipeline_schedule, scheduled_pipeline_skip_window: 3600, project: project) + + expect(pipeline_schedule).to be_valid + end + + it 'does allow 0 for scheduled_pipeline_skip_window' do + pipeline_schedule = build(:ci_pipeline_schedule, scheduled_pipeline_skip_window: 0, project: project) + + expect(pipeline_schedule).to be_valid + end + + it 'does not allow negative values for scheduled_pipeline_skip_window' do + pipeline_schedule = build(:ci_pipeline_schedule, scheduled_pipeline_skip_window: -1, project: project) + + expect(pipeline_schedule).not_to be_valid + end + it 'does not allow invalid cron patterns' do pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *', project: project) -- GitLab From eb31abef811ab11ad58825fab015ac7995f5f3ac Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Wed, 3 Sep 2025 16:35:14 +0200 Subject: [PATCH 04/22] Add front-end code for scheduled pipeline skip window --- .../components/pipeline_schedules_form.vue | 27 +++++++++++++++++++ .../get_pipeline_schedules.query.graphql | 1 + .../projects/pipeline_schedules_controller.rb | 2 +- .../mutations/ci/pipeline_schedule/create.rb | 3 +++ .../mutations/ci/pipeline_schedule/update.rb | 3 +++ .../types/ci/pipeline_schedule_type.rb | 3 +++ doc/api/graphql/reference/_index.md | 3 +++ locale/gitlab.pot | 6 +++++ 8 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue index b8beba01915f01..a9b497a2b4adee 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue @@ -94,6 +94,7 @@ export default { this.cronTimezone = schedule.cronTimezone; this.savedInputs = schedule.inputs?.nodes || []; this.scheduleRef = schedule.ref || this.defaultBranch; + this.scheduledPipelineSkipWindow = schedule.scheduledPipelineSkipWindow; this.variables = variables.map((variable) => { return { id: variable.id, @@ -119,6 +120,7 @@ export default { activated: true, cron: '', cronTimezone: '', + scheduledPipelineSkipWindow: null, description: '', pipelineInputs: [], savedInputs: [], @@ -144,6 +146,7 @@ export default { createScheduleBtnText: s__('PipelineSchedules|Create pipeline schedule'), cancel: __('Cancel'), targetBranchTag: __('Select target branch or tag'), + scheduledPipelineSkipWindow: __('Scheduled pipeline skip window'), intervalPattern: s__('PipelineSchedules|Interval Pattern'), scheduleCreateError: s__( @@ -157,6 +160,14 @@ export default { ), }, computed: { + scheduledPipelineSkipWindowNumber: { + get() { + return this.scheduledPipelineSkipWindow; + }, + set(value) { + this.scheduledPipelineSkipWindow = value !== '' ? Number(value) : null; + }, + }, dropdownTranslations() { return { dropdownHeader: this.$options.i18n.targetBranchTag, @@ -215,6 +226,7 @@ export default { cronTimezone: this.cronTimezone, ref: this.scheduleRef, variables: this.preparedVariablesCreate, + scheduledPipelineSkipWindow: this.scheduledPipelineSkipWindow, active: this.activated, projectPath: this.projectPath, inputs: this.pipelineInputs, @@ -246,6 +258,7 @@ export default { description: this.description, cron: this.cron, cronTimezone: this.cronTimezone, + scheduledPipelineSkipWindow: this.scheduledPipelineSkipWindow, ref: this.scheduleRef, variables: this.preparedVariablesUpdate, active: this.activated, @@ -362,6 +375,20 @@ export default { class="gl-w-full" /> + + + + `inputs` {{< icon name="warning-solid" >}} | [`[CiInputsInput!]`](#ciinputsinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 17.10. | | `projectPath` | [`ID!`](#id) | Full path of the project the pipeline schedule is associated with. | | `ref` | [`String!`](#string) | Ref of the pipeline schedule. | +| `scheduledPipelineSkipWindow` | [`Int`](#int) | Scheduled pipeline skip window (in seconds). | | `variables` | [`[PipelineScheduleVariableInput!]`](#pipelineschedulevariableinput) | Variables for the pipeline schedule. | #### Fields @@ -10183,6 +10184,7 @@ Input type: `PipelineScheduleUpdateInput` | `id` | [`CiPipelineScheduleID!`](#cipipelinescheduleid) | ID of the pipeline schedule to mutate. | | `inputs` {{< icon name="warning-solid" >}} | [`[CiInputsInput!]`](#ciinputsinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 17.11. | | `ref` | [`String`](#string) | Ref of the pipeline schedule. | +| `scheduledPipelineSkipWindow` | [`Int`](#int) | Scheduled pipeline skip window (in seconds). | | `variables` | [`[PipelineScheduleVariableInput!]`](#pipelineschedulevariableinput) | Variables for the pipeline schedule. | #### Fields @@ -38173,6 +38175,7 @@ Represents a pipeline schedule. | `ref` | [`String`](#string) | Ref of the pipeline schedule. | | `refForDisplay` | [`String`](#string) | Git ref for the pipeline schedule. | | `refPath` | [`String`](#string) | Path to the ref that triggered the pipeline. | +| `scheduledPipelineSkipWindow` | [`Int`](#int) | Scheduled pipeline skip window (in seconds). | | `updatedAt` | [`Time!`](#time) | Timestamp of when the pipeline schedule was last updated. | | `userPermissions` | [`PipelineSchedulePermissions!`](#pipelineschedulepermissions) | Permissions for the current user on the resource. | | `variables` | [`PipelineScheduleVariableConnection`](#pipelineschedulevariableconnection) | Pipeline schedule variables. (see [Connections](#connections)) | diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 602843c478d8cb..78a7d67451523f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22103,6 +22103,9 @@ msgstr "" msgid "Define a custom pattern with cron syntax" msgstr "" +msgid "Define a custom pipeline execution window (in seconds)" +msgstr "" + msgid "Define custom rules for what constitutes spam, independent of Akismet" msgstr "" @@ -57090,6 +57093,9 @@ msgstr "" msgid "Scheduled a rebase of branch %{branch}." msgstr "" +msgid "Scheduled pipeline skip window" +msgstr "" + msgid "Scheduled pipelines" msgstr "" -- GitLab From 2086c464b786c5b4f2126de80f7fd9e725fb260f Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Wed, 3 Sep 2025 18:28:21 +0200 Subject: [PATCH 05/22] Fix test --- spec/graphql/types/ci/pipeline_schedule_type_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/graphql/types/ci/pipeline_schedule_type_spec.rb b/spec/graphql/types/ci/pipeline_schedule_type_spec.rb index f5c6c7ada730d7..a4ab26f99e2b9e 100644 --- a/spec/graphql/types/ci/pipeline_schedule_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_schedule_type_spec.rb @@ -21,6 +21,7 @@ forTag nextRunAt realNextRun + scheduledPipelineSkipWindow cron cronTimezone userPermissions -- GitLab From fecda8e0ad7e627de8e9af4c0126a6346c27462f Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Wed, 3 Sep 2025 18:51:50 +0200 Subject: [PATCH 06/22] Update jest specs --- .../components/pipeline_schedules_form_spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js index 9e762265c759c7..4eaaea2785791d 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js @@ -323,6 +323,7 @@ describe('Pipeline schedules form', () => { cronTimezone: 'America/New_York', description: 'My schedule', projectPath: 'gitlab-org/gitlab', + scheduledPipelineSkipWindow: null, ref: 'main', variables: updatedVariables, inputs: updatedInputs, @@ -501,6 +502,7 @@ describe('Pipeline schedules form', () => { cronTimezone: schedule.cronTimezone, id: schedule.id, ref: schedule.ref, + scheduledPipelineSkipWindow: null, description: 'Updated schedule', variables: updatedVariables, inputs: updatedInputs, -- GitLab From b7af7f99b1456d543b6a0127eafa015aa7cebf26 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Wed, 3 Sep 2025 19:17:15 +0200 Subject: [PATCH 07/22] Fix jest specs --- spec/fixtures/api/schemas/pipeline_schedule.json | 3 +++ .../components/pipeline_schedules_form_spec.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json index 1a05770aacd131..70e23a142fe54a 100644 --- a/spec/fixtures/api/schemas/pipeline_schedule.json +++ b/spec/fixtures/api/schemas/pipeline_schedule.json @@ -16,6 +16,9 @@ "cron_timezone": { "type": "string" }, + "scheduledPipelineSkipWindow": { + "type": "integer" + }, "next_run_at": { "type": "string" }, diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js index 4eaaea2785791d..e64f5709005611 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js @@ -502,7 +502,7 @@ describe('Pipeline schedules form', () => { cronTimezone: schedule.cronTimezone, id: schedule.id, ref: schedule.ref, - scheduledPipelineSkipWindow: null, + scheduledPipelineSkipWindow: 0, description: 'Updated schedule', variables: updatedVariables, inputs: updatedInputs, -- GitLab From 41c18007099cf8a4dfe4f84b207b3b4c3031bfe1 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Wed, 3 Sep 2025 19:26:41 +0200 Subject: [PATCH 08/22] Run prettier --- spec/fixtures/api/schemas/pipeline_schedule.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json index 70e23a142fe54a..446c94afa185b9 100644 --- a/spec/fixtures/api/schemas/pipeline_schedule.json +++ b/spec/fixtures/api/schemas/pipeline_schedule.json @@ -99,7 +99,10 @@ "type": "string" }, "public_email": { - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "id": { "type": "integer" -- GitLab From 2122efee5484612bf31279d2a8927f5cdd8c7a94 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Wed, 17 Sep 2025 14:36:48 +0200 Subject: [PATCH 09/22] Add description to pipeline execution window --- .../pipeline_schedules/components/pipeline_schedules_form.vue | 4 ++++ locale/gitlab.pot | 3 +++ 2 files changed, 7 insertions(+) diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue index a9b497a2b4adee..3f334f51ec3f34 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue @@ -147,6 +147,7 @@ export default { cancel: __('Cancel'), targetBranchTag: __('Select target branch or tag'), scheduledPipelineSkipWindow: __('Scheduled pipeline skip window'), + scheduledPipelineSkipWindowDescription: s__('PipelineSchedules|Define a custom pipeline execution window that allows the pipeline to run for a given amount of time after its scheduled time. This window must be specified in seconds (86,400 seconds equal one day).'), intervalPattern: s__('PipelineSchedules|Interval Pattern'), scheduleCreateError: s__( @@ -381,6 +382,9 @@ export default { label-for="schedule-scheduled-pipeline-skip-window" class="lg:gl-w-2/3" > +

+ {{ $options.i18n.scheduledPipelineSkipWindowDescription }} +

Date: Wed, 17 Sep 2025 14:48:31 +0200 Subject: [PATCH 10/22] Run prettier --- .../components/pipeline_schedules_form.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue index 3f334f51ec3f34..f973674dc04e6b 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue @@ -147,7 +147,9 @@ export default { cancel: __('Cancel'), targetBranchTag: __('Select target branch or tag'), scheduledPipelineSkipWindow: __('Scheduled pipeline skip window'), - scheduledPipelineSkipWindowDescription: s__('PipelineSchedules|Define a custom pipeline execution window that allows the pipeline to run for a given amount of time after its scheduled time. This window must be specified in seconds (86,400 seconds equal one day).'), + scheduledPipelineSkipWindowDescription: s__( + 'PipelineSchedules|Define a custom pipeline execution window that allows the pipeline to run for a given amount of time after its scheduled time. This window must be specified in seconds (86,400 seconds equal one day).', + ), intervalPattern: s__('PipelineSchedules|Interval Pattern'), scheduleCreateError: s__( @@ -382,9 +384,9 @@ export default { label-for="schedule-scheduled-pipeline-skip-window" class="lg:gl-w-2/3" > -

- {{ $options.i18n.scheduledPipelineSkipWindowDescription }} -

+

+ {{ $options.i18n.scheduledPipelineSkipWindowDescription }} +

Date: Thu, 18 Sep 2025 10:16:40 +0200 Subject: [PATCH 11/22] Add ff to derisk pipeline execution based on possible window --- .../atomic_processing_service.rb | 3 ++- app/workers/run_pipeline_schedule_worker.rb | 4 ++-- .../scheduled_pipline_skip_window.yml | 10 ++++++++++ .../atomic_processing_service_spec.rb | 17 ++++++++++++++++- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 config/feature_flags/gitlab_com_derisk/scheduled_pipline_skip_window.yml diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index e70b00910b2c88..9ead392a2d01a1 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -19,7 +19,8 @@ def initialize(pipeline) def execute return unless pipeline.needs_processing? - if pipeline.scheduled? && !pipeline.within_scheduled_window_after_creation? + if Feature.enabled?(:scheduled_pipline_skip_window, + pipeline.project) && (pipeline.scheduled? && !pipeline.within_scheduled_window_after_creation?) pipeline.skip return end diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb index c76d718e228595..beb4dad545fe39 100644 --- a/app/workers/run_pipeline_schedule_worker.rb +++ b/app/workers/run_pipeline_schedule_worker.rb @@ -19,11 +19,11 @@ def perform(schedule_id, user_id, options = {}) return unless schedule_valid?(schedule, schedule_id, user, options) - old_next_run_at = schedule.next_run_at + old_next_run_at = schedule.next_run_at if Feature.enabled?(:scheduled_pipline_skip_window, schedule.project) update_next_run_at_for(schedule) if options['scheduling'] - unless schedule.within_window?(old_next_run_at) + if Feature.enabled?(:scheduled_pipline_skip_window, schedule.project) && !schedule.within_window?(old_next_run_at) message = 'Pipeline schedule runnable window exceeded.' response = ServiceResponse.error(message: message) log_error(schedule.id, message) diff --git a/config/feature_flags/gitlab_com_derisk/scheduled_pipline_skip_window.yml b/config/feature_flags/gitlab_com_derisk/scheduled_pipline_skip_window.yml new file mode 100644 index 00000000000000..7d447f788bc1b0 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/scheduled_pipline_skip_window.yml @@ -0,0 +1,10 @@ +--- +name: scheduled_pipline_skip_window +description: +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539393 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/202610 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/571092 +milestone: '18.4' +group: group::pipeline execution +type: gitlab_com_derisk +default_enabled: false diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index 0d33a87d100db5..3b2aeb15c215f0 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -526,7 +526,7 @@ def event_on_pipeline(event) expect(pipeline.reload.status).to eq 'skipped' end - it 'does not skip the pipeline when it is out of the window' do + it 'does not skip the pipeline when it within the window' do travel_to 1.day.from_now do expect(process_pipeline).to be_truthy expect(builds_names_and_statuses).to eq({ delayed: 'scheduled' }) @@ -534,6 +534,21 @@ def event_on_pipeline(event) end expect(pipeline.reload.status).to eq 'pending' end + + context 'when derisk feature flag is disabled' do + before do + stub_feature_flags(scheduled_pipline_skip_window: false) + end + + it 'does not skip the pipeline when it is out of the window' do + travel_to 3.days.from_now do + expect(process_pipeline).to be_truthy + expect(builds_names_and_statuses).to eq({ delayed: 'scheduled' }) + enqueue_scheduled('delayed') + end + expect(pipeline.reload.status).to eq 'pending' + end + end end end -- GitLab From f94747e13caf4cdd0c08857110bdc7b8574a5339 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 08:16:25 +0200 Subject: [PATCH 12/22] Fix pipeline typo --- .../ci/pipeline_processing/atomic_processing_service.rb | 2 +- app/workers/run_pipeline_schedule_worker.rb | 4 ++-- ...ine_skip_window.yml => scheduled_pipeline_skip_window.yml} | 2 +- .../ci/pipeline_processing/atomic_processing_service_spec.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename config/feature_flags/gitlab_com_derisk/{scheduled_pipline_skip_window.yml => scheduled_pipeline_skip_window.yml} (90%) diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index 9ead392a2d01a1..e9c06da87fcd69 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -19,7 +19,7 @@ def initialize(pipeline) def execute return unless pipeline.needs_processing? - if Feature.enabled?(:scheduled_pipline_skip_window, + if Feature.enabled?(:scheduled_pipeline_skip_window, pipeline.project) && (pipeline.scheduled? && !pipeline.within_scheduled_window_after_creation?) pipeline.skip return diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb index beb4dad545fe39..0bb1392e625b0c 100644 --- a/app/workers/run_pipeline_schedule_worker.rb +++ b/app/workers/run_pipeline_schedule_worker.rb @@ -19,11 +19,11 @@ def perform(schedule_id, user_id, options = {}) return unless schedule_valid?(schedule, schedule_id, user, options) - old_next_run_at = schedule.next_run_at if Feature.enabled?(:scheduled_pipline_skip_window, schedule.project) + old_next_run_at = schedule.next_run_at if Feature.enabled?(:scheduled_pipeline_skip_window, schedule.project) update_next_run_at_for(schedule) if options['scheduling'] - if Feature.enabled?(:scheduled_pipline_skip_window, schedule.project) && !schedule.within_window?(old_next_run_at) + if Feature.enabled?(:scheduled_pipeline_skip_window, schedule.project) && !schedule.within_window?(old_next_run_at) message = 'Pipeline schedule runnable window exceeded.' response = ServiceResponse.error(message: message) log_error(schedule.id, message) diff --git a/config/feature_flags/gitlab_com_derisk/scheduled_pipline_skip_window.yml b/config/feature_flags/gitlab_com_derisk/scheduled_pipeline_skip_window.yml similarity index 90% rename from config/feature_flags/gitlab_com_derisk/scheduled_pipline_skip_window.yml rename to config/feature_flags/gitlab_com_derisk/scheduled_pipeline_skip_window.yml index 7d447f788bc1b0..9c54831b4486e8 100644 --- a/config/feature_flags/gitlab_com_derisk/scheduled_pipline_skip_window.yml +++ b/config/feature_flags/gitlab_com_derisk/scheduled_pipeline_skip_window.yml @@ -1,5 +1,5 @@ --- -name: scheduled_pipline_skip_window +name: scheduled_pipeline_skip_window description: feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539393 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/202610 diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb index 3b2aeb15c215f0..fee70153c1e652 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb @@ -537,7 +537,7 @@ def event_on_pipeline(event) context 'when derisk feature flag is disabled' do before do - stub_feature_flags(scheduled_pipline_skip_window: false) + stub_feature_flags(scheduled_pipeline_skip_window: false) end it 'does not skip the pipeline when it is out of the window' do -- GitLab From fe3529978df66f93813663283f21aa03c08e6fc8 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 09:20:21 +0200 Subject: [PATCH 13/22] Fix within_window logic --- app/models/ci/pipeline_schedule.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index e930dea9f0738a..881254f3b73389 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -147,7 +147,7 @@ def within_window?(old_next_run_at) return true if scheduled_pipeline_skip_window.to_i == 0 scheduled_pipeline_skip_window.to_i > 0 && - (old_next_run_at + scheduled_pipeline_skip_window.to_i).past? + (old_next_run_at + scheduled_pipeline_skip_window.to_i).future? end private -- GitLab From 530959e1a3dfe00e06ad676cf8cc88feee8c9326 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 09:36:46 +0200 Subject: [PATCH 14/22] Set migration default to nil --- ...d_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb | 2 +- db/structure.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb b/db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb index 6b7eaa1fd428c7..6939bb53c559e2 100644 --- a/db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb +++ b/db/migrate/20250828154201_add_scheduled_pipeline_skip_window_to_ci_pipeline_schedules.rb @@ -4,6 +4,6 @@ class AddScheduledPipelineSkipWindowToCiPipelineSchedules < Gitlab::Database::Mi milestone '18.4' def change - add_column :ci_pipeline_schedules, :scheduled_pipeline_skip_window, :integer, default: 0 + add_column :ci_pipeline_schedules, :scheduled_pipeline_skip_window, :integer, default: nil end end diff --git a/db/structure.sql b/db/structure.sql index 27d55a42c1ed72..732dbdc572795a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13251,7 +13251,7 @@ CREATE TABLE ci_pipeline_schedules ( active boolean DEFAULT true, created_at timestamp without time zone, updated_at timestamp without time zone, - scheduled_pipeline_skip_window integer DEFAULT 0, + scheduled_pipeline_skip_window integer, CONSTRAINT check_4a0f7b994d CHECK ((project_id IS NOT NULL)) ); -- GitLab From 53d620fc08758fbb538a0df1c0847d286373fe93 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 10:23:48 +0200 Subject: [PATCH 15/22] Fix frontend fixture after migration change --- .../components/pipeline_schedules_form_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js index e64f5709005611..4eaaea2785791d 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js @@ -502,7 +502,7 @@ describe('Pipeline schedules form', () => { cronTimezone: schedule.cronTimezone, id: schedule.id, ref: schedule.ref, - scheduledPipelineSkipWindow: 0, + scheduledPipelineSkipWindow: null, description: 'Updated schedule', variables: updatedVariables, inputs: updatedInputs, -- GitLab From 6fa3bebe0dba1f05106ea2ec4b82e4ca66e84d76 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 11:01:34 +0200 Subject: [PATCH 16/22] Fix within_window spec --- spec/workers/run_pipeline_schedule_worker_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb index 5f57fff7e5e7c0..146811740e0f47 100644 --- a/spec/workers/run_pipeline_schedule_worker_spec.rb +++ b/spec/workers/run_pipeline_schedule_worker_spec.rb @@ -146,10 +146,11 @@ end before do - pipeline_schedule.update!(next_run_at: 3.days.ago, scheduled_pipeline_skip_window: 1) + pipeline_schedule.update!(scheduled_pipeline_skip_window: 1) allow(ServiceResponse).to receive(:error) .with(message: 'Pipeline schedule runnable window exceeded.') .and_return(error_response) + allow(Time).to receive(:now).and_return(Time.now.utc + 30.days) end it "returns the a timeout response" do -- GitLab From 7c14d9436a0a49f578730b84d9fd3da9999dcbf1 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 16:11:41 +0200 Subject: [PATCH 17/22] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Florian Jedelhauser --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index a8c1dcc33ea902..95589cfe7a47d5 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -902,7 +902,7 @@ def within_scheduled_window_after_creation? return true unless scheduled? return true if pipeline_schedule.scheduled_pipeline_skip_window.to_i == 0 - (created_at + pipeline_schedule.scheduled_pipeline_skip_window).future? + (created_at + pipeline_schedule.scheduled_pipeline_skip_window.to_i).future? end def has_yaml_errors? -- GitLab From 36df8adb6435ed1ca6eb5af0eff3e588d3c05b44 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 16:48:13 +0200 Subject: [PATCH 18/22] Remove redundant check for scheduled state --- .../ci/pipeline_processing/atomic_processing_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index e9c06da87fcd69..a0f4474f57c82f 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -20,7 +20,7 @@ def execute return unless pipeline.needs_processing? if Feature.enabled?(:scheduled_pipeline_skip_window, - pipeline.project) && (pipeline.scheduled? && !pipeline.within_scheduled_window_after_creation?) + pipeline.project) && !pipeline.within_scheduled_window_after_creation? pipeline.skip return end -- GitLab From 3275c2908e0db9fb8e059e50d6dc106293aed201 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 17:32:14 +0200 Subject: [PATCH 19/22] Remove frontend part to split this into two MRs --- .../components/pipeline_schedules_form.vue | 33 ------------------- .../get_pipeline_schedules.query.graphql | 1 - .../api/schemas/pipeline_schedule.json | 3 -- .../pipeline_schedules_form_spec.js | 2 -- 4 files changed, 39 deletions(-) diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue index f973674dc04e6b..b8beba01915f01 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue @@ -94,7 +94,6 @@ export default { this.cronTimezone = schedule.cronTimezone; this.savedInputs = schedule.inputs?.nodes || []; this.scheduleRef = schedule.ref || this.defaultBranch; - this.scheduledPipelineSkipWindow = schedule.scheduledPipelineSkipWindow; this.variables = variables.map((variable) => { return { id: variable.id, @@ -120,7 +119,6 @@ export default { activated: true, cron: '', cronTimezone: '', - scheduledPipelineSkipWindow: null, description: '', pipelineInputs: [], savedInputs: [], @@ -146,10 +144,6 @@ export default { createScheduleBtnText: s__('PipelineSchedules|Create pipeline schedule'), cancel: __('Cancel'), targetBranchTag: __('Select target branch or tag'), - scheduledPipelineSkipWindow: __('Scheduled pipeline skip window'), - scheduledPipelineSkipWindowDescription: s__( - 'PipelineSchedules|Define a custom pipeline execution window that allows the pipeline to run for a given amount of time after its scheduled time. This window must be specified in seconds (86,400 seconds equal one day).', - ), intervalPattern: s__('PipelineSchedules|Interval Pattern'), scheduleCreateError: s__( @@ -163,14 +157,6 @@ export default { ), }, computed: { - scheduledPipelineSkipWindowNumber: { - get() { - return this.scheduledPipelineSkipWindow; - }, - set(value) { - this.scheduledPipelineSkipWindow = value !== '' ? Number(value) : null; - }, - }, dropdownTranslations() { return { dropdownHeader: this.$options.i18n.targetBranchTag, @@ -229,7 +215,6 @@ export default { cronTimezone: this.cronTimezone, ref: this.scheduleRef, variables: this.preparedVariablesCreate, - scheduledPipelineSkipWindow: this.scheduledPipelineSkipWindow, active: this.activated, projectPath: this.projectPath, inputs: this.pipelineInputs, @@ -261,7 +246,6 @@ export default { description: this.description, cron: this.cron, cronTimezone: this.cronTimezone, - scheduledPipelineSkipWindow: this.scheduledPipelineSkipWindow, ref: this.scheduleRef, variables: this.preparedVariablesUpdate, active: this.activated, @@ -378,23 +362,6 @@ export default { class="gl-w-full" /> - - -

- {{ $options.i18n.scheduledPipelineSkipWindowDescription }} -

- -
{ cronTimezone: 'America/New_York', description: 'My schedule', projectPath: 'gitlab-org/gitlab', - scheduledPipelineSkipWindow: null, ref: 'main', variables: updatedVariables, inputs: updatedInputs, @@ -502,7 +501,6 @@ describe('Pipeline schedules form', () => { cronTimezone: schedule.cronTimezone, id: schedule.id, ref: schedule.ref, - scheduledPipelineSkipWindow: null, description: 'Updated schedule', variables: updatedVariables, inputs: updatedInputs, -- GitLab From 1b74a01260bb6fa11347a25a57068cca5ec66e9e Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Fri, 19 Sep 2025 17:57:36 +0200 Subject: [PATCH 20/22] Remove not yet used translations --- locale/gitlab.pot | 9 --------- 1 file changed, 9 deletions(-) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 760413d00621f0..602843c478d8cb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22103,9 +22103,6 @@ msgstr "" msgid "Define a custom pattern with cron syntax" msgstr "" -msgid "Define a custom pipeline execution window (in seconds)" -msgstr "" - msgid "Define custom rules for what constitutes spam, independent of Akismet" msgstr "" @@ -47616,9 +47613,6 @@ msgstr "" msgid "PipelineSchedules|Cron timezone" msgstr "" -msgid "PipelineSchedules|Define a custom pipeline execution window that allows the pipeline to run for a given amount of time after its scheduled time. This window must be specified in seconds (86,400 seconds equal one day)." -msgstr "" - msgid "PipelineSchedules|Delete scheduled pipeline" msgstr "" @@ -57096,9 +57090,6 @@ msgstr "" msgid "Scheduled a rebase of branch %{branch}." msgstr "" -msgid "Scheduled pipeline skip window" -msgstr "" - msgid "Scheduled pipelines" msgstr "" -- GitLab From 3cf9dc17bd529470207775a1b92c6e6cef615ca4 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Mon, 22 Sep 2025 11:44:46 +0200 Subject: [PATCH 21/22] Revert "Remove not yet used translations" This reverts commit 1b74a01260bb6fa11347a25a57068cca5ec66e9e. --- locale/gitlab.pot | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 602843c478d8cb..760413d00621f0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22103,6 +22103,9 @@ msgstr "" msgid "Define a custom pattern with cron syntax" msgstr "" +msgid "Define a custom pipeline execution window (in seconds)" +msgstr "" + msgid "Define custom rules for what constitutes spam, independent of Akismet" msgstr "" @@ -47613,6 +47616,9 @@ msgstr "" msgid "PipelineSchedules|Cron timezone" msgstr "" +msgid "PipelineSchedules|Define a custom pipeline execution window that allows the pipeline to run for a given amount of time after its scheduled time. This window must be specified in seconds (86,400 seconds equal one day)." +msgstr "" + msgid "PipelineSchedules|Delete scheduled pipeline" msgstr "" @@ -57090,6 +57096,9 @@ msgstr "" msgid "Scheduled a rebase of branch %{branch}." msgstr "" +msgid "Scheduled pipeline skip window" +msgstr "" + msgid "Scheduled pipelines" msgstr "" -- GitLab From 87816c5231563f9ba7b45b18bf0b7b225832e9b4 Mon Sep 17 00:00:00 2001 From: Daniel Prause Date: Mon, 22 Sep 2025 11:44:50 +0200 Subject: [PATCH 22/22] Revert "Remove frontend part to split this into two MRs" This reverts commit 3275c2908e0db9fb8e059e50d6dc106293aed201. --- .../components/pipeline_schedules_form.vue | 33 +++++++++++++++++++ .../get_pipeline_schedules.query.graphql | 1 + .../api/schemas/pipeline_schedule.json | 3 ++ .../pipeline_schedules_form_spec.js | 2 ++ 4 files changed, 39 insertions(+) diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue index b8beba01915f01..f973674dc04e6b 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue @@ -94,6 +94,7 @@ export default { this.cronTimezone = schedule.cronTimezone; this.savedInputs = schedule.inputs?.nodes || []; this.scheduleRef = schedule.ref || this.defaultBranch; + this.scheduledPipelineSkipWindow = schedule.scheduledPipelineSkipWindow; this.variables = variables.map((variable) => { return { id: variable.id, @@ -119,6 +120,7 @@ export default { activated: true, cron: '', cronTimezone: '', + scheduledPipelineSkipWindow: null, description: '', pipelineInputs: [], savedInputs: [], @@ -144,6 +146,10 @@ export default { createScheduleBtnText: s__('PipelineSchedules|Create pipeline schedule'), cancel: __('Cancel'), targetBranchTag: __('Select target branch or tag'), + scheduledPipelineSkipWindow: __('Scheduled pipeline skip window'), + scheduledPipelineSkipWindowDescription: s__( + 'PipelineSchedules|Define a custom pipeline execution window that allows the pipeline to run for a given amount of time after its scheduled time. This window must be specified in seconds (86,400 seconds equal one day).', + ), intervalPattern: s__('PipelineSchedules|Interval Pattern'), scheduleCreateError: s__( @@ -157,6 +163,14 @@ export default { ), }, computed: { + scheduledPipelineSkipWindowNumber: { + get() { + return this.scheduledPipelineSkipWindow; + }, + set(value) { + this.scheduledPipelineSkipWindow = value !== '' ? Number(value) : null; + }, + }, dropdownTranslations() { return { dropdownHeader: this.$options.i18n.targetBranchTag, @@ -215,6 +229,7 @@ export default { cronTimezone: this.cronTimezone, ref: this.scheduleRef, variables: this.preparedVariablesCreate, + scheduledPipelineSkipWindow: this.scheduledPipelineSkipWindow, active: this.activated, projectPath: this.projectPath, inputs: this.pipelineInputs, @@ -246,6 +261,7 @@ export default { description: this.description, cron: this.cron, cronTimezone: this.cronTimezone, + scheduledPipelineSkipWindow: this.scheduledPipelineSkipWindow, ref: this.scheduleRef, variables: this.preparedVariablesUpdate, active: this.activated, @@ -362,6 +378,23 @@ export default { class="gl-w-full" /> + + +

+ {{ $options.i18n.scheduledPipelineSkipWindowDescription }} +

+ +
{ cronTimezone: 'America/New_York', description: 'My schedule', projectPath: 'gitlab-org/gitlab', + scheduledPipelineSkipWindow: null, ref: 'main', variables: updatedVariables, inputs: updatedInputs, @@ -501,6 +502,7 @@ describe('Pipeline schedules form', () => { cronTimezone: schedule.cronTimezone, id: schedule.id, ref: schedule.ref, + scheduledPipelineSkipWindow: null, description: 'Updated schedule', variables: updatedVariables, inputs: updatedInputs, -- GitLab