From f5b9c212ad644858ec22776568f699e0d7ef0ae5 Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Tue, 21 Mar 2023 14:09:38 +0530 Subject: [PATCH] Allow pipelines to be filtered by name in public pipelines API Add an optional name parameter to the project pipelines public API request, and include the names of pipelines in the response. We have created a new entity called PipelineBasicWithMetadata since PipelineBasic is used in a lot of places. With a new entity, we don't need to change all of them at once. --- app/models/ci/pipeline.rb | 1 + .../development/pipeline_name_in_api.yml | 8 +++ doc/api/pipelines.md | 12 ++++- lib/api/ci/pipelines.rb | 8 ++- .../ci/pipeline_basic_with_metadata.rb | 13 +++++ spec/models/ci/pipeline_spec.rb | 10 ++++ spec/requests/api/ci/pipelines_spec.rb | 53 ++++++++++++++++++- 7 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 config/feature_flags/development/pipeline_name_in_api.yml create mode 100644 lib/api/entities/ci/pipeline_basic_with_metadata.rb diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 644e9c31eb47ed..8c7b0193199952 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -399,6 +399,7 @@ class Pipeline < Ci::ApplicationRecord scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) } scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) } scope :with_pipeline_source, -> (source) { where(source: source) } + scope :preload_pipeline_metadata, -> { preload(:pipeline_metadata) } scope :outside_pipeline_family, ->(pipeline) do where.not(id: pipeline.same_family_pipeline_ids) diff --git a/config/feature_flags/development/pipeline_name_in_api.yml b/config/feature_flags/development/pipeline_name_in_api.yml new file mode 100644 index 00000000000000..cb22fca293286b --- /dev/null +++ b/config/feature_flags/development/pipeline_name_in_api.yml @@ -0,0 +1,8 @@ +--- +name: pipeline_name_in_api +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115310 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/398131 +milestone: '15.11' +type: development +group: group::delivery +default_enabled: false diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 7049d3c3927a5a..88a0b601a42a8d 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -15,7 +15,14 @@ Read more on [pagination](rest/index.md#pagination). ## List project pipelines -> `iid` in response [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342223) in GitLab 14.6. +> - `iid` in response [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342223) in GitLab 14.6. +> - `name` in request and response [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115310) in GitLab 15.11 [with a flag](../administration/feature_flags.md) named `pipeline_name_in_api`. Disabled by default. + +FLAG: +On self-managed GitLab, by default the `name` field is not available. +To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) +named `pipeline_name_in_api`. This feature is not ready for production use. +On GitLab.com, this feature is not available. List pipelines in a project. Child pipelines are not included in the results, but you can [get child pipeline](pipelines.md#get-a-single-pipeline) individually. @@ -36,6 +43,7 @@ GET /projects/:id/pipelines | `username`| string | no | The username of the user who triggered pipelines | | `updated_after` | datetime | no | Return pipelines updated after the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | | `updated_before` | datetime | no | Return pipelines updated before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `name` | string | no | Return pipelines with the specified name. Introduced in GitLab 15.11, not available by default. | | `order_by`| string | no | Order pipelines by `id`, `status`, `ref`, `updated_at` or `user_id` (default: `id`) | | `sort` | string | no | Sort pipelines in `asc` or `desc` order (default: `desc`) | @@ -55,6 +63,7 @@ Example of response "source": "push", "ref": "new-pipeline", "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "name": "Build pipeline", "web_url": "https://example.com/foo/bar/pipelines/47", "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z" @@ -67,6 +76,7 @@ Example of response "source": "web", "ref": "new-pipeline", "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a", + "name": "Build pipeline", "web_url": "https://example.com/foo/bar/pipelines/48", "created_at": "2016-08-12T10:06:04.561Z", "updated_at": "2016-08-12T10:09:56.223Z" diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index c683fdf7aad182..2ea0b5f1cdb463 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -69,13 +69,19 @@ class Pipelines < ::API::Base documentation: { example: 'asc' } optional :source, type: String, values: ::Ci::Pipeline.sources.keys, documentation: { example: 'push' } + optional :name, types: String, desc: 'Filter pipelines by name', + documentation: { example: 'Build pipeline' } end get ':id/pipelines', urgency: :low, feature_category: :continuous_integration do authorize! :read_pipeline, user_project authorize! :read_build, user_project + params.delete(:name) unless ::Feature.enabled?(:pipeline_name_in_api, user_project) + pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute - present paginate(pipelines), with: Entities::Ci::PipelineBasic, project: user_project + pipelines = pipelines.preload_pipeline_metadata if ::Feature.enabled?(:pipeline_name_in_api, user_project) + + present paginate(pipelines), with: Entities::Ci::PipelineBasicWithMetadata, project: user_project end desc 'Create a new pipeline' do diff --git a/lib/api/entities/ci/pipeline_basic_with_metadata.rb b/lib/api/entities/ci/pipeline_basic_with_metadata.rb new file mode 100644 index 00000000000000..4eeba3aec41b67 --- /dev/null +++ b/lib/api/entities/ci/pipeline_basic_with_metadata.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class PipelineBasicWithMetadata < PipelineBasic + expose :name, + documentation: { type: 'string', example: 'Build pipeline' }, + if: ->(pipeline, _) { ::Feature.enabled?(:pipeline_name_in_api, pipeline.project) } + end + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 263db8e58c7a50..ee1410ade918f1 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -444,6 +444,16 @@ def success_to_success? end end + describe '.preload_pipeline_metadata' do + let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project, user: user, name: 'Chatops pipeline') } + + it 'loads associations' do + result = described_class.preload_pipeline_metadata.first + + expect(result.association(:pipeline_metadata).loaded?).to be(true) + end + end + describe '.ci_sources' do subject { described_class.ci_sources } diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index 4e81a052ecf340..9660778bc919c4 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -14,7 +14,7 @@ let_it_be(:pipeline) do create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch, user: user) + ref: project.default_branch, user: user, name: 'Build pipeline') end before do @@ -41,10 +41,46 @@ it 'includes pipeline source' do get api("/projects/#{project.id}/pipelines", user) - expect(json_response.first.keys).to contain_exactly(*%w[id iid project_id sha ref status web_url created_at updated_at source]) + expect(json_response.first.keys).to contain_exactly(*%w[id iid project_id sha ref status web_url created_at updated_at source name]) + end + + context 'when pipeline_name_in_api feature flag is off' do + before do + stub_feature_flags(pipeline_name_in_api: false) + end + + it 'does not include pipeline name in response and ignores name parameter' do + get api("/projects/#{project.id}/pipelines", user), params: { name: 'Chatops pipeline' } + + expect(json_response.length).to eq(1) + expect(json_response.first.keys).not_to include('name') + end end end + it 'avoids N+1 queries' do + # Call to trigger any one time queries + get api("/projects/#{project.id}/pipelines", user), params: {} + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get api("/projects/#{project.id}/pipelines", user), params: {} + end + + 3.times do + create( + :ci_empty_pipeline, + project: project, + sha: project.commit.id, + ref: project.default_branch, + user: user, + name: 'Build pipeline') + end + + expect do + get api("/projects/#{project.id}/pipelines", user), params: {} + end.not_to exceed_all_query_limit(control) + end + context 'when parameter is passed' do %w[running pending].each do |target| context "when scope is #{target}" do @@ -303,6 +339,19 @@ end end end + + context 'when name is provided' do + let_it_be(:pipeline2) { create(:ci_empty_pipeline, project: project, user: user, name: 'Chatops pipeline') } + + it 'filters by name' do + get api("/projects/#{project.id}/pipelines", user), params: { name: 'Build pipeline' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq('Build pipeline') + end + end end end -- GitLab