From cc000b48848916dcee2564e22bc2dfdff8f49729 Mon Sep 17 00:00:00 2001
From: Timo Furrer <tfurrer@gitlab.com>
Date: Wed, 13 Nov 2024 17:57:52 +0100
Subject: [PATCH] Support running pipeline templates directly in child pipeline
 with `trigger_in_child_pipeline` input

This change set allows a user of this component to directly execute a
pipeline template in its own child pipeline. This is useful to run
dedicated child pipeline for multiple environments.

Changelog: added
---
 .gitlab/README.md.template                    | 15 ++++
 README.md                                     | 15 ++++
 templates/full-pipeline.yml                   | 87 ++++++++++++++++++-
 templates/validate-plan-apply.yml             | 71 +++++++++++++++
 templates/validate-plan-destroy.yml           | 74 ++++++++++++++++
 templates/validate-plan.yml                   | 66 ++++++++++++++
 .../TriggerInChildPipeline.gitlab-ci.yml      | 25 ++++++
 tests/integration.gitlab-ci.yml               |  1 +
 8 files changed, 351 insertions(+), 3 deletions(-)
 create mode 100644 tests/integration-tests/TriggerInChildPipeline.gitlab-ci.yml

diff --git a/.gitlab/README.md.template b/.gitlab/README.md.template
index d48a45e..1930a95 100644
--- a/.gitlab/README.md.template
+++ b/.gitlab/README.md.template
@@ -44,6 +44,17 @@ include:
       opentofu_version: <OPENTOFU_VERSION>
 
 stages: [validate, build, deploy]
+
+# ... or in a child pipeline:
+include:
+  - component: $CI_SERVER_FQDN/components/opentofu/validate-plan-apply@<VERSION>
+    inputs:
+      # The version must currently be specified explicitly as an input,
+      # to find the correctly associated images. # This can be removed
+      # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
+      version: <VERSION> # component version
+      opentofu_version: <OPENTOFU_VERSION>
+      trigger_in_child_pipeline: true
 ```
 
 A concrete example may look like this:
@@ -158,6 +169,10 @@ but no destructive actions.
 - [`validate-plan-apply`](templates/validate-plan-apply.yml)
 - [`validate-plan-destroy`](templates/validate-plan-destroy.yml)
 
+These templates support the `trigger_in_child_pipeline` input which will
+include the component but run all its job in a child pipeline. This may be useful in cases
+where you want to run dedicated child pipeline for each of your environments.
+
 ### Job Templates
 
 Instead of including the `full-pipeline` or another opinionated template,
diff --git a/README.md b/README.md
index 11e74bb..9a43bc7 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,17 @@ include:
       opentofu_version: <OPENTOFU_VERSION>
 
 stages: [validate, build, deploy]
+
+# ... or in a child pipeline:
+include:
+  - component: $CI_SERVER_FQDN/components/opentofu/validate-plan-apply@<VERSION>
+    inputs:
+      # The version must currently be specified explicitly as an input,
+      # to find the correctly associated images. # This can be removed
+      # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
+      version: <VERSION> # component version
+      opentofu_version: <OPENTOFU_VERSION>
+      trigger_in_child_pipeline: true
 ```
 
 A concrete example may look like this:
@@ -160,6 +171,10 @@ but no destructive actions.
 - [`validate-plan-apply`](templates/validate-plan-apply.yml)
 - [`validate-plan-destroy`](templates/validate-plan-destroy.yml)
 
+These templates support the `trigger_in_child_pipeline` input which will
+include the component but run all its job in a child pipeline. This may be useful in cases
+where you want to run dedicated child pipeline for each of your environments.
+
 ### Job Templates
 
 Instead of including the `full-pipeline` or another opinionated template,
diff --git a/templates/full-pipeline.yml b/templates/full-pipeline.yml
index 3146971..3baa72f 100644
--- a/templates/full-pipeline.yml
+++ b/templates/full-pipeline.yml
@@ -159,11 +159,25 @@ spec:
       default: false
       type: boolean
       description: 'Whether to mark the job with a warning if the plan contains a diff.'
