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