Pipeline page branch links return 404 due to GraphQL refPath returning git ref path instead of web URL
### Summary
On the CI/CD pipelines page, clicking a branch name link results in a **404 error**. The link points to a URL like:
```
https://gitlab.com/<namespace>/<project>/-/refs/heads/<branch-name>
```
instead of the correct URL:
```
https://gitlab.com/<namespace>/<project>/-/commits/<branch-name>
```
or:
```
https://gitlab.com/<namespace>/<project>/-/tree/<branch-name>?ref_type=heads
```
This regression was introduced when the **Build > Pipelines** page migrated from REST to GraphQL data (behind the `pipelines_page_graphql` feature flag). The branch still exists and is accessible via the correct URL, but the link generated on the pipelines page is broken.
*This issue was investigated with the help of GitLab Duo.*
### Steps to reproduce
1. Navigate to any project's **Build > Pipelines** page (e.g., `https://gitlab.com/gitlab-com/support/toolbox/engineering-directory/-/pipelines`)
2. Click on any **branch name** link next to a pipeline
3. Observe a **404** response
**Example:**
- Broken link: `https://gitlab.com/gitlab-com/support/toolbox/engineering-directory/-/refs/heads/metric-graphs` → 404
- Working link: `https://gitlab.com/gitlab-com/support/toolbox/engineering-directory/-/tree/metric-graphs?ref_type=heads` → works
**To reproduce on self-managed 18.8.3**, both feature flags must be enabled:
```ruby
Feature.enable(:pipelines_page_graphql)
Feature.enable(:ci_pipeline_statuses_updated_subscription)
```
On 18.8.3, the GraphQL pipelines page entry point (`app/assets/javascripts/pages/projects/pipelines/index/index.js`) requires both flags:
```javascript
const shouldUseGraphql =
gon?.features?.pipelinesPageGraphql && gon?.features?.ciPipelineStatusesUpdatedSubscription;
```
### Root cause
The issue is a mismatch between what the GraphQL `refPath` field returns and what the frontend expects as a clickable web URL.
#### Full trace
**1. GraphQL fragment requests `refPath`**
`app/assets/javascripts/ci/pipelines_page/graphql/fragments/pipeline_details.fragment.graphql`:
```graphql
fragment PipelineDetails on Pipeline {
ref # returns plain string: "metric-graphs"
refPath # returns git ref path: "refs/heads/metric-graphs"
}
```
**2. GraphQL `refPath` field resolves to `source_ref_path` on the model**
`app/graphql/types/ci/pipeline_type.rb`:
```ruby
field :ref_path, GraphQL::Types::String, null: true,
description: 'Reference path to the branch from which the pipeline was triggered.',
method: :source_ref_path # delegates to the model method
```
**3. `source_ref_path` returns a git ref, NOT a web URL**
`app/models/ci/pipeline.rb`:
```ruby
def source_ref_path
if branch? || merge_request?
Gitlab::Git::BRANCH_REF_PREFIX + source_ref.to_s
# BRANCH_REF_PREFIX = "refs/heads/"
# Result: "refs/heads/metric-graphs" -- git internal path, not a web URL
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + source_ref.to_s
end
end
```
**4. The Vue component falls through to `refPath` as the link href**
`app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue`:
```javascript
commitRef() {
return this.pipeline?.ref;
// With GraphQL data, this is a plain string "metric-graphs"
// NOT an object with .ref_url or .path properties
},
refUrl() {
return this.commitRef?.ref_url // undefined (string has no .ref_url)
|| this.commitRef?.path // undefined (string has no .path)
|| this.pipeline?.refPath; // falls through to: "refs/heads/metric-graphs"
},
```
The `|| this.pipeline?.refPath` fallback was added in !206072 to handle GraphQL data, but `refPath` resolves to `source_ref_path` which returns a git internal path, not a web URL.
The browser resolves this relative URL against the project path, producing:
`/-/refs/heads/metric-graphs` which returns **404** (no route handles this)
#### Why it worked before (REST)
The REST serializer in `app/serializers/ci/pipeline_entity.rb` exposed `ref` as a **nested object**:
```ruby
expose :ref do
expose :name do |pipeline|
pipeline.ref # "metric-graphs"
end
expose :path do |pipeline|
project_ref_path(pipeline.project, pipeline.ref) # "/-/commits/metric-graphs" -- correct web URL
end
end
```
With REST data, `pipeline.ref` was `{ name: "metric-graphs", path: "/-/commits/metric-graphs" }`, so `refUrl` resolved via `this.commitRef?.path` and never fell through to `refPath`.
| | REST (before) | GraphQL (after) |
|---|---|---|
| `pipeline.ref` | `{ name: "metric-graphs", path: "/-/commits/metric-graphs" }` | `"metric-graphs"` (plain string) |
| `pipeline.refPath` | N/A | `"refs/heads/metric-graphs"` |
| `refUrl` resolves to | `this.commitRef?.path` then `"/-/commits/metric-graphs"` (correct) | `this.pipeline?.refPath` then `"refs/heads/metric-graphs"` (broken) |
### Introduced by
- !206072 - "Move pipelines page to GraphQL - Part 1" (merged 2025-10-02, milestone 18.5). This MR migrated the **Build > Pipelines** page to use GraphQL data behind the `pipelines_page_graphql` feature flag. It modified `pipeline_url.vue` to add a `|| this.pipeline?.refPath` fallback in the `refUrl` computed property, which resolves to the git internal ref path (`refs/heads/...`) instead of a web URL when GraphQL data is used.
- The underlying GraphQL field was added in !72591 (merged 2021-12-22) which mapped `ref_path` to `source_ref_path`, a method designed for internal git operations, not web URLs.
**Note:** The issue description previously cited !213414 (`mr_pipelines_graphql` / MR Pipelines tab) as the introducing MR. That MR is unrelated to the **Build > Pipelines** page. The `mr_pipelines_graphql` feature flag controls the MR Pipelines tab and is **not enabled** on GitLab.com. The actual feature flag controlling the main pipelines page is `pipelines_page_graphql` (along with `ci_pipeline_statuses_updated_subscription` on 18.8.x).
### Affected versions
- **Broken on**: GitLab.com (currently running with `pipelines_page_graphql` enabled), and any self-managed version >= 18.5 where both `pipelines_page_graphql` and `ci_pipeline_statuses_updated_subscription` feature flags are enabled (on 18.8.x, both are required; the entry point gate was later simplified)
- **Reproducible on**: 18.8.3 self-managed with both feature flags enabled
### Proposal
Update the GraphQL `ref_path` field in `app/graphql/types/ci/pipeline_type.rb` to return a proper web URL instead of the raw git ref path. For example:
```ruby
field :ref_path, GraphQL::Types::String, null: true,
description: 'Reference path to the branch from which the pipeline was triggered.'
def ref_path
return unless object.ref
::Gitlab::Routing.url_helpers.project_commits_path(object.project, object.ref)
end
```
**Note:** Changing the return value of `ref_path` in GraphQL is a **breaking change** for any API consumers relying on the current `refs/heads/...` format. An alternative approach would be to add a new field (e.g., `ref_web_path`) and update the frontend fragment to use it, preserving backward compatibility.
issue