MR-event pipelines can receive malformed git fetch refspec (refs/heads/refs/merge-requests/<iid>/head) in job payload
## Summary For some merge-request-event pipelines on GitLab.com, the job payload returned by Rails contains a malformed git fetch refspec. The branch-pipeline refspec template (`+refs/heads/<ref>:refs/remotes/origin/<ref>`) is applied to a build whose `ref` is already a full MR ref path (`refs/merge-requests/<iid>/head`), producing: ``` +refs/heads/refs/merge-requests/<iid>/head:refs/remotes/origin/refs/merge-requests/<iid>/head ``` The runner passes `git_info.refspecs` to `git fetch` verbatim and fails with: ``` fatal: couldn't find remote ref refs/heads/refs/merge-requests/<iid>/head ``` The runner is not at fault (`shells/abstract.go:writeRefspecFetchCmd` and `get_sources.go:gitFetch` append `build.GitInfo.Refspecs` verbatim). The malformation originates in the job payload constructed by Rails. Reported via [gitlab-com/request-for-help#4896](https://gitlab.com/gitlab-com/request-for-help/-/issues/4896). ## Root cause analysis The refspec is built in `Ci::BuildRunnerPresenter#refspecs` (`app/presenters/ci/build_runner_presenter.rb`): ```ruby def refspecs specs = [] specs << refspec_for_persistent_ref if git_depth > 0 specs << refspec_for_branch(ref) if branch? || legacy_detached_merge_request_pipeline? specs << refspec_for_tag(ref) if tag? else specs << refspec_for_branch specs << refspec_for_tag end specs end ``` `refspec_for_branch(ref)` produces `+refs/heads/#{ref}:refs/remotes/origin/#{ref}`. When `ref` is already `refs/merge-requests/<iid>/head`, this yields the doubled prefix. `branch?` comes from `Ci::HasRef` (`app/models/concerns/ci/has_ref.rb`): ```ruby def branch? !tag? && !merge_request? && !workload? end ``` The decisive subtlety is in `Ci::Pipeline#merge_request?` (`app/models/ci/pipeline.rb`): ```ruby def merge_request? merge_request_id.present? && merge_request.present? end ``` The `&& merge_request.present?` clause is the failure mode. If a detached MR pipeline/build still carries a `merge_request_id` but the associated `MergeRequest` record can no longer be loaded (e.g. the MR was deleted, or the association fails to load), then: - `merge_request?` returns **false**, so `branch?` returns **true** - `legacy_detached_merge_request_pipeline?` (`detached_merge_request_pipeline? && !merge_request_ref?`) also cannot resolve correctly - the build's persisted `ref` is still `refs/merge-requests/<iid>/head` - `refspec_for_branch(ref)` wraps that full MR ref inside the branch template This matches all reported symptoms: the API `ref` is clean, the doubled prefix is server-side, and there is no user-supplied `git fetch` command involved. ## Proposed fix Make refspec construction defensive so it never re-wraps a ref that is already a fully-qualified ref path, regardless of how the MR association resolves. Option A — guard the caller in `#refspecs`: ```ruby specs << refspec_for_branch(ref) if (branch? || legacy_detached_merge_request_pipeline?) && !ref.to_s.start_with?('refs/') ``` Option B (preferred) — self-contained guard in the helper so a doubled prefix can never be emitted: ```ruby def refspec_for_branch(ref = '*') return "+#{ref}:#{RUNNER_REMOTE_BRANCH_PREFIX}#{ref}" if ref.to_s.start_with?('refs/') "+#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref}:#{RUNNER_REMOTE_BRANCH_PREFIX}#{ref}" end ``` ## Next steps 1. Confirm **why** `merge_request.present?` returns false for these jobs (deleted MR vs. stale `merge_request_id` vs. association load failure). This determines whether the long-term fix is purely hardening `#refspecs` or also fixing the data/association path. 2. Implement the defensive guard in `Ci::BuildRunnerPresenter` (Option B preferred). 3. Add a regression spec alongside the existing `#refspecs` examples in `spec/presenters/ci/build_runner_presenter_spec.rb` covering a build with an MR `ref` where `merge_request?` is false, asserting no `refs/heads/refs/...` doubled prefix is produced. 4. Verify against the affected job in #4896 (Poppulo, namespace/project 38545744, job 14671025948) and the possible second affected customer (Geotab self-managed, ZD-696893). ## Affected - GitLab.com (SaaS), GitLab Runner 18.11.3 - RFH: gitlab-com/request-for-help#4896 /cc @drew
issue