From a04dd010f4732850a9ffba45e7ccab8d30ec1e9e Mon Sep 17 00:00:00 2001 From: Payton Burdette Date: Thu, 17 Jul 2025 11:24:32 -0400 Subject: [PATCH 1/2] Add ability to filter jobs by kind Add ability to find build and bridge jobs in the UI / API. Changelog: changed --- .../private/jobs_filtered_search/app.vue | 40 +++++-- .../tokens/job_kind_token.vue | 54 ++++++++++ .../private/jobs_filtered_search/utils.js | 5 + .../graphql/queries/get_jobs.query.graphql | 2 + .../queries/get_jobs_count.query.graphql | 3 +- .../ci/jobs_page/jobs_page_app.vue | 29 ++++- .../filtered_search_bar/constants.js | 2 + .../resolvers/project_jobs_resolver.rb | 36 +++++-- app/models/project.rb | 1 + spec/frontend/ci/jobs_mock_data.js | 11 ++ .../ci/jobs_page/job_page_app_spec.js | 101 ++++++++++++++++-- .../resolvers/project_jobs_resolver_spec.rb | 41 +++++-- spec/models/project_spec.rb | 1 + 13 files changed, 285 insertions(+), 41 deletions(-) create mode 100644 app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_kind_token.vue diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue index 2a5385dd239c68..22efa47690aa54 100644 --- a/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue +++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue @@ -9,10 +9,13 @@ import { TOKEN_TYPE_JOBS_RUNNER_TYPE, TOKEN_TITLE_JOBS_SOURCE, TOKEN_TYPE_JOBS_SOURCE, + TOKEN_TYPE_JOB_KIND, + TOKEN_TITLE_JOB_KIND, } from '~/vue_shared/components/filtered_search_bar/constants'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import JobSourceToken from './tokens/job_source_token.vue'; import JobStatusToken from './tokens/job_status_token.vue'; +import JobKindToken from './tokens/job_kind_token.vue'; import JobRunnerTypeToken from './tokens/job_runner_type_token.vue'; export default { @@ -38,17 +41,24 @@ export default { token: JobStatusToken, operators: OPERATORS_IS, }, + { + type: TOKEN_TYPE_JOBS_SOURCE, + title: TOKEN_TITLE_JOBS_SOURCE, + icon: 'trigger-source', + unique: true, + token: JobSourceToken, + operators: OPERATORS_IS, + }, + { + type: TOKEN_TYPE_JOB_KIND, + title: TOKEN_TITLE_JOB_KIND, + icon: 'preferences', + unique: true, + token: JobKindToken, + operators: OPERATORS_IS, + }, ]; - tokens.push({ - type: TOKEN_TYPE_JOBS_SOURCE, - title: TOKEN_TITLE_JOBS_SOURCE, - icon: 'trigger-source', - unique: true, - token: JobSourceToken, - operators: OPERATORS_IS, - }); - if (this.glFeatures.adminJobsFilterRunnerType) { tokens.push({ type: TOKEN_TYPE_JOBS_RUNNER_TYPE, @@ -62,7 +72,7 @@ export default { return tokens; }, filteredSearchValue() { - return Object.entries(this.queryString || {}).reduce( + const filters = Object.entries(this.queryString || {}).reduce( (acc, [queryStringKey, queryStringValue]) => { switch (queryStringKey) { case 'statuses': @@ -81,6 +91,14 @@ export default { value: { data: queryStringValue, operator: OPERATOR_IS }, }, ]; + case 'kind': + return [ + ...acc, + { + type: TOKEN_TYPE_JOB_KIND, + value: { data: queryStringValue, operator: OPERATOR_IS }, + }, + ]; case 'runnerTypes': if (!this.glFeatures.adminJobsFilterRunnerType) { return acc; @@ -111,6 +129,8 @@ export default { }, [], ); + + return filters; }, }, methods: { diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_kind_token.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_kind_token.vue new file mode 100644 index 00000000000000..f8b5c9444afbff --- /dev/null +++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_kind_token.vue @@ -0,0 +1,54 @@ + + + diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js b/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js index 5a0caa97b6dd8a..9355d3078d5d7c 100644 --- a/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js +++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js @@ -21,6 +21,11 @@ export const validateQueryString = (queryStringObj) => { const runnerTypesValueValid = jobRunnerTypeValues.includes(runnerTypesValue); return runnerTypesValueValid ? { ...acc, runnerTypes: runnerTypesValue } : acc; } + case 'kind': { + const jobkindValue = queryStringValue.toUpperCase(); + const jobKindValueValid = ['BUILD', 'BRIDGE'].includes(jobkindValue); + return jobKindValueValid ? { ...acc, kind: jobkindValue } : acc; + } case 'name': { return { ...acc, name: queryStringValue }; } diff --git a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql index 36cf33a712509d..a2fa7e1a013268 100644 --- a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql +++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql @@ -7,6 +7,7 @@ query getJobs( $statuses: [CiJobStatus!] $sources: [CiJobSource!] $name: String + $kind: CiJobKind! = BUILD ) { project(fullPath: $fullPath) { id @@ -18,6 +19,7 @@ query getJobs( statuses: $statuses sources: $sources name: $name + kind: $kind ) { pageInfo { endCursor diff --git a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql index 42533e170b7ce1..961c546aad4a55 100644 --- a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql +++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql @@ -3,10 +3,11 @@ query getJobsCount( $statuses: [CiJobStatus!] $sources: [CiJobSource!] $name: String + $kind: CiJobKind! = BUILD ) { project(fullPath: $fullPath) { id - jobs(statuses: $statuses, sources: $sources, name: $name) { + jobs(statuses: $statuses, sources: $sources, name: $name, kind: $kind) { count } } diff --git a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue index f0f716775f3314..995aa735b165f5 100644 --- a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue +++ b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue @@ -114,8 +114,12 @@ export default { }, validatedQueryString() { const queryStringObject = queryToObject(window.location.search); + const validated = validateQueryString(queryStringObject); - return validateQueryString(queryStringObject); + return { + kind: 'BUILD', + ...validated, + }; }, }, watch: { @@ -129,12 +133,27 @@ export default { } }, }, + async mounted() { + const queryStringObject = queryToObject(window.location.search); + + // Check if kind is missing from the URL and add default + if (!queryStringObject?.kind) { + const defaultParams = { + ...this.validatedQueryString, + kind: 'BUILD', + }; + + updateHistory({ + url: setUrlParams(defaultParams, window.location.href, true), + }); + } + }, methods: { resetRequestData() { if (this.glFeatures.feSearchBuildByName) { - this.requestData = { statuses: null, sources: null, name: null }; + this.requestData = { statuses: null, sources: null, name: null, kind: 'BUILD' }; } else { - this.requestData = { statuses: null, sources: null }; + this.requestData = { statuses: null, sources: null, kind: 'BUILD' }; } }, resetPagination() { @@ -186,6 +205,10 @@ export default { if (filter.type === 'jobs-source') { this.requestData.sources = filter.value.data; } + + if (filter.type === 'kind') { + this.requestData.kind = filter.value.data; + } }); this.$apollo.queries.jobs.refetch({ diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 016e3c4546b965..1320889769b0c6 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -96,6 +96,7 @@ export const TOKEN_TITLE_ASSIGNED_SEAT = __('Assigned seat'); export const TOKEN_TITLE_ENVIRONMENT = __('Environment'); export const TOKEN_TITLE_STATE = __('State'); export const TOKEN_TITLE_SUBSCRIBED = __('Subscribed'); +export const TOKEN_TITLE_JOB_KIND = s__('Job|Kind'); export const TOKEN_TYPE_APPROVER = 'approver'; export const TOKEN_TYPE_APPROVED_BY = 'approved-by'; @@ -141,6 +142,7 @@ export const TOKEN_TYPE_DEPLOYED_AFTER = 'deployed-after'; export const TOKEN_TYPE_ENVIRONMENT = 'environment'; export const TOKEN_TYPE_STATE = 'state'; export const TOKEN_TYPE_SUBSCRIBED = 'subscribed'; +export const TOKEN_TYPE_JOB_KIND = 'kind'; // Due to the i18n eslint rule we can't have a capitalized string even if it is a case-aware URL param /* eslint-disable @gitlab/require-i18n-strings */ diff --git a/app/graphql/resolvers/project_jobs_resolver.rb b/app/graphql/resolvers/project_jobs_resolver.rb index adc4fbac3b9231..719760bb50956f 100644 --- a/app/graphql/resolvers/project_jobs_resolver.rb +++ b/app/graphql/resolvers/project_jobs_resolver.rb @@ -23,6 +23,11 @@ class ProjectJobsResolver < BaseResolver experiment: { milestone: '17.11' }, description: 'Filter jobs by name.' + argument :kind, ::Types::Ci::JobKindEnum, + required: false, + default_value: ::Ci::Build, + description: 'Filter jobs by kind.' + argument :sources, [::Types::Ci::JobSourceEnum], required: false, experiment: { milestone: '17.7' }, @@ -31,15 +36,27 @@ class ProjectJobsResolver < BaseResolver alias_method :project, :object def resolve_with_lookahead(**args) + @resolver_args = args + filter_by_name = Feature.enabled?(:populate_and_use_build_names_table, project) && args[:name].to_s.present? filter_by_sources = args[:sources].present? + filter_by_kind = args[:kind].present? - jobs = ::Ci::JobsFinder.new( - current_user: current_user, project: project, params: { - scope: args[:statuses], with_artifacts: args[:with_artifacts], - skip_ordering: filter_by_sources - } - ).execute + jobs = if filter_by_kind && args[:kind] == ::Ci::Bridge + ::Ci::JobsFinder.new( + current_user: current_user, project: project, params: { + scope: args[:statuses], with_artifacts: args[:with_artifacts], + skip_ordering: filter_by_sources + }, type: ::Ci::Bridge + ).execute + else + ::Ci::JobsFinder.new( + current_user: current_user, project: project, params: { + scope: args[:statuses], with_artifacts: args[:with_artifacts], + skip_ordering: filter_by_sources + } + ).execute + end # These job filters are currently exclusive with each other if filter_by_name @@ -64,12 +81,15 @@ def resolve_with_lookahead(**args) private def preloads - { + base_preloads = { previous_stage_jobs_or_needs: [:needs, :pipeline], - artifacts: [:job_artifacts], pipeline: [:user], build_source: [:source] } + + base_preloads[:artifacts] = [:job_artifacts] if @resolver_args[:kind] == ::Ci::Build + + base_preloads end end end diff --git a/app/models/project.rb b/app/models/project.rb index f08999959e82df..fb452707fe9596 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -454,6 +454,7 @@ def with_developer_access has_many :pipeline_metadata, class_name: 'Ci::PipelineMetadata', inverse_of: :project has_many :pending_builds, class_name: 'Ci::PendingBuild' has_many :builds, class_name: 'Ci::Build', inverse_of: :project + has_many :bridges, class_name: 'Ci::Bridge', inverse_of: :project has_many :processables, class_name: 'Ci::Processable', inverse_of: :project has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks, dependent: :restrict_with_error has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project diff --git a/spec/frontend/ci/jobs_mock_data.js b/spec/frontend/ci/jobs_mock_data.js index 76de8712b542f1..2a59120d9a0d9c 100644 --- a/spec/frontend/ci/jobs_mock_data.js +++ b/spec/frontend/ci/jobs_mock_data.js @@ -12,6 +12,7 @@ import { TEST_HOST } from 'spec/test_constants'; import { TOKEN_TYPE_STATUS, TOKEN_TYPE_JOBS_SOURCE, + TOKEN_TYPE_JOB_KIND, } from '~/vue_shared/components/filtered_search_bar/constants'; const threeWeeksAgo = new Date(); @@ -1402,6 +1403,16 @@ export const mockPushSourceToken = { value: { data: 'PUSH', operator: '=' }, }; +export const mockBridgeKindToken = { + type: TOKEN_TYPE_JOB_KIND, + value: { data: 'BRIDGE', operator: '=' }, +}; + +export const mockBuildKindToken = { + type: TOKEN_TYPE_JOB_KIND, + value: { data: 'BUILD', operator: '=' }, +}; + export const retryMutationResponse = { data: { jobRetry: { diff --git a/spec/frontend/ci/jobs_page/job_page_app_spec.js b/spec/frontend/ci/jobs_page/job_page_app_spec.js index b2379721888604..41b15c977784d6 100644 --- a/spec/frontend/ci/jobs_page/job_page_app_spec.js +++ b/spec/frontend/ci/jobs_page/job_page_app_spec.js @@ -20,6 +20,8 @@ import { mockFailedSearchToken, mockJobsCountResponse, mockPushSourceToken, + mockBridgeKindToken, + mockBuildKindToken, } from 'jest/ci/jobs_mock_data'; import { RAW_TEXT_WARNING, DEFAULT_PAGINATION, JOBS_PER_PAGE } from '~/ci/jobs_page/constants'; @@ -230,12 +232,14 @@ describe('Job table app', () => { fullPath: 'gitlab-org/gitlab', statuses: 'FAILED', sources: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', statuses: 'FAILED', sources: null, + kind: 'BUILD', }); }); @@ -280,7 +284,7 @@ describe('Job table app', () => { await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?statuses=FAILED`, + url: `${TEST_HOST}/?statuses=FAILED&kind=BUILD`, }); }); @@ -298,33 +302,37 @@ describe('Job table app', () => { fullPath: 'gitlab-org/gitlab', statuses: 'FAILED', sources: mockJobSource, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', statuses: 'FAILED', sources: mockJobSource, + kind: 'BUILD', }); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?statuses=FAILED&sources=${mockJobSource}`, + url: `${TEST_HOST}/?statuses=FAILED&sources=${mockJobSource}&kind=BUILD`, }); findFilteredSearch().vm.$emit('filterJobsBySearch', []); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/`, + url: `${TEST_HOST}/?kind=BUILD`, }); expect(successHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', statuses: null, sources: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', statuses: null, sources: null, + kind: 'BUILD', }); }); @@ -337,12 +345,14 @@ describe('Job table app', () => { fullPath: 'gitlab-org/gitlab', sources: mockJobSource, statuses: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', sources: mockJobSource, statuses: null, + kind: 'BUILD', }); }); @@ -358,12 +368,14 @@ describe('Job table app', () => { fullPath: 'gitlab-org/gitlab', sources: mockJobSource, statuses: 'FAILED', + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', sources: mockJobSource, statuses: 'FAILED', + kind: 'BUILD', }); await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockPushSourceToken]); @@ -372,12 +384,14 @@ describe('Job table app', () => { fullPath: 'gitlab-org/gitlab', sources: mockJobSource, statuses: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', sources: mockJobSource, statuses: null, + kind: 'BUILD', }); }); @@ -389,7 +403,7 @@ describe('Job table app', () => { await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockPushSourceToken]); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?sources=${mockJobSource}`, + url: `${TEST_HOST}/?sources=${mockJobSource}&kind=BUILD`, }); }); @@ -404,7 +418,7 @@ describe('Job table app', () => { ]); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?statuses=FAILED&sources=${mockJobSource}`, + url: `${TEST_HOST}/?statuses=FAILED&sources=${mockJobSource}&kind=BUILD`, }); }); @@ -421,6 +435,7 @@ describe('Job table app', () => { name: mockJobName, statuses: null, sources: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ @@ -428,6 +443,7 @@ describe('Job table app', () => { name: mockJobName, statuses: null, sources: null, + kind: 'BUILD', }); }); @@ -442,6 +458,7 @@ describe('Job table app', () => { name: mockJobName, statuses: 'FAILED', sources: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ @@ -449,6 +466,7 @@ describe('Job table app', () => { name: mockJobName, statuses: 'FAILED', sources: null, + kind: 'BUILD', }); await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockJobName]); @@ -458,6 +476,7 @@ describe('Job table app', () => { name: mockJobName, statuses: null, sources: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ @@ -465,6 +484,7 @@ describe('Job table app', () => { name: mockJobName, statuses: null, sources: null, + kind: 'BUILD', }); }); @@ -474,7 +494,7 @@ describe('Job table app', () => { await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockJobName]); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?name=${mockJobName}`, + url: `${TEST_HOST}/?name=${mockJobName}&kind=BUILD`, }); }); @@ -487,7 +507,7 @@ describe('Job table app', () => { ]); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}`, + url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}&kind=BUILD`, }); }); @@ -501,6 +521,7 @@ describe('Job table app', () => { statuses: 'FAILED', sources: null, name: mockJobName, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ @@ -508,15 +529,16 @@ describe('Job table app', () => { statuses: 'FAILED', sources: null, name: mockJobName, + kind: 'BUILD', }); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}`, + url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}&kind=BUILD`, }); findFilteredSearch().vm.$emit('filterJobsBySearch', []); expect(urlUtils.updateHistory).toHaveBeenCalledWith({ - url: `${TEST_HOST}/`, + url: `${TEST_HOST}/?kind=BUILD`, }); expect(successHandler).toHaveBeenCalledWith({ @@ -524,6 +546,7 @@ describe('Job table app', () => { statuses: null, sources: null, name: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); expect(countSuccessHandler).toHaveBeenCalledWith({ @@ -531,6 +554,64 @@ describe('Job table app', () => { statuses: null, sources: null, name: null, + kind: 'BUILD', + }); + }); + }); + + describe('filters jobs by kind', () => { + beforeEach(async () => { + createComponent(); + + successHandler.mockClear(); + countSuccessHandler.mockClear(); + + await waitForPromises(); + }); + + it('filters trigger jobs', async () => { + jest.spyOn(urlUtils, 'updateHistory'); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockBridgeKindToken]); + + expect(successHandler).toHaveBeenCalledWith({ + fullPath: 'gitlab-org/gitlab', + statuses: null, + sources: null, + kind: 'BRIDGE', + ...DEFAULT_PAGINATION, + }); + expect(countSuccessHandler).toHaveBeenCalledWith({ + fullPath: 'gitlab-org/gitlab', + statuses: null, + sources: null, + kind: 'BRIDGE', + }); + expect(urlUtils.updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/?kind=BRIDGE`, + }); + }); + + it('filters build jobs', async () => { + jest.spyOn(urlUtils, 'updateHistory'); + + await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockBuildKindToken]); + + expect(successHandler).toHaveBeenCalledWith({ + fullPath: 'gitlab-org/gitlab', + statuses: null, + sources: null, + kind: 'BUILD', + ...DEFAULT_PAGINATION, + }); + expect(countSuccessHandler).toHaveBeenCalledWith({ + fullPath: 'gitlab-org/gitlab', + statuses: null, + sources: null, + kind: 'BUILD', + }); + expect(urlUtils.updateHistory).toHaveBeenCalledWith({ + url: `${TEST_HOST}/?kind=BUILD`, }); }); }); @@ -635,6 +716,7 @@ describe('Job table app', () => { fullPath: 'gitlab-org/gitlab', statuses: 'FAILED', sources: null, + kind: 'BUILD', ...DEFAULT_PAGINATION, }); }); @@ -653,6 +735,7 @@ describe('Job table app', () => { expect(successHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org/gitlab', statuses: ['FAILED', 'SUCCESS', 'CANCELED'], + kind: 'BUILD', ...DEFAULT_PAGINATION, }); }); diff --git a/spec/graphql/resolvers/project_jobs_resolver_spec.rb b/spec/graphql/resolvers/project_jobs_resolver_spec.rb index f1f24be4129593..632beaf0537451 100644 --- a/spec/graphql/resolvers/project_jobs_resolver_spec.rb +++ b/spec/graphql/resolvers/project_jobs_resolver_spec.rb @@ -14,6 +14,7 @@ let_it_be(:failed_build) { create(:ci_build, :failed, :with_build_name, name: 'Build Three', pipeline: pipeline) } let_it_be(:pending_build) { create(:ci_build, :pending, :with_build_name, name: 'Build Three', pipeline: pipeline) } let_it_be(:irrelevant_build) { create(:ci_build, name: 'Irrelevant Build', pipeline: irrelevant_pipeline) } + let_it_be(:bridge_job) { create(:ci_bridge, :success, name: 'Bridge Job', pipeline: pipeline) } describe '#resolve' do let(:args) { {} } @@ -23,20 +24,22 @@ context 'with authorized user' do let_it_be(:current_user) { create(:user, developer_of: project) } - context 'with statuses argument' do - let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } } + context 'when filtering by status' do + context 'with statuses argument' do + let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } } - it { is_expected.to contain_exactly(successful_build, successful_build_two) } - end + it { is_expected.to contain_exactly(successful_build, successful_build_two) } + end - context 'with multiple statuses' do - let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS'), Types::Ci::JobStatusEnum.coerce_isolated_input('FAILED')] } } + context 'with multiple statuses' do + let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS'), Types::Ci::JobStatusEnum.coerce_isolated_input('FAILED')] } } - it { is_expected.to contain_exactly(successful_build, successful_build_two, failed_build) } - end + it { is_expected.to contain_exactly(successful_build, successful_build_two, failed_build) } + end - context 'without statuses argument' do - it { is_expected.to contain_exactly(successful_build, successful_build_two, failed_build, pending_build) } + context 'without statuses argument' do + it { is_expected.to contain_exactly(successful_build, successful_build_two, failed_build, pending_build) } + end end context 'when filtering by source' do @@ -132,6 +135,24 @@ end end end + + context 'when filtering by job kind' do + context 'when filtering for bridge jobs' do + let(:args) { { kind: 'BRIDGE' } } + + it { is_expected.to contain_exactly(bridge_job) } + end + + context 'when filtering for build jobs' do + let(:args) { { kind: 'BUILD' } } + + it { is_expected.to contain_exactly(successful_build, successful_build_two, failed_build, pending_build) } + end + + context 'without kind arugment' do + it { is_expected.to contain_exactly(successful_build, successful_build_two, failed_build, pending_build) } + end + end end context 'with unauthorized user' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 037deb45028847..c7400cc1c6a67b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -124,6 +124,7 @@ it { is_expected.to have_many(:ci_pipelines) } it { is_expected.to have_many(:ci_refs) } it { is_expected.to have_many(:builds) } + it { is_expected.to have_many(:bridges) } it { is_expected.to have_many(:build_report_results) } it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } -- GitLab From 5a800a90de2144dafd5e26db933e4114f773458b Mon Sep 17 00:00:00 2001 From: Payton Burdette Date: Thu, 17 Jul 2025 11:27:30 -0400 Subject: [PATCH 2/2] Regenerate doc files --- doc/api/graphql/reference/_index.md | 1 + locale/gitlab.pot | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 810a00bb7046a2..4e81b2dc368b5a 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -38023,6 +38023,7 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | +| `kind` | [`CiJobKind`](#cijobkind) | Filter jobs by kind. | | `name` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 17.11. **Status**: Experiment. Filter jobs by name. | | `sources` {{< icon name="warning-solid" >}} | [`[CiJobSource!]`](#cijobsource) | **Introduced** in GitLab 17.7. **Status**: Experiment. Filter jobs by source. | | `statuses` | [`[CiJobStatus!]`](#cijobstatus) | Filter jobs by status. | diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5bc01d5bf9b87e..a2ee0496ce15be 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -35761,6 +35761,9 @@ msgstr "" msgid "Job|Browse" msgstr "" +msgid "Job|Build" +msgstr "" + msgid "Job|Cancel" msgstr "" @@ -35821,6 +35824,9 @@ msgstr "" msgid "Job|Keep" msgstr "" +msgid "Job|Kind" +msgstr "" + msgid "Job|Log timestamps in UTC." msgstr "" @@ -35923,6 +35929,9 @@ msgstr "" msgid "Job|This job is stuck because you don't have any active runners that can run this job." msgstr "" +msgid "Job|Trigger" +msgstr "" + msgid "Job|Update CI/CD variables" msgstr "" -- GitLab