+    trigger_in_child_pipeline:
+      default: false
+      type: boolean
+      description: 'Whether to run all the jobs in a child pipeline.'
+    child_pipeline_name:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the name of the child pipeline bridge job.'
+    child_pipeline_stage:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the stage of the child pipeline bridge job.'
 
 ---
 
 include:
   - local: '/templates/fmt.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]fmt'
       stage: $[[ inputs.stage_validate ]]
@@ -177,6 +191,8 @@ include:
       needs: []
       rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
       stage: $[[ inputs.stage_validate ]]
@@ -191,6 +207,11 @@ include:
       var_file: $[[ inputs.var_file ]]
       rules: $[[ inputs.validate_rules ]]
   - local: '/templates/test.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "true"'
+        when: never
+      - exists:
+          - $[[ inputs.root_dir ]]/**/*.tftest.hcl
     inputs:
       as: '$[[ inputs.job_name_prefix ]]test'
       stage: $[[ inputs.stage_test ]]
@@ -205,10 +226,9 @@ include:
       var_file: $[[ inputs.var_file ]]
       needs: []
       rules: $[[ inputs.test_rules ]]
-    rules:
-      - exists:
-          - $[[ inputs.root_dir ]]/**/*.tftest.hcl
   - local: '/templates/plan.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
       stage: $[[ inputs.stage_build ]]
@@ -226,6 +246,8 @@ include:
       rules: $[[ inputs.plan_rules ]]
       warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
   - local: '/templates/apply.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]apply'
       stage: $[[ inputs.stage_deploy ]]
@@ -241,6 +263,8 @@ include:
       var_file: $[[ inputs.var_file ]]
       rules: $[[ inputs.apply_rules ]]
   - local: '/templates/destroy.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]destroy'
       stage: $[[ inputs.stage_cleanup ]]
@@ -255,6 +279,8 @@ include:
       var_file: $[[ inputs.var_file ]]
       rules: $[[ inputs.destroy_rules ]]
   - local: '/templates/delete-state.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]delete-state'
       stage: $[[ inputs.stage_cleanup ]]
@@ -267,3 +293,58 @@ include:
 # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
 '$[[ inputs.job_name_prefix ]]delete-state':
   needs: ['$[[ inputs.job_name_prefix ]]destroy']
+
+
+# NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
+stages:
+  - $[[ inputs.stage_validate ]]
+  - $[[ inputs.stage_build ]]
+  - $[[ inputs.stage_deploy ]]
+  - $[[ inputs.stage_cleanup ]]
+  - $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:true':
+  stage: $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:false':
+  # NOTE: this is a hack for the bridge job below to have a valid stage even if it isn't used.
+  # The thing is that the stage needs to be defined even if the job is "ruled" out and not created.
+  # The .pre stage always exists.
+  stage: .pre
+
+'$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]':
+  rules:
+    - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "true"'
+  extends: '.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:$[[ inputs.trigger_in_child_pipeline ]]'
+  trigger:
+    include:
+      - local: /templates/validate-plan-apply.yml
+        inputs:
+          stage_validate: $[[ inputs.stage_validate ]]
+          stage_build: $[[ inputs.stage_build ]]
+          stage_deploy: $[[ inputs.stage_deploy ]]
+          stage_cleanup: $[[ inputs.stage_cleanup ]]
+          version: $[[ inputs.version ]]
+          base_os: $[[ inputs.base_os ]]
+          opentofu_version: $[[ inputs.opentofu_version ]]
+          image_registry_base: $[[ inputs.image_registry_base ]]
+          image_name: $[[ inputs.image_name]]
+          image_digest: $[[ inputs.image_digest]]
+          job_name_prefix: $[[ inputs.job_name_prefix]]
+          root_dir: $[[ inputs.root_dir]]
+          state_name: $[[ inputs.state_name]]
+          plan_name: $[[ inputs.plan_name ]]
+          plan_artifacts_access: $[[ inputs.plan_artifacts_access ]]
+          var_file: $[[ inputs.var_file ]]
+          fmt_rules: $[[ inputs.fmt_rules ]]
+          validate_rules: $[[ inputs.validate_rules ]]
+          plan_rules: $[[ inputs.plan_rules ]]
+          apply_rules: $[[ inputs.apply_rules ]]
+          destroy_rules: $[[ inputs.destroy_rules ]]
+          delete_state_rules: $[[ inputs.delete_state_rules ]]
+          warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
+          trigger_in_child_pipeline: false
+    forward:
+      yaml_variables: true
+      pipeline_variables: true
+    strategy: depend
diff --git a/templates/validate-plan-apply.yml b/templates/validate-plan-apply.yml
index 555574e..05dfc7a 100644
--- a/templates/validate-plan-apply.yml
+++ b/templates/validate-plan-apply.yml
@@ -126,11 +126,25 @@ spec:
       default: false
       type: boolean
       description: 'Whether to mark the job with a warning if the plan contains a diff.'
