[backend] Add pipeline execution schedule policy branch filter

Following the introduction of the pipeline execution policy, which allows for enforcing CI jobs/scripts within triggered pipelines, we plan to extend support for scheduled enforcement.

Like scheduled scan execution policies (SEP), we want to be able to start pipelines on specific branches. For SEPs there are two options:

  • branch_type which is either all, protected or `default.
  • branches which is an array of literal branch names, limited to 5.

We need to analyze the risk of triggering too many pipelines and add limits to branch_type: all and branches.

Implementation Plan

Schema change

Add branch/branch_type fields to schedule objects (see the schema change for schedules).

schedules:
  - type: daily
    branch_type: default
    start_time: "05:00"
    end_time: "11:00"
  - type: weekly
    branches: [rc-*]
    days:
      - Monday
      - Friday
    start_time: "02:00"
    end_time: "04:00"

backend

diff --git a/ee/app/models/security/pipeline_execution_project_schedule.rb b/ee/app/models/security/pipeline_execution_project_schedule.rb
index c36452f9e1d0..7e7df00d5051 100644
--- a/ee/app/models/security/pipeline_execution_project_schedule.rb
+++ b/ee/app/models/security/pipeline_execution_project_schedule.rb
@@ -47,8 +47,20 @@ def snoozed?
       snoozed_until.future?
     end
 
+    def branches
+      return [project.default_branch_or_main] if branches_content.nil?
+
+      branches_content.select { |branch| project.repository.branch_names.include?(branch) }
+    end
+
     private
 
+    def branches_content
+      schedule_content = security_policy.content['schedules']&.first || {}
+
+      schedule_content['branches']
+    end
+
     def timezone
       security_policy.content.dig('schedule', 'timezone')
     end
diff --git a/ee/app/validators/json_schemas/pipeline_execution_schedule_policy_content.json b/ee/app/validators/json_schemas/pipeline_execution_schedule_policy_content.json
index 6c4112313c61..09628f86cd0f 100644
--- a/ee/app/validators/json_schemas/pipeline_execution_schedule_policy_content.json
+++ b/ee/app/validators/json_schemas/pipeline_execution_schedule_policy_content.json
@@ -53,6 +53,15 @@
                   "daily"
                 ]
               },
+              "branches": {
+                "type": "array",
+                "description": "List of branches to run pipelines for (max 5). Pipelines will only be created for branches that exist in the project.",
+                "maxItems": 5,
+                "uniqueItems": true,
+                "items": {
+                  "type": "string"
+                }
+              },
               "start_time": {
                 "type": "string",
                 "description": "HH:mm format",
@@ -137,6 +146,15 @@
                 },
                 "description": "List of days, e.g. ['Monday', 'Friday']"
               },
+              "branches": {
+                "type": "array",
+                "description": "List of branches to run pipelines for (max 5). Pipelines will only be created for branches that exist in the project.",
+                "maxItems": 5,
+                "uniqueItems": true,
+                "items": {
+                  "type": "string"
+                }
+              },
               "start_time": {
                 "type": "string",
                 "description": "HH:mm format",
@@ -215,6 +233,15 @@
                 },
                 "description": "Dates within the month, e.g. [3, 10, 17]"
               },
+              "branches": {
+                "type": "array",
+                "description": "List of branches to run pipelines for (max 5). Pipelines will only be created for branches that exist in the project.",
+                "maxItems": 5,
+                "uniqueItems": true,
+                "items": {
+                  "type": "string"
+                }
+              },
               "start_time": {
                 "type": "string",
                 "description": "HH:mm format",
diff --git a/ee/app/workers/security/pipeline_execution_policies/run_schedule_worker.rb b/ee/app/workers/security/pipeline_execution_policies/run_schedule_worker.rb
index 4701e85a05e2..5c00fc21c8f6 100644
--- a/ee/app/workers/security/pipeline_execution_policies/run_schedule_worker.rb
+++ b/ee/app/workers/security/pipeline_execution_policies/run_schedule_worker.rb
@@ -26,22 +26,26 @@ def perform(schedule_id)
           return
         end
 
-        result = execute(schedule)
+        ci_content = schedule.ci_content.deep_stringify_keys.to_yaml
 
-        log_pipeline_creation_failure(result, schedule) if result.error?
+        schedule.branches.each do |branch|
+          result = create_pipeline(schedule, ci_content, branch)
+          track_pipeline_creation_event(schedule, result)
+          log_pipeline_creation_failure(result, schedule) if result.error?
+        end
       end
 
       private
 
-      def execute(schedule)
-        ci_content = schedule.ci_content.deep_stringify_keys.to_yaml
-
-        result = Ci::CreatePipelineService.new(
+      def create_pipeline(schedule, ci_content, ref)
+        Ci::CreatePipelineService.new(
           schedule.project,
           schedule.project.security_policy_bot,
-          ref: schedule.project.default_branch_or_main
+          ref: ref
         ).execute(PIPELINE_SOURCE, content: ci_content, ignore_skip_ci: true)
+      end
 
+      def track_pipeline_creation_event(schedule, result)
         ::Gitlab::InternalEvents.track_event(
           'execute_job_scheduled_pipeline_execution_policy',
           project: schedule.project,
@@ -49,8 +53,6 @@ def execute(schedule)
             label: result.status.to_s
           }
         )
-
-        result
       end
 
       def log_pipeline_creation_failure(result, schedule)
@@ -61,7 +63,9 @@ def log_pipeline_creation_failure(result, schedule)
             reason: result.reason,
             project_id: schedule.project_id,
             schedule_id: schedule.id,
-            policy_id: schedule.security_policy.id))
+            policy_id: schedule.security_policy.id
+          )
+        )
       end
 
       def experiment_enabled?(schedule)
Edited by Andy Schoenen