Chapter 16Lesson 02~195 minutes

Merge Request Pipelines, Merged Results Pipelines, Merge Trains, and Pre-Merge Validation: Guided Hands-On Workflow and Core Operations

Create a disposable merge request and compare branch-pipeline evidence with merge-request-pipeline evidence. You will remove a deliberate duplicate-pipeline bug, record the exact MR ref/SHA under test, and simulate merged-result integration locally so the mandatory path remains Free-compatible.

Hands-onMR pipelinesworkflow:rulesFork safetySimulation

Learning objectives

  • Create a disposable merge request whose root configuration intentionally supports merge request pipelines.
  • Compare safe branch and merge-request metadata without printing secrets or broad environment dumps.
  • Use workflow:rules to avoid duplicate branch and MR pipelines for the same source-branch update.
  • Prove the exact commit/ref under test from job and pipeline evidence.
  • Simulate source+target integration locally when merged-results pipelines are unavailable on the mandatory Free path.

1. Disposable lab scope and assumptions

Use a throwaway GitLab project such as glci-mr-lab. Create a default branch main and a feature branch glci/ch16-feature. Do not use a proprietary repository, deployment credentials, protected production runners, or real secrets.

Current assumptions verified 2026-09-11: ordinary merge request pipelines are available on Free/Premium/Ultimate; merged-results pipelines and merge trains are Premium/Ultimate. The mandatory workflow below uses only ordinary MR pipelines plus a local merge simulation.

Preflight: confirm the project is disposable, record its path and default branch, and ensure no valuable CI/CD variables are required. If you test fork behavior, use only fake variables and isolated runners.

2. Create synthetic source with a target/source interaction

The lab needs a change that is easy to identify and easy to merge locally.

mkdir -p src
printf 'mode=base\n' > src/app.conf
git add src/app.conf
git commit -m "ch16: base config"
git push origin main

git switch -c glci/ch16-feature
printf 'mode=feature\n' > src/app.conf
printf 'feature=ch16\n' > src/feature.txt
git add src
git commit -m "ch16: feature change"
git push -u origin glci/ch16-feature

Record the feature commit SHA locally with git rev-parse HEAD. That SHA becomes one prediction to compare with the MR pipeline’s CI_COMMIT_SHA.

3. Start with a deliberate duplicate-pipeline bug

The first configuration is semantically valid but operationally poor: its job rule has an MR rule plus a broad final rule. A push to a branch with an open MR can therefore result in both a branch pipeline and an MR pipeline.

stages: [verify]

evidence:
  image: alpine:3.22.1
  stage: verify
  script:
    - mkdir -p evidence
    - printf 'pipeline_id=%s\n' "$CI_PIPELINE_ID" | tee evidence/context.txt
    - printf 'pipeline_source=%s\n' "$CI_PIPELINE_SOURCE" | tee -a evidence/context.txt
    - printf 'commit_sha=%s\n' "$CI_COMMIT_SHA" | tee -a evidence/context.txt
    - printf 'commit_ref=%s\n' "$CI_COMMIT_REF_NAME" | tee -a evidence/context.txt
  artifacts:
    paths: [evidence/context.txt]
    expire_in: 2 days
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - when: on_success

Push this file and open a merge request from glci/ch16-feature to main. Record every pipeline ID created for the push/MR update. The lesson is not to celebrate two green pipelines; it is to prove why two were created.

4. Compare branch and MR evidence

For each pipeline, record only non-secret predefined metadata. Do not dump the environment. In the MR pipeline, add the MR-specific fields explicitly:

printf 'mr_iid=%s\n' "${CI_MERGE_REQUEST_IID:-not-an-mr}"
printf 'mr_ref=%s\n' "${CI_MERGE_REQUEST_REF_PATH:-not-an-mr}"
printf 'mr_event_type=%s\n' "${CI_MERGE_REQUEST_EVENT_TYPE:-not-an-mr}"
printf 'mr_source_branch=%s\n' "${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME:-not-an-mr}"
printf 'mr_target_branch=%s\n' "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-not-an-mr}"
Field Branch pipeline expectation MR pipeline expectation
CI_PIPELINE_SOURCE push merge_request_event
CI_COMMIT_SHA Feature branch commit for the push Pipeline commit for MR source content
CI_MERGE_REQUEST_IID Empty/unset MR IID
CI_MERGE_REQUEST_REF_PATH Empty/unset refs/merge-requests/<iid>/head style ref path
CI_MERGE_REQUEST_EVENT_TYPE Empty/unset Usually detached for ordinary MR pipeline

5. Repair creation at the workflow layer

The causal problem is pipeline creation, so repair it at workflow:rules rather than adding more job-level exceptions. This allowlist creates MR pipelines for MR updates and default-branch pipelines after merge, but no feature-branch push pipeline in parallel.

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - when: never

stages: [verify]

evidence:
  image: alpine:3.22.1
  stage: verify
  script:
    - mkdir -p evidence
    - printf 'pipeline_id=%s\n' "$CI_PIPELINE_ID" | tee evidence/context.txt
    - printf 'source=%s\n' "$CI_PIPELINE_SOURCE" | tee -a evidence/context.txt
    - printf 'sha=%s\n' "$CI_COMMIT_SHA" | tee -a evidence/context.txt
    - printf 'mr_iid=%s\n' "${CI_MERGE_REQUEST_IID:-none}" | tee -a evidence/context.txt
    - printf 'mr_ref=%s\n' "${CI_MERGE_REQUEST_REF_PATH:-none}" | tee -a evidence/context.txt
    - printf 'event_type=%s\n' "${CI_MERGE_REQUEST_EVENT_TYPE:-none}" | tee -a evidence/context.txt
  artifacts:
    paths: [evidence/context.txt]
    expire_in: 2 days

Push one more harmless commit to the feature branch. Expect one MR pipeline for that update. Preserve the earlier duplicate pipeline IDs as the “before” evidence rather than deleting them.

6. Prove the exact revision under test

Compare three values:

  1. Local feature HEAD: git rev-parse glci/ch16-feature.
  2. MR pipeline CI_COMMIT_SHA recorded in the evidence artifact.
  3. Pipeline details/API SHA for the same pipeline ID.

For an ordinary MR pipeline, the code under test is the source-branch content, so these should represent the same source revision even though GitLab presents it under merge-request pipeline context.

Evidence rule: never infer the tested SHA from the current branch after the fact. A later push can move the branch while the earlier pipeline still points to the old commit.

7. Free-path merged-result simulation: test integration without claiming GitLab merged-results

If your account/project does not have merged-results pipelines, you can still teach the integration question locally. Fetch both refs and create a temporary merge commit in a disposable local clone. The output is a simulation, not a GitLab merged-results pipeline and not merge-train evidence.

git fetch origin main glci/ch16-feature
rm -rf .ch16-merge-sim
git worktree add --detach .ch16-merge-sim origin/main
(
  cd .ch16-merge-sim
  git config user.name "CI Course Simulator"
  git config user.email "ci-course@example.invalid"
  git merge --no-ff --no-edit origin/glci/ch16-feature
  printf 'simulated_merge_sha=%s\n' "$(git rev-parse HEAD)"
  printf 'target_parent=%s\n' "$(git rev-parse HEAD^1)"
  printf 'source_parent=%s\n' "$(git rev-parse HEAD^2)"
  test "$(cat src/app.conf)" = "mode=feature"
)
git worktree remove .ch16-merge-sim

This proves the concept “test source+target integration.” It does not reproduce GitLab’s temporary merged commit identity, project settings, event variables, UI labels, or mergeability engine. Mark all of those as not observed.

8. Optional Premium/Ultimate observation path

If you have an authorized disposable Premium/Ultimate project, enable merged-results pipelines only after the root configuration already supports MR pipelines. Then record the pipeline ID, CI_COMMIT_SHA, CI_MERGE_REQUEST_EVENT_TYPE, source branch SHA, and target branch SHA. The event type should identify the merged-result context.

If merge trains are also enabled, place only disposable merge requests on the train. Record the candidate pipeline’s SHA/event type and its queue position. Do not use a production project merely to complete this lesson.

Tier boundary: merged-results and merge-train observations are optional. The chapter is complete when the Free MR pipeline and local integration simulation are understood and evidenced.

9. Fork trust exercise without exposing parent secrets

Create no real fork if you do not need one. Instead, answer the trust question from the documented model:

  • Where would a fork MR pipeline run by default?
  • Whose CI configuration, runners, and variables would it use?
  • What changes if an authorized parent member runs the fork MR pipeline in the parent project?
  • Why must CI configuration changes be reviewed before doing so?

If you do create a disposable fork, keep every project variable synthetic and never enable protected-resource access just for the exercise.

10. Evidence packet for the hands-on workflow

Repository

Disposable project path + default branch

Merge request

IID + source/target project/branch

Before repair

Duplicate branch/MR pipeline IDs and sources

After repair

Single MR pipeline ID + source + SHA + MR ref path

Configuration

Relevant workflow:rules from compiled configuration

Simulation

Local target/source parents + simulated merge SHA, explicitly labeled simulated

Limits

No real protected secrets, no production runner, merged-results/train optional

11. Challenge: select the correct layer

You still see two pipelines after changing a job’s rules. One has source push, the other merge_request_event. Which layer should you inspect first?

Answer after investigation: pipeline creation. Inspect workflow:rules and any root-level logic that permits both sources. Do not tune runner concurrency or delete one pipeline as a “fix.”

12. Cleanup and rollback

  1. Preserve the evidence packet until the lesson is reviewed.
  2. Remove the local worktree simulation if it remains.
  3. Close the disposable merge request.
  4. Delete only the exact disposable branch/project after confirming its path.
  5. Do not bulk-cancel unrelated pipelines or delete artifacts by “latest.”

Knowledge check

Why did the first configuration create duplicates?

Why is workflow:rules a better repair for duplicate pipeline creation than runner tuning?

What proves the exact revision tested by the repaired MR pipeline?

What does the local merge worktree prove?

Why avoid environment dumps in the evidence job?

Next lesson

Configuration, design choices, and tradeoffs

Choose the right pre-merge mode for integration risk, trust, throughput, tier, and auditability instead of enabling every feature indiscriminately.

Version and compatibility note

GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.

Official references and version notes

Documentation verification date: 2026-09-11. Merge-request pipeline behavior, protected-resource access, merged-results pipelines, merge trains, predefined variables, and merge checks are version-sensitive. Re-check the deployed GitLab version and project settings before using these patterns in production.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.