diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-23 21:09:26 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-23 21:09:26 +0300 |
commit | 35272ed523e03fd0f2a77e6951eb4017cdb224ff (patch) | |
tree | 0491e10e68a7932168c9ddadfb0de26780de4ad3 | |
parent | cd81aadde2782c9fbb0ad69d5471bad792345120 (diff) |
Add latest changes from gitlab-org/gitlab@master
31 files changed, 358 insertions, 62 deletions
diff --git a/.gitlab/issue_templates/Broken Master - Flaky.md b/.gitlab/issue_templates/Broken Master - Flaky.md new file mode 100644 index 00000000000..6b56845ba8c --- /dev/null +++ b/.gitlab/issue_templates/Broken Master - Flaky.md @@ -0,0 +1,25 @@ +<!--- +This issue template is for a master pipeline is failing for a flaky reason that cannot be reliably reproduced. + +Please read the below documentations for a workflow of triaging and resolving broken master. + +- https://about.gitlab.com/handbook/engineering/workflow/#triage-broken-master +- https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/runbooks/master-broken.md +- https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/testing_guide/flaky_tests.md +---> + +### Summary + +<!-- Link to the failing master build and add the build failure output in the below code block section. --> + +### Steps to reproduce + +<!-- If the pipeline failure is reproducible, provide steps to recreate the issue locally. Please use an ordered list. --> + +### Proposed Resolution + +<!-- Describe the proposed change to restore master stability. --> + +Please refer to the [Resolution guidance](https://about.gitlab.com/handbook/engineering/workflow/#resolution-of-broken-master) to learn more about resolution of broken master. + +/label ~"failure::flaky-test" ~"Engineering Productivity" ~"priority::2" ~"severity::2" diff --git a/.gitlab/issue_templates/Broken Master - Non-flaky.md b/.gitlab/issue_templates/Broken Master - Non-flaky.md new file mode 100644 index 00000000000..97a34aa759d --- /dev/null +++ b/.gitlab/issue_templates/Broken Master - Non-flaky.md @@ -0,0 +1,24 @@ +<!--- +This issue template is for a master pipeline is failing for a non-flaky reason. + +Please read the below documentations for a workflow of triaging and resolving broken master. + +- https://about.gitlab.com/handbook/engineering/workflow/#triage-broken-master +- https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/runbooks/master-broken.md +---> + +### Summary + +<!-- Link to the failing master build and add the build failure output in the below code block section. --> + +### Steps to reproduce + +<!-- If the pipeline failure is reproducible, provide steps to recreate the issue locally. Please use an ordered list. --> + +### Proposed Resolution + +<!-- Describe the proposed change to restore master stability. --> + +Please refer to the [Resolution guidance](https://about.gitlab.com/handbook/engineering/workflow/#resolution-of-broken-master) to learn more about resolution of broken master. + +/label ~"master:broken" ~"Engineering Productivity" ~"priority::1" ~"severity::1" diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue index 944a2ef7f64..59f0e0dd17d 100644 --- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue +++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue @@ -138,10 +138,9 @@ export default { }}</span> </template> - <template #cell(action)="{ item: { revokePath, expiresAt } }"> + <template #cell(action)="{ item: { revokePath } }"> <gl-button - variant="danger" - :category="expiresAt ? 'primary' : 'secondary'" + category="tertiary" :aria-label="$options.i18n.revokeButton" :data-confirm="modalMessage" data-confirm-btn-variant="danger" diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 00f3f0b08b2..12147196749 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -49,7 +49,7 @@ class Import::BitbucketServerController < Import::BaseController session[bitbucket_server_username_key] = params[:bitbucket_server_username] session[bitbucket_server_url_key] = params[:bitbucket_server_url] - redirect_to status_import_bitbucket_server_path + redirect_to status_import_bitbucket_server_path(namespace_id: params[:namespace_id]) end # We need to re-expose controller's internal method 'status' as action. @@ -115,7 +115,7 @@ class Import::BitbucketServerController < Import::BaseController unless session[bitbucket_server_url_key].present? && session[bitbucket_server_username_key].present? && session[personal_access_token_key].present? - redirect_to new_import_bitbucket_server_path + redirect_to new_import_bitbucket_server_path(namespace_id: params[:namespace_id]) end end @@ -170,9 +170,6 @@ class Import::BitbucketServerController < Import::BaseController } }, status: :unprocessable_entity end - format.html do - redirect_to new_import_bitbucket_server_path - end end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5d316906bd3..d7889407849 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1004,6 +1004,10 @@ module Ci object_hierarchy(project_condition: :same).base_and_descendants end + def self_and_descendants_complete? + self_and_descendants.all?(&:complete?) + end + # Follow the parent-child relationships and return the top-level parent def root_ancestor return self unless child? diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb index 2284a05bcc9..cdc3d69f754 100644 --- a/app/models/ci/pipeline_artifact.rb +++ b/app/models/ci/pipeline_artifact.rb @@ -51,6 +51,23 @@ module Ci def find_by_file_type(file_type) find_by(file_type: file_type) end + + def create_or_replace_for_pipeline!(pipeline:, file_type:, file:, size:) + transaction do + pipeline.pipeline_artifacts.find_by_file_type(file_type)&.destroy! + + pipeline.pipeline_artifacts.create!( + file_type: file_type, + project_id: pipeline.project_id, + size: size, + file: file, + file_format: REPORT_TYPES[file_type], + expire_at: EXPIRATION_DATE.from_now + ) + end + rescue ActiveRecord::ActiveRecordError => err + Gitlab::ErrorTracking.track_and_raise_exception(err, { pipeline_id: pipeline.id, file_type: file_type }) + end end def present diff --git a/app/services/ci/pipeline_artifacts/coverage_report_service.rb b/app/services/ci/pipeline_artifacts/coverage_report_service.rb index b0acb1d5a0b..c69cb721bb4 100644 --- a/app/services/ci/pipeline_artifacts/coverage_report_service.rb +++ b/app/services/ci/pipeline_artifacts/coverage_report_service.rb @@ -9,17 +9,16 @@ module Ci end def execute - return if pipeline.has_coverage_reports? + unless Feature.enabled?(:ci_child_pipeline_coverage_reports, pipeline.project) || + !pipeline.has_coverage_reports? + return + end + return if report.empty? - pipeline.pipeline_artifacts.create!( - project_id: pipeline.project_id, - file_type: :code_coverage, - file_format: Ci::PipelineArtifact::REPORT_TYPES.fetch(:code_coverage), - size: carrierwave_file["tempfile"].size, - file: carrierwave_file, - expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now - ) + Ci::PipelineArtifact.create_or_replace_for_pipeline!(**pipeline_artifact_params).tap do |pipeline_artifact| + Gitlab::AppLogger.info(log_params(pipeline_artifact)) + end end private @@ -32,6 +31,15 @@ module Ci end end + def pipeline_artifact_params + { + pipeline: pipeline, + file_type: :code_coverage, + file: carrierwave_file, + size: carrierwave_file['tempfile'].size + } + end + def carrierwave_file strong_memoize(:carrier_wave_file) do CarrierWaveStringFile.new_file( @@ -41,6 +49,15 @@ module Ci ) end end + + def log_params(pipeline_artifact) + { + project_id: pipeline.project_id, + pipeline_id: pipeline.id, + pipeline_artifact_id: pipeline_artifact.id, + message: "Created code coverage for pipeline." + } + end end end end diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml index 0d87cf66814..292dd9d071c 100644 --- a/app/views/import/bitbucket_server/new.html.haml +++ b/app/views/import/bitbucket_server/new.html.haml @@ -10,7 +10,7 @@ %p = _('Enter in your Bitbucket Server URL and personal access token below') -= form_tag configure_import_bitbucket_server_path, method: :post do += form_tag configure_import_bitbucket_server_path(namespace_id: params[:namespace_id]), method: :post do .form-group.row = label_tag :bitbucket_server_url, 'Bitbucket Server URL', class: 'col-form-label col-md-2' .col-md-4 diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index 05b42767668..7e0c7b3dd74 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -5,4 +5,4 @@ = sprite_icon('bitbucket', css_class: 'gl-mr-2') = _('Import projects from Bitbucket Server') -= render 'import/githubish_status', provider: 'bitbucket_server', paginatable: true, extra_data: { reconfigure_path: configure_import_bitbucket_server_path } += render 'import/githubish_status', provider: 'bitbucket_server', paginatable: true, default_namespace: @namespace, extra_data: { reconfigure_path: configure_import_bitbucket_server_path } diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index 034c4a7501d..9f2c7dd2559 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -31,7 +31,7 @@ Bitbucket Cloud - if bitbucket_server_import_enabled? %div - = link_to status_import_bitbucket_server_path, class: "gl-button btn-default btn import_bitbucket js-import-project-btn", data: { platform: 'bitbucket_server', **tracking_attrs_data(track_label, 'click_button', 'bitbucket_server') } do + = link_to status_import_bitbucket_server_path(namespace_id: namespace_id), class: "gl-button btn-default btn import_bitbucket js-import-project-btn", data: { platform: 'bitbucket_server', **tracking_attrs_data(track_label, 'click_button', 'bitbucket_server') } do .gl-button-icon = sprite_icon('bitbucket') Bitbucket Server diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb index 8ee518e3ae6..5be40f64930 100644 --- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb +++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb @@ -15,7 +15,17 @@ module Ci idempotent! def perform(pipeline_id) - Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline| + pipeline = Ci::Pipeline.find_by_id(pipeline_id) + + return unless pipeline + + if Feature.enabled?(:ci_child_pipeline_coverage_reports, pipeline.project) + pipeline.root_ancestor.try do |root_ancestor_pipeline| + next unless root_ancestor_pipeline.self_and_descendants_complete? + + Ci::PipelineArtifacts::CoverageReportService.new(root_ancestor_pipeline).execute + end + else Ci::PipelineArtifacts::CoverageReportService.new(pipeline).execute end end diff --git a/config/metrics/counts_all/20210916200931_clusters_integrations_elastic_stack.yml b/config/metrics/counts_all/20210916200931_clusters_integrations_elastic_stack.yml index 54437cfbf36..bbaba8463a0 100644 --- a/config/metrics/counts_all/20210916200931_clusters_integrations_elastic_stack.yml +++ b/config/metrics/counts_all/20210916200931_clusters_integrations_elastic_stack.yml @@ -7,7 +7,7 @@ product_stage: configure product_group: group::configure product_category: kubernetes_management value_type: number -status: active +status: removed time_frame: all data_source: database distribution: @@ -19,3 +19,5 @@ tier: - ultimate performance_indicator_type: [] milestone: "14.4" +milestone_removed: "15.2" +removed_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90699" diff --git a/doc/development/deprecation_guidelines/img/deprecation_removal_process.png b/doc/development/deprecation_guidelines/img/deprecation_removal_process.png Binary files differindex 99642ebbae0..594e15861b0 100644 --- a/doc/development/deprecation_guidelines/img/deprecation_removal_process.png +++ b/doc/development/deprecation_guidelines/img/deprecation_removal_process.png diff --git a/doc/user/analytics/img/time_to_restore_service_charts_v15_1.png b/doc/user/analytics/img/time_to_restore_service_charts_v15_1.png Binary files differindex 25aac385750..fbc59f72e72 100644 --- a/doc/user/analytics/img/time_to_restore_service_charts_v15_1.png +++ b/doc/user/analytics/img/time_to_restore_service_charts_v15_1.png diff --git a/doc/user/application_security/policies/scan-execution-policies.md b/doc/user/application_security/policies/scan-execution-policies.md index 6c1367b3fc9..37c1cff4ce6 100644 --- a/doc/user/application_security/policies/scan-execution-policies.md +++ b/doc/user/application_security/policies/scan-execution-policies.md @@ -209,16 +209,10 @@ scan_execution_policy: rules: - type: schedule cadence: "15 3 * * *" - clusters: - production-cluster: - containers: - - database - resources: - - production-application + agents: + production-agent: namespaces: - production-namespace - kinds: - - deployment actions: - scan: cluster_image_scanning ``` diff --git a/doc/user/clusters/agent/troubleshooting.md b/doc/user/clusters/agent/troubleshooting.md index cc2ef48c341..1eca2eaecd5 100644 --- a/doc/user/clusters/agent/troubleshooting.md +++ b/doc/user/clusters/agent/troubleshooting.md @@ -207,3 +207,25 @@ to the Helm command when [installing the agent](install/#install-the-agent-in-th ```shell kubectl create serviceaccount gitlab-agent -n gitlab-agent ``` + +## Failed to perform vulnerability scan on workload: jobs.batch already exists + +```json +{ + "level": "error", + "time": "2022-06-22T21:03:04.769Z", + "msg": "Failed to perform vulnerability scan on workload", + "mod_name": "starboard_vulnerability", + "error": "running scan job: creating job: jobs.batch \"scan-vulnerabilityreport-b8d497769\" already exists" +} +``` + +The GitLab agent performs vulnerability scans by creating a job to scan each workload. If a scan +is interrupted, these jobs may be left behind and will need to be cleaned up before more jobs can +be run. You can clean up these jobs by running: + +```shell +kubectl delete jobs -l app.kubernetes.io/managed-by=starboard -n gitlab-agent +``` + +[We're working on making the cleanup of these jobs more robust.](https://gitlab.com/gitlab-org/gitlab/-/issues/362016) diff --git a/doc/user/infrastructure/clusters/connect/img/variables_civo.png b/doc/user/infrastructure/clusters/connect/img/variables_civo.png Binary files differindex 5a20478b13c..a668c3dd53c 100644 --- a/doc/user/infrastructure/clusters/connect/img/variables_civo.png +++ b/doc/user/infrastructure/clusters/connect/img/variables_civo.png diff --git a/doc/user/project/img/time_tracking_report_v15_1.png b/doc/user/project/img/time_tracking_report_v15_1.png Binary files differindex a9ddefebb2f..4eeccf8a684 100644 --- a/doc/user/project/img/time_tracking_report_v15_1.png +++ b/doc/user/project/img/time_tracking_report_v15_1.png diff --git a/doc/user/project/issues/img/turn_on_confidentiality_v15_1.png b/doc/user/project/issues/img/turn_on_confidentiality_v15_1.png Binary files differindex 24a7ad554f8..c81ac85ab13 100644 --- a/doc/user/project/issues/img/turn_on_confidentiality_v15_1.png +++ b/doc/user/project/issues/img/turn_on_confidentiality_v15_1.png diff --git a/doc/user/search/img/basic_search_results_v15_1.png b/doc/user/search/img/basic_search_results_v15_1.png Binary files differindex b85627c9b95..0de0b976d7d 100644 --- a/doc/user/search/img/basic_search_results_v15_1.png +++ b/doc/user/search/img/basic_search_results_v15_1.png diff --git a/doc/user/search/img/code_search_git_blame_v15_1.png b/doc/user/search/img/code_search_git_blame_v15_1.png Binary files differindex e61ee5993c2..426f829b186 100644 --- a/doc/user/search/img/code_search_git_blame_v15_1.png +++ b/doc/user/search/img/code_search_git_blame_v15_1.png diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index cb4cbc81b33..12dae79c60e 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -105,7 +105,6 @@ module Gitlab clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled), clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled), clusters_management_project: count(::Clusters::Cluster.with_management_project), - clusters_integrations_elastic_stack: count(::Clusters::Integrations::ElasticStack.enabled), clusters_integrations_prometheus: count(::Clusters::Integrations::Prometheus.enabled), kubernetes_agents: count(::Clusters::Agent), kubernetes_agents_with_token: distinct_count(::Clusters::AgentToken, :agent_id), diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index d5f94be65b6..ac56d3af54f 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -134,6 +134,15 @@ RSpec.describe Import::BitbucketServerController do expect(response).to have_gitlab_http_status(:found) expect(response).to redirect_to(status_import_bitbucket_server_path) end + + it 'passes namespace_id to status page if provided' do + namespace_id = 5 + allow(controller).to receive(:allow_local_requests?).and_return(true) + + post :configure, params: { personal_access_token: token, bitbucket_server_username: username, bitbucket_server_url: url, namespace_id: namespace_id } + + expect(response).to redirect_to(status_import_bitbucket_server_path(namespace_id: namespace_id)) + end end describe 'GET status' do @@ -160,6 +169,14 @@ RSpec.describe Import::BitbucketServerController do expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name) end + it 'redirects to connection form if session is missing auth data' do + session[:bitbucket_server_url] = nil + + get :status, format: :html + + expect(response).to redirect_to(new_import_bitbucket_server_path) + end + it_behaves_like 'import controller status' do let(:repo) { @repo } let(:repo_id) { "#{@repo.project_key}/#{@repo.slug}" } diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb index 8a43ea64390..0e944b90d0c 100644 --- a/spec/factories/usage_data.rb +++ b/spec/factories/usage_data.rb @@ -83,7 +83,6 @@ FactoryBot.define do # Cluster Integrations create(:clusters_integrations_prometheus, cluster: gcp_cluster) - create(:clusters_integrations_elastic_stack, cluster: gcp_cluster) create(:grafana_integration, project: projects[0], enabled: true) create(:grafana_integration, project: projects[1], enabled: true) diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js index b45abe418e4..6013fa3ec39 100644 --- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js +++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js @@ -1,4 +1,4 @@ -import { GlPagination, GlTable } from '@gitlab/ui'; +import { GlButton, GlPagination, GlTable } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue'; @@ -164,8 +164,8 @@ describe('~/access_tokens/components/access_token_table_app', () => { expect(cells.at(3).text()).toBe(__('Never')); expect(cells.at(4).text()).toBe(__('Never')); expect(cells.at(5).text()).toBe('Maintainer'); - let anchor = cells.at(6).find('a'); - expect(anchor.attributes()).toMatchObject({ + let button = cells.at(6).findComponent(GlButton); + expect(button.attributes()).toMatchObject({ 'aria-label': __('Revoke'), 'data-qa-selector': __('revoke_button'), href: '/-/profile/personal_access_tokens/1/revoke', @@ -176,8 +176,7 @@ describe('~/access_tokens/components/access_token_table_app', () => { { accessTokenType }, ), }); - - expect(anchor.classes()).toContain('btn-danger-secondary'); + expect(button.props('category')).toBe('tertiary'); // Second row expect(cells.at(7).text()).toBe('b'); @@ -186,9 +185,9 @@ describe('~/access_tokens/components/access_token_table_app', () => { expect(cells.at(10).text()).not.toBe(__('Never')); expect(cells.at(11).text()).toBe(__('Expired')); expect(cells.at(12).text()).toBe('Maintainer'); - anchor = cells.at(13).find('a'); - expect(anchor.attributes('href')).toBe('/-/profile/personal_access_tokens/2/revoke'); - expect(anchor.classes()).toEqual(['btn', 'btn-danger', 'btn-md', 'gl-button', 'btn-icon']); + button = cells.at(13).findComponent(GlButton); + expect(button.attributes('href')).toBe('/-/profile/personal_access_tokens/2/revoke'); + expect(button.props('category')).toBe('tertiary'); }); it('sorts rows alphabetically', async () => { diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 3a223dfd491..77e112edaca 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -560,7 +560,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:clusters_platforms_eks]).to eq(1) expect(count_data[:clusters_platforms_gke]).to eq(1) expect(count_data[:clusters_platforms_user]).to eq(1) - expect(count_data[:clusters_integrations_elastic_stack]).to eq(1) expect(count_data[:clusters_integrations_prometheus]).to eq(1) expect(count_data[:grafana_integrated_projects]).to eq(2) expect(count_data[:clusters_management_project]).to eq(1) diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb index 801505f0231..b051f646bd4 100644 --- a/spec/models/ci/pipeline_artifact_spec.rb +++ b/spec/models/ci/pipeline_artifact_spec.rb @@ -196,6 +196,80 @@ RSpec.describe Ci::PipelineArtifact, type: :model do end end + describe '.create_or_replace_for_pipeline!' do + let_it_be(:pipeline) { create(:ci_empty_pipeline) } + + let(:file_type) { :code_coverage } + let(:file) { CarrierWaveStringFile.new_file(file_content: 'content', filename: 'file.json', content_type: 'json') } + let(:size) { file['tempfile'].size } + + subject do + Ci::PipelineArtifact.create_or_replace_for_pipeline!( + pipeline: pipeline, + file_type: file_type, + file: file, + size: size + ) + end + + around do |example| + freeze_time { example.run } + end + + context 'when there is no existing record' do + it 'creates a new pipeline artifact for the given parameters' do + expect { subject }.to change { Ci::PipelineArtifact.count }.from(0).to(1) + + expect(subject.code_coverage?).to be(true) + expect(subject.pipeline).to eq(pipeline) + expect(subject.project_id).to eq(pipeline.project_id) + expect(subject.file.filename).to eq(file['filename']) + expect(subject.size).to eq(size) + expect(subject.file_format).to eq(Ci::PipelineArtifact::REPORT_TYPES[file_type].to_s) + expect(subject.expire_at).to eq(Ci::PipelineArtifact::EXPIRATION_DATE.from_now) + end + end + + context 'when there are existing records with different types' do + let!(:existing_artifact) do + create(:ci_pipeline_artifact, pipeline: pipeline, file_type: file_type, expire_at: 1.day.from_now) + end + + let!(:other_artifact) { create(:ci_pipeline_artifact, pipeline: pipeline, file_type: :code_quality_mr_diff) } + + it 'replaces the existing pipeline artifact record with the given file type' do + expect { subject }.not_to change { Ci::PipelineArtifact.count } + + expect(subject.id).not_to eq(existing_artifact.id) + + expect(subject.code_coverage?).to be(true) + expect(subject.pipeline).to eq(pipeline) + expect(subject.project_id).to eq(pipeline.project_id) + expect(subject.file.filename).to eq(file['filename']) + expect(subject.size).to eq(size) + expect(subject.file_format).to eq(Ci::PipelineArtifact::REPORT_TYPES[file_type].to_s) + expect(subject.expire_at).to eq(Ci::PipelineArtifact::EXPIRATION_DATE.from_now) + end + end + + context 'when ActiveRecordError is raised' do + let(:pipeline) { instance_double(Ci::Pipeline, id: 1) } + let(:file_type) { :code_coverage } + let(:error) { ActiveRecord::ActiveRecordError.new('something went wrong') } + + before do + allow(pipeline).to receive(:pipeline_artifacts).and_raise(error) + end + + it 'tracks and raise the exception' do + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_exception) + .with(error, { pipeline_id: pipeline.id, file_type: file_type }).and_call_original + + expect { subject }.to raise_error(ActiveRecord::ActiveRecordError, 'something went wrong') + end + end + end + describe '#present' do subject(:presenter) { report.present } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 31752f300f4..b719adcdace 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -106,6 +106,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe 'state machine transitions' do + context 'from failed to success' do + let_it_be(:pipeline) { create(:ci_empty_pipeline, :failed) } + + it 'schedules CoverageReportWorker' do + expect(Ci::PipelineArtifacts::CoverageReportWorker).to receive(:perform_async).with(pipeline.id) + + pipeline.succeed! + end + end + end + describe '#set_status' do let(:pipeline) { build(:ci_empty_pipeline, :created) } @@ -3760,6 +3772,24 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#self_and_descendants_complete?' do + let_it_be(:pipeline) { create(:ci_pipeline, :success) } + let_it_be(:child_pipeline) { create(:ci_pipeline, :success, child_of: pipeline) } + let_it_be_with_reload(:grandchild_pipeline) { create(:ci_pipeline, :success, child_of: child_pipeline) } + + context 'when all pipelines in the hierarchy is complete' do + it { expect(pipeline.self_and_descendants_complete?).to be(true) } + end + + context 'when a pipeline in the hierarchy is not complete' do + before do + grandchild_pipeline.update!(status: :running) + end + + it { expect(pipeline.self_and_descendants_complete?).to be(false) } + end + end + describe '#builds_in_self_and_descendants' do subject(:builds) { pipeline.builds_in_self_and_descendants } diff --git a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb index 403afde5da3..4b85c52ebce 100644 --- a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb +++ b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb @@ -2,16 +2,18 @@ require 'spec_helper' -RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do +RSpec.describe Ci::PipelineArtifacts::CoverageReportService do describe '#execute' do let_it_be(:project) { create(:project, :repository) } subject { described_class.new(pipeline).execute } - shared_examples 'creating a pipeline coverage report' do + shared_examples 'creating or updating a pipeline coverage report' do context 'when pipeline is finished' do - it 'creates a pipeline artifact' do - expect { subject }.to change { Ci::PipelineArtifact.count }.from(0).to(1) + it 'creates or updates a pipeline artifact' do + subject + + expect(pipeline.reload.pipeline_artifacts.count).to eq(1) end it 'persists the default file name' do @@ -22,7 +24,7 @@ RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do expect(file.filename).to eq('code_coverage.json') end - it 'sets expire_at to 1 week' do + it 'sets expire_at to 1 week from now' do freeze_time do subject @@ -31,13 +33,16 @@ RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do expect(pipeline_artifact.expire_at).to eq(1.week.from_now) end end - end - context 'when pipeline artifact has already been created' do - it 'does not raise an error and does not persist the same artifact twice' do - expect { 2.times { described_class.new(pipeline).execute } }.not_to raise_error + it 'logs relevant information' do + expect(Gitlab::AppLogger).to receive(:info).with({ + project_id: project.id, + pipeline_id: pipeline.id, + pipeline_artifact_id: kind_of(Numeric), + message: kind_of(String) + }) - expect(Ci::PipelineArtifact.count).to eq(1) + subject end end end @@ -45,21 +50,42 @@ RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do context 'when pipeline has coverage report' do let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) } - it_behaves_like 'creating a pipeline coverage report' + it_behaves_like 'creating or updating a pipeline coverage report' end context 'when pipeline has coverage report from child pipeline' do let!(:pipeline) { create(:ci_pipeline, :success, project: project) } let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) } - it_behaves_like 'creating a pipeline coverage report' + it_behaves_like 'creating or updating a pipeline coverage report' + end + + context 'when pipeline has existing pipeline artifact for coverage report' do + let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) } + let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) } + + let!(:pipeline_artifact) do + create(:ci_pipeline_artifact, :with_coverage_report, pipeline: pipeline, expire_at: 1.day.from_now) + end + + it_behaves_like 'creating or updating a pipeline coverage report' + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(ci_child_pipeline_coverage_reports: false) + end + + it 'does not change existing pipeline artifact' do + expect { subject }.not_to change { pipeline_artifact.reload.updated_at } + end + end end context 'when pipeline is running and coverage report does not exist' do let(:pipeline) { create(:ci_pipeline, :running) } it 'does not persist data' do - expect { subject }.not_to change { Ci::PipelineArtifact.count } + expect { subject }.not_to change { Ci::PipelineArtifact.count }.from(0) end end end diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 1041350129e..2a9144614d0 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -53,7 +53,6 @@ module UsageDataHelpers clusters_platforms_eks clusters_platforms_gke clusters_platforms_user - clusters_integrations_elastic_stack clusters_integrations_prometheus clusters_management_project in_review_folder diff --git a/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb index 000eda055af..f51e66aa948 100644 --- a/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb +++ b/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb @@ -2,28 +2,71 @@ require 'spec_helper' -RSpec.describe ::Ci::PipelineArtifacts::CoverageReportWorker do +RSpec.describe Ci::PipelineArtifacts::CoverageReportWorker do describe '#perform' do + let(:pipeline_id) { pipeline.id } + subject { described_class.new.perform(pipeline_id) } context 'when pipeline exists' do - let(:pipeline) { create(:ci_pipeline) } - let(:pipeline_id) { pipeline.id } + let(:pipeline) { create(:ci_pipeline, :success) } - it 'calls pipeline report result service' do - expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService) do |create_artifact_service| - expect(create_artifact_service).to receive(:execute) + it 'calls the pipeline coverage report service' do + expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService, pipeline) do |service| + expect(service).to receive(:execute) end subject end end + context 'when the pipeline is part of a hierarchy' do + let_it_be(:root_ancestor_pipeline) { create(:ci_pipeline, :success) } + let_it_be(:pipeline) { create(:ci_pipeline, :success, child_of: root_ancestor_pipeline) } + let_it_be(:another_child_pipeline) { create(:ci_pipeline, :success, child_of: root_ancestor_pipeline) } + + context 'when all pipelines is complete' do + it 'calls the pipeline coverage report service on the root ancestor pipeline' do + expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService, root_ancestor_pipeline) do |service| + expect(service).to receive(:execute) + end + + subject + end + end + + context 'when the pipeline hierarchy has incomplete pipeline' do + before do + another_child_pipeline.update!(status: :running) + end + + it 'does not call pipeline coverage report service' do + expect(Ci::PipelineArtifacts::CoverageReportService).not_to receive(:new) + + subject + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ci_child_pipeline_coverage_reports: false) + end + + it 'calls the pipeline coverage report service on the pipeline' do + expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService, pipeline) do |service| + expect(service).to receive(:execute) + end + + subject + end + end + end + context 'when pipeline does not exist' do let(:pipeline_id) { non_existing_record_id } it 'does not call pipeline create artifact service' do - expect(Ci::PipelineArtifacts::CoverageReportService).not_to receive(:execute) + expect(Ci::PipelineArtifacts::CoverageReportService).not_to receive(:new) subject end |