The branch pipeline went green on your last push. The merge request pipeline failed on that same change. You now have two logs and one merge button. Which pipeline is the real merge gate?
I keep seeing teams treat those two runs as twins. GitLab starts them for different event reasons. They see different refs, rules, and variables.
This FAQ is a corrected mental model for GitLab. Each myth has a claim, a check, and a fix. Examples below are proposed reproductions you can run. They are not production receipts or metrics.
Myth 1: A green push job proves the MR
Claim: The same job name passed on the feature branch. So the merge request should pass too.
Evidence: GitLab sets CI_PIPELINE_SOURCE per pipeline kind. A branch push uses push. A merge request uses merge_request_event. Did the failing job even exist on push?
# example only: stop duplicate pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_PIPELINE_SOURCE == "push" && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_PIPELINE_SOURCE == "push"
That workflow block is a common duplicate-pipeline guard. Without it, GitLab may run both events. A green push job can hide a missing MR-only job. Status icons do not list skipped jobs for you.
Corrected model: The merge request pipeline is the merge gate. The push pipeline is a different GitLab event. Compare CI_PIPELINE_SOURCE before you compare job status.
Quick source check
- Open both pipeline pages in two tabs.
- Read Pipeline source, not only the badge color.
- Diff the job names, not the retry buttons.
- Ask which jobs are MR-only by
rules.
Still sure those pipelines are the same animal?
Myth 2: rules:changes means the same skip
Claim: The job skipped on push, so it skips on the MR. Or the reverse: it ran on push, so the MR must run it.
Evidence: GitLab compares different diffs for changes. On a branch push, the baseline is the previous commit on that branch. On a merge request pipeline, the baseline is the merge request diff against the target branch.
# example only: paths match; baselines do not
test:
stage: test
script:
- npm test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- package.json
- src/**/*
- if: $CI_PIPELINE_SOURCE == "push"
changes:
- package.json
- src/**/*
Push three docs-only commits after a src/ change. The later push pipeline may skip test. Open the MR against main. GitLab now diffs the whole feature branch. src/ changed relative to target. The job runs. Surprise?
Corrected model: changes is not a live file watcher. It is a comparison against a baseline ref. Name that baseline before you trust a skip.
What I print before I argue
# example only: see which files GitLab compared
git fetch origin main
git diff --name-only origin/main...HEAD
git diff --name-only HEAD~1 HEAD
Those two lists often disagree. That disagreement is the myth.
Myth 3: The SHA in the log is your branch tip
Claim: CI_COMMIT_SHA in the MR job is the commit you just pushed.
Evidence: Merged results pipelines build a temporary merge commit. GitLab merges your source into the current target first. Then it runs jobs on that merge product. Target main can move without your branch moving.
Check this set in the job log:
CI_PIPELINE_SOURCECI_COMMIT_SHACI_MERGE_REQUEST_SOURCE_BRANCH_SHACI_MERGE_REQUEST_TARGET_BRANCH_SHA-
CI_MERGE_REQUEST_DIFF_BASE_SHA
# example only: print the refs GitLab actually used
echo "source=${CI_PIPELINE_SOURCE}"
echo "commit=${CI_COMMIT_SHA}"
echo "src_sha=${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}"
echo "tgt_sha=${CI_MERGE_REQUEST_TARGET_BRANCH_SHA}"
echo "base=${CI_MERGE_REQUEST_DIFF_BASE_SHA}"
If merged results are enabled, CI_COMMIT_SHA is not your tip. It is the merge product. Your push pipeline stayed green on the old tip. The MR pipeline compiled against main as it exists now. Who drifted?
Corrected model: Read the SHA family before you blame the test file. The merge product is what the default branch will contain.
Confirm merged results under Settings → Merge requests. Docs live with GitLab, not with chat memory. Your instance version still matters here.
Myth 4: Parsed YAML is a valid MR pipeline
Claim: An agent rewrote .gitlab-ci.yml and YAML loaded. Ship it.
Evidence: A YAML loader does not apply GitLab rules. It does not expand include. It does not simulate workflow. GitLab does that in CI Lint. Local parse is not lint.
# example only: lint against the project, not PyYAML
# use a PAT with api scope; never commit the token
python -c 'import json,sys; print(json.dumps({"content": sys.stdin.read(), "dry_run": True, "ref": "refs/heads/feature/demo"}))' \
< .gitlab-ci.yml > /tmp/ci-lint.json
curl --silent --fail-with-body \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
--header "Content-Type: application/json" \
--data @/tmp/ci-lint.json \
"https://gitlab.example.com/api/v4/projects/${PROJECT_ID}/ci/lint?include_merged_yaml=true"
Need a draft loop before lint? Treat chat output as a sketch only.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode's free model access can rewrite a rules block. Its free server option can run the job script as a shell. Neither sets CI_PIPELINE_SOURCE=merge_request_event the way GitLab does. Neither expands project include files with your project's permissions.
Corrected model: Lint on GitLab. Then run on GitLab. A chat file is a draft. A free shell is a script rehearsal.
What lint still cannot do
- It will not execute
script:. - It will not pull your private images.
- It will not prove tests or coverage.
- It will not mint
CI_JOB_TOKEN.
So why skip it? Because skipping it is how invalid rules reach merge.
Myth 5: Protected variables follow the job name
Claim: deploy has the token on main. The MR job with the same name has it too.
Evidence: Protected CI/CD variables follow ref protection. Merge request pipelines often run on a temporary ref. An MR from an unprotected source branch may not receive them. Fork merge requests do not see parent project secrets. Check current GitLab docs for your version. Details around merged results have moved across releases.
# example only: keep prod tokens off the MR pipeline
deploy:
stage: deploy
script:
- test -n "${PROD_TOKEN}"
- ./deploy.sh
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
Want a dry run of deploy.sh? Use dummy env on a scratch host. Never paste protected values into a chat transcript. Job name reuse is not variable reuse.
Corrected model: Protection is a ref policy, not a script: policy. Confirm the protected flag under Settings → CI/CD → Variables.
Artifact: fill this table from one pipeline pair
Do not argue from memory after the stand-up. Fill the table from one real push pipeline and one real MR pipeline.
| Check | Push pipeline | MR pipeline | Match? |
|---|---|---|---|
| Pipeline ID | |||
CI_PIPELINE_SOURCE |
|||
CI_COMMIT_SHA |
|||
| Source branch SHA | |||
rules:changes baseline |
previous commit | target diff | |
| Job names from the UI | |||
CI Lint valid
|
|||
| Protected vars present | |||
Expanded include list |
Reproduction steps
- Push a commit that only touches docs.
- Open or update the merge request.
- Copy both pipeline IDs into the table.
- Diff job names, not status icons.
- Dump SHA variables from a debug job.
- POST the YAML to CI Lint with
dry_run. - Confirm
workflow:rulesdrops the duplicate you do not want.
# example only: delete this job after the check
debug:refs:
stage: .pre
script:
- env | grep -E '^CI_(PIPELINE_SOURCE|COMMIT_SHA|MERGE_REQUEST_)' | sort
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_PIPELINE_SOURCE == "push"
That filled table plus two pipeline URLs is the artifact. A chat summary is not the artifact.
Limitations
This model assumes GitLab.com or a recent self-managed instance. Predefined variable names are mostly stable. Merged results and merge trains introduce extra refs. Read the project setting before you quote CI_COMMIT_SHA in a postmortem.
CI Lint dry_run is still not a runner. A free remote shell cannot impersonate merge request permissions. Do not dress it up as GitLab. only/except may still parse on old configs. GitLab has directed new work to rules. I do not treat only: merge_requests as current guidance.
Who should not use this approach
Do not use a chat rewrite as merge evidence. Security-sensitive pipelines need human review of rules and variables. Fork workflows need a separate permission model. If your gate is a merge train, this FAQ is incomplete. Read merge train docs first. If you cannot call CI Lint, stop guessing.
What I actually trust
I trust the merge request pipeline URL. I trust the lint JSON. I trust the SHA dump in the job log. I do not trust a green push icon. I do not trust a local YAML parse. I do not trust a rehearsed script on a free box.
If you sketch rules in a free model session, still lint on GitLab before you merge. Paste one lint response into review, not two job GIFs.
Top comments (0)