+    trigger_in_child_pipeline:
+      default: false
+      type: boolean
+      description: 'Whether to run all the jobs in a child pipeline.'
+    child_pipeline_name:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the name of the child pipeline bridge job.'
+    child_pipeline_stage:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the stage of the child pipeline bridge job.'
 
 ---
 
 include:
   - local: '/templates/fmt.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]fmt'
       stage: $[[ inputs.stage_validate ]]
@@ -144,6 +158,8 @@ include:
       needs: []
       rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
       stage: $[[ inputs.stage_validate ]]
@@ -159,6 +175,8 @@ include:
       rules: $[[ inputs.validate_rules ]]
       cache_policy: pull-push
   - local: '/templates/plan.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
       stage: $[[ inputs.stage_build ]]
@@ -177,6 +195,8 @@ include:
       cache_policy: pull
       warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
   - local: '/templates/apply.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]apply'
       stage: $[[ inputs.stage_deploy ]]
@@ -192,3 +212,54 @@ include:
       var_file: $[[ inputs.var_file ]]
       rules: $[[ inputs.apply_rules ]]
       cache_policy: pull
+
+
+# NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
+stages:
+  - $[[ inputs.stage_validate ]]
+  - $[[ inputs.stage_build ]]
+  - $[[ inputs.stage_deploy ]]
+  - $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:true':
+  stage: $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:false':
+  # NOTE: this is a hack for the bridge job below to have a valid stage even if it isn't used.
+  # The thing is that the stage needs to be defined even if the job is "ruled" out and not created.
+  # The .pre stage always exists.
+  stage: .pre
+
+'$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]':
+  rules:
+    - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "true"'
+  extends: '.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:$[[ inputs.trigger_in_child_pipeline ]]'
+  trigger:
+    include:
+      - local: /templates/validate-plan-apply.yml
+        inputs:
+          stage_validate: $[[ inputs.stage_validate ]]
+          stage_build: $[[ inputs.stage_build ]]
+          stage_deploy: $[[ inputs.stage_deploy ]]
+          version: $[[ inputs.version ]]
+          base_os: $[[ inputs.base_os ]]
+          opentofu_version: $[[ inputs.opentofu_version ]]
+          image_registry_base: $[[ inputs.image_registry_base ]]
+          image_name: $[[ inputs.image_name]]
+          image_digest: $[[ inputs.image_digest]]
+          job_name_prefix: $[[ inputs.job_name_prefix]]
+          root_dir: $[[ inputs.root_dir]]
+          state_name: $[[ inputs.state_name]]
+          plan_name: $[[ inputs.plan_name ]]
+          plan_artifacts_access: $[[ inputs.plan_artifacts_access ]]
+          var_file: $[[ inputs.var_file ]]
+          fmt_rules: $[[ inputs.fmt_rules ]]
+          validate_rules: $[[ inputs.validate_rules ]]
+          plan_rules: $[[ inputs.plan_rules ]]
+          apply_rules: $[[ inputs.apply_rules ]]
+          warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
+          trigger_in_child_pipeline: false
+    forward:
+      yaml_variables: true
+      pipeline_variables: true
+    strategy: depend
diff --git a/templates/validate-plan-destroy.yml b/templates/validate-plan-destroy.yml
index 40cde9b..1c9281a 100644
--- a/templates/validate-plan-destroy.yml
+++ b/templates/validate-plan-destroy.yml
@@ -132,11 +132,25 @@ spec:
       default: false
       type: boolean
       description: 'Whether to mark the job with a warning if the plan contains a diff.'
+    trigger_in_child_pipeline:
+      default: false
+      type: boolean
+      description: 'Whether to run all the jobs in a child pipeline.'
+    child_pipeline_name:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the name of the child pipeline bridge job.'
+    child_pipeline_stage:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the stage of the child pipeline bridge job.'
 
 ---
 
 include:
   - local: '/templates/fmt.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]fmt'
       stage: $[[ inputs.stage_validate ]]
@@ -150,6 +164,8 @@ include:
       needs: []
       rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
       stage: $[[ inputs.stage_validate ]]
@@ -165,6 +181,8 @@ include:
       rules: $[[ inputs.validate_rules ]]
       cache_policy: pull-push
   - local: '/templates/plan.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
       stage: $[[ inputs.stage_build ]]
@@ -184,6 +202,8 @@ include:
       cache_policy: pull
       warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
   - local: '/templates/destroy.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]destroy'
       stage: $[[ inputs.stage_cleanup ]]
@@ -201,6 +221,8 @@ include:
       rules: $[[ inputs.destroy_rules ]]
       cache_policy: pull
   - local: '/templates/delete-state.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]delete-state'
       stage: $[[ inputs.stage_cleanup ]]
@@ -213,3 +235,55 @@ include:
 # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
 '$[[ inputs.job_name_prefix ]]delete-state':
   needs: ['$[[ inputs.job_name_prefix ]]destroy']
+
+
+# NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
+stages:
+  - $[[ inputs.stage_validate ]]
+  - $[[ inputs.stage_build ]]
+  - $[[ inputs.stage_cleanup ]]
+  - $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:true':
+  stage: $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:false':
+  # NOTE: this is a hack for the bridge job below to have a valid stage even if it isn't used.
+  # The thing is that the stage needs to be defined even if the job is "ruled" out and not created.
+  # The .pre stage always exists.
+  stage: .pre
+
+'$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]':
+  rules:
+    - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "true"'
+  extends: '.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:$[[ inputs.trigger_in_child_pipeline ]]'
+  trigger:
+    include:
+      - local: /templates/validate-plan-destroy.yml
+        inputs:
+          stage_validate: $[[ inputs.stage_validate ]]
+          stage_build: $[[ inputs.stage_build ]]
+          stage_cleanup: $[[ inputs.stage_cleanup ]]
+          version: $[[ inputs.version ]]
+          base_os: $[[ inputs.base_os ]]
+          opentofu_version: $[[ inputs.opentofu_version ]]
+          image_registry_base: $[[ inputs.image_registry_base ]]
+          image_name: $[[ inputs.image_name]]
+          image_digest: $[[ inputs.image_digest]]
+          job_name_prefix: $[[ inputs.job_name_prefix]]
+          root_dir: $[[ inputs.root_dir]]
+          state_name: $[[ inputs.state_name]]
+          plan_name: $[[ inputs.plan_name ]]
+          plan_artifacts_access: $[[ inputs.plan_artifacts_access ]]
+          var_file: $[[ inputs.var_file ]]
+          fmt_rules: $[[ inputs.fmt_rules ]]
+          validate_rules: $[[ inputs.validate_rules ]]
+          plan_rules: $[[ inputs.plan_rules ]]
+          destroy_rules: $[[ inputs.destroy_rules ]]
+          delete_state_rules: $[[ inputs.delete_state_rules ]]
+          warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
+          trigger_in_child_pipeline: false
+    forward:
+      yaml_variables: true
+      pipeline_variables: true
+    strategy: depend
diff --git a/templates/validate-plan.yml b/templates/validate-plan.yml
index ecf271a..238fa18 100644
--- a/templates/validate-plan.yml
+++ b/templates/validate-plan.yml
@@ -110,11 +110,25 @@ spec:
       default: false
       type: boolean
       description: 'Whether to mark the job with a warning if the plan contains a diff.'
+    trigger_in_child_pipeline:
+      default: false
+      type: boolean
+      description: 'Whether to run all the jobs in a child pipeline.'
+    child_pipeline_name:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the name of the child pipeline bridge job.'
+    child_pipeline_stage:
+      default: opentofu
+      type: string
+      description: 'If `trigger_in_child_pipeline` is `true` then this defines the stage of the child pipeline bridge job.'
 
 ---
 
 include:
   - local: '/templates/fmt.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]fmt'
       stage: $[[ inputs.stage_validate ]]
@@ -128,6 +142,8 @@ include:
       needs: []
       rules: $[[ inputs.fmt_rules ]]
   - local: '/templates/validate.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]validate'
       stage: $[[ inputs.stage_validate ]]
@@ -143,6 +159,8 @@ include:
       rules: $[[ inputs.validate_rules ]]
       cache_policy: pull-push
   - local: '/templates/plan.yml'
+    rules:
+      - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
       stage: $[[ inputs.stage_build ]]
@@ -160,3 +178,51 @@ include:
       rules: $[[ inputs.plan_rules ]]
       cache_policy: pull
       warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
+
+
+# NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
+stages:
+  - $[[ inputs.stage_validate ]]
+  - $[[ inputs.stage_build ]]
+  - $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:true':
+  stage: $[[ inputs.child_pipeline_stage ]]
+
+'.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:false':
+  # NOTE: this is a hack for the bridge job below to have a valid stage even if it isn't used.
+  # The thing is that the stage needs to be defined even if the job is "ruled" out and not created.
+  # The .pre stage always exists.
+  stage: .pre
+
+'$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]':
+  rules:
+    - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "true"'
+  extends: '.$[[ inputs.job_name_prefix ]]$[[ inputs.child_pipeline_name ]]:bridge_job_stage:$[[ inputs.trigger_in_child_pipeline ]]'
+  trigger:
+    include:
+      - local: /templates/validate-plan.yml
+        inputs:
+          stage_validate: $[[ inputs.stage_validate ]]
+          stage_build: $[[ inputs.stage_build ]]
+          version: $[[ inputs.version ]]
+          base_os: $[[ inputs.base_os ]]
+          opentofu_version: $[[ inputs.opentofu_version ]]
+          image_registry_base: $[[ inputs.image_registry_base ]]
+          image_name: $[[ inputs.image_name]]
+          image_digest: $[[ inputs.image_digest]]
+          job_name_prefix: $[[ inputs.job_name_prefix]]
+          root_dir: $[[ inputs.root_dir]]
+          state_name: $[[ inputs.state_name]]
+          plan_name: $[[ inputs.plan_name ]]
+          artifacts_access: $[[ inputs.artifacts_access ]]
+          var_file: $[[ inputs.var_file ]]
+          fmt_rules: $[[ inputs.fmt_rules ]]
+          validate_rules: $[[ inputs.validate_rules ]]
+          plan_rules: $[[ inputs.plan_rules ]]
+          warning_on_non_empty_plan: $[[ inputs.warning_on_non_empty_plan ]]
+          trigger_in_child_pipeline: false
+    forward:
+      yaml_variables: true
+      pipeline_variables: true
+    strategy: depend
diff --git a/tests/integration-tests/TriggerInChildPipeline.gitlab-ci.yml b/tests/integration-tests/TriggerInChildPipeline.gitlab-ci.yml
new file mode 100644
index 0000000..d1fd3cf
--- /dev/null
+++ b/tests/integration-tests/TriggerInChildPipeline.gitlab-ci.yml
@@ -0,0 +1,25 @@
+include:
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/validate-plan-apply@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_TF_ROOT
+      state_name: $TEST_TF_STATE_NAME
+      fmt_rules: [{when: on_success}]
+      validate_rules: [{when: on_success}]
+      plan_rules: [{when: on_success}]
+      apply_rules: [{when: on_success}]
+      trigger_in_child_pipeline: true
+      child_pipeline_name: test-pipeline
+      child_pipeline_stage: test
+
+  # For CI Terraform state cleanup
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/delete-state@$CI_COMMIT_SHA
+    inputs:
+      stage: cleanup
+      state_name: $TEST_TF_STATE_NAME
+      rules: [{when: always}]
+
+stages: [test, cleanup]
diff --git a/tests/integration.gitlab-ci.yml b/tests/integration.gitlab-ci.yml
index 0ab3012..83ee35c 100644
--- a/tests/integration.gitlab-ci.yml
+++ b/tests/integration.gitlab-ci.yml
@@ -17,6 +17,7 @@ component:
           - Destroy
           - VarFile
           - WarningOnNonEmptyPlan
+          - TriggerInChildPipeline
         GITLAB_OPENTOFU_BASE_IMAGE_OS:
           - alpine
           - debian
-- 
GitLab