diff options
43 files changed, 712 insertions, 508 deletions
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index f93b1da4f58..de6755e0414 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -81,9 +81,8 @@ export default { time: new Date(), value: 0, }, - currentDataIndex: 0, currentXCoordinate: 0, - currentFlagPosition: 0, + currentCoordinates: [], showFlag: false, showFlagContent: false, timeSeries: [], @@ -273,6 +272,9 @@ export default { :line-style="path.lineStyle" :line-color="path.lineColor" :area-color="path.areaColor" + :current-coordinates="currentCoordinates[index]" + :current-time-series-index="index" + :show-dot="showFlagContent" /> <graph-deployment :deployment-data="reducedDeploymentData" @@ -298,9 +300,9 @@ export default { :show-flag-content="showFlagContent" :time-series="timeSeries" :unit-of-display="unitOfDisplay" - :current-data-index="currentDataIndex" :legend-title="legendTitle" :deployment-flag-data="deploymentFlagData" + :current-coordinates="currentCoordinates" /> </div> <graph-legend diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index b8202e25685..8a771107de8 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -47,14 +47,14 @@ export default { type: String, required: true, }, - currentDataIndex: { - type: Number, - required: true, - }, legendTitle: { type: String, required: true, }, + currentCoordinates: { + type: Array, + required: true, + }, }, computed: { formatTime() { @@ -90,10 +90,12 @@ export default { }, }, methods: { - seriesMetricValue(series) { + seriesMetricValue(seriesIndex, series) { + const indexFromCoordinates = this.currentCoordinates[seriesIndex] + ? this.currentCoordinates[seriesIndex].currentDataIndex : 0; const index = this.deploymentFlagData ? this.deploymentFlagData.seriesIndex - : this.currentDataIndex; + : indexFromCoordinates; const value = series.values[index] && series.values[index].value; if (isNaN(value)) { return '-'; @@ -128,7 +130,7 @@ export default { <h5 v-if="deploymentFlagData"> Deployed </h5> - {{ formatDate }} at + {{ formatDate }} <strong>{{ formatTime }}</strong> </div> <div @@ -163,9 +165,11 @@ export default { :key="index" > <track-line :track="series"/> - <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td> <td> - <strong>{{ seriesMetricValue(series) }}</strong> + {{ series.track }} {{ seriesMetricLabel(index, series) }} + </td> + <td> + <strong>{{ seriesMetricValue(index, series) }}</strong> </td> </tr> </table> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 881560124a5..52f8aa2ee3f 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -22,6 +22,15 @@ export default { type: String, required: true, }, + currentCoordinates: { + type: Object, + required: false, + default: () => ({ currentX: 0, currentY: 0 }), + }, + showDot: { + type: Boolean, + required: true, + }, }, computed: { strokeDashArray() { @@ -33,12 +42,20 @@ export default { }; </script> <template> - <g> + <g transform="translate(-5, 20)"> + <circle + class="circle-path" + :cx="currentCoordinates.currentX" + :cy="currentCoordinates.currentY" + :fill="lineColor" + :stroke="lineColor" + r="3" + v-if="showDot" + /> <path class="metric-area" :d="generatedAreaPath" :fill="areaColor" - transform="translate(-5, 20)" /> <path class="metric-line" @@ -47,7 +64,6 @@ export default { fill="none" stroke-width="1" :stroke-dasharray="strokeDashArray" - transform="translate(-5, 20)" /> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue index 79b322e2e42..18be65fd1ef 100644 --- a/app/assets/javascripts/monitoring/components/graph/track_line.vue +++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue @@ -19,16 +19,16 @@ export default { <template> <td> <svg - width="15" - height="6"> + width="16" + height="8"> <line :stroke-dasharray="stylizedLine" :stroke="track.lineColor" stroke-width="4" :x1="0" - :x2="15" - :y1="2" - :y2="2" + :x2="16" + :y1="4" + :y2="4" /> </svg> </td> diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 6cc67ba57ee..4f23814ff3e 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -52,14 +52,22 @@ const mixins = { positionFlag() { const timeSeries = this.timeSeries[0]; const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1); + this.currentData = timeSeries.values[hoveredDataIndex]; - this.currentDataIndex = hoveredDataIndex; this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); - if (this.currentXCoordinate > (this.graphWidth - 200)) { - this.currentFlagPosition = this.currentXCoordinate - 103; - } else { - this.currentFlagPosition = this.currentXCoordinate; - } + + this.currentCoordinates = this.timeSeries.map((series) => { + const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1); + const currentData = series.values[currentDataIndex]; + const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); + const currentY = Math.floor(series.timeSeriesScaleY(currentData.value)); + + return { + currentX, + currentY, + currentDataIndex, + }; + }); if (this.hoverData.currentDeployXPos) { this.showFlag = false; diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js index f3c9acdd93e..d88c13609dc 100644 --- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js +++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js @@ -14,7 +14,7 @@ const d3 = { timeYear, }; -export const dateFormat = d3.time('%a, %b %-d'); +export const dateFormat = d3.time('%d %b %Y, '); export const timeFormat = d3.time('%-I:%M%p'); export const dateFormatWithName = d3.time('%a, %b %-d'); export const bisectDate = d3.bisector(d => d.time).left; diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 8a93c7e6bae..4d3f1f1a7cc 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom linePath: lineFunction(timeSeries.values), areaPath: areaFunction(timeSeries.values), timeSeriesScaleX, + timeSeriesScaleY, values: timeSeries.values, max: maximumValue, average: accum / timeSeries.values.length, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index c1618bc6ea0..3e36a3a10f9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import { s__, __ } from '~/locale'; + import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -16,6 +17,7 @@ mrWidgetAuthorTime, loadingIcon, statusIcon, + ClipboardButton, }, props: { mr: { @@ -162,6 +164,18 @@ <span class="label-branch"> <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> </span> + with + <a + :href="mr.mergeCommitPath" + class="commit-sha js-mr-merged-commit-sha" + > + {{ mr.shortMergeCommitSha }} + </a> + <clipboard-button + :title="__('Copy commit SHA to clipboard')" + :text="mr.shortMergeCommitSha" + css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha" + /> </p> <p v-if="mr.sourceBranchRemoved"> {{ s__("mrWidget|The source branch has been removed") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index a47ca9fae86..83b7b054e6f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -20,6 +20,7 @@ export default class MergeRequestStore { this.sourceBranch = data.source_branch; this.mergeStatus = data.merge_status; this.commitMessage = data.merge_commit_message; + this.shortMergeCommitSha = data.short_merge_commit_sha; this.commitMessageWithDescription = data.merge_commit_message_with_description; this.commitsCount = data.commits_count; this.divergedCommitsCount = data.diverged_commits_count; @@ -65,6 +66,7 @@ export default class MergeRequestStore { this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path; this.mergeCheckPath = data.merge_check_path; this.mergeActionsContentPath = data.commit_change_content_path; + this.mergeCommitPath = data.merge_commit_path; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; this.isOpen = data.state === 'opened'; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3a300086fa3..1f406cc1c2d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -283,28 +283,59 @@ } &.popover { + padding: 0; + border: 1px solid $border-color; + &.left { left: auto; right: 0; margin-right: 10px; + + > .arrow { + right: -16px; + border-left-color: $border-color; + } + + > .arrow::after { + border-left-color: $theme-gray-50; + } } &.right { left: 0; right: auto; margin-left: 10px; + + > .arrow { + left: -16px; + border-right-color: $border-color; + } + + > .arrow::after { + border-right-color: $theme-gray-50; + } } > .arrow { - top: 40px; + top: 16px; + margin-top: -8px; + border-width: 8px; } > .popover-title, > .popover-content { - padding: 5px 8px; + padding: 8px; font-size: 12px; white-space: nowrap; } + + > .popover-title { + background-color: $theme-gray-50; + } + } + + strong { + font-weight: 600; } } @@ -317,7 +348,7 @@ vertical-align: middle; + td { - padding-left: 5px; + padding-left: 8px; vertical-align: top; } } diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 898e88344db..0b1b46944aa 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -87,7 +87,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def failures - if @pipeline.statuses.latest.failed.present? + if @pipeline.failed_builds.present? render_show else redirect_to pipeline_path(@pipeline) diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 2589215ad19..eef9caf1c8e 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -60,13 +60,16 @@ module ReactiveCaching end def with_reactive_cache(*args, &blk) - within_reactive_cache_lifetime(*args) do + bootstrap = !within_reactive_cache_lifetime?(*args) + Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) + + if bootstrap + ReactiveCachingWorker.perform_async(self.class, id, *args) + nil + else data = Rails.cache.read(full_reactive_cache_key(*args)) yield data if data.present? end - ensure - Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) - ReactiveCachingWorker.perform_async(self.class, id, *args) end def clear_reactive_cache!(*args) @@ -75,7 +78,7 @@ module ReactiveCaching def exclusively_update_reactive_cache!(*args) locking_reactive_cache(*args) do - within_reactive_cache_lifetime(*args) do + if within_reactive_cache_lifetime?(*args) enqueuing_update(*args) do value = calculate_reactive_cache(*args) Rails.cache.write(full_reactive_cache_key(*args), value) @@ -105,8 +108,8 @@ module ReactiveCaching Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid) end - def within_reactive_cache_lifetime(*args) - yield if Rails.cache.read(alive_reactive_cache_key(*args)) + def within_reactive_cache_lifetime?(*args) + !!Rails.cache.read(alive_reactive_cache_key(*args)) end def enqueuing_update(*args) diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb index 703a72c355c..3340dc96e9f 100644 --- a/app/models/concerns/sha_attribute.rb +++ b/app/models/concerns/sha_attribute.rb @@ -4,18 +4,33 @@ module ShaAttribute module ClassMethods def sha_attribute(name) return if ENV['STATIC_VERIFICATION'] - return unless table_exists? + + validate_binary_column_exists!(name) unless Rails.env.production? + + attribute(name, Gitlab::Database::ShaAttribute.new) + end + + # This only gets executed in non-production environments as an additional check to ensure + # the column is the correct type. In production it should behave like any other attribute. + # See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5502 for more discussion + def validate_binary_column_exists!(name) + unless table_exists? + warn "WARNING: sha_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations" + return + end column = columns.find { |c| c.name == name.to_s } - # In case the table doesn't exist we won't be able to find the column, - # thus we will only check the type if the column is present. - if column && column.type != :binary - raise ArgumentError, - "sha_attribute #{name.inspect} is invalid since the column type is not :binary" + unless column + raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column doesn't exist") end - attribute(name, Gitlab::Database::ShaAttribute.new) + unless column.type == :binary + raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column type is not :binary") + end + rescue => error + Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}" + raise end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 63c6ada86e1..628c61d5d69 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1007,6 +1007,10 @@ class MergeRequest < ActiveRecord::Base @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha end + def short_merge_commit_sha + Commit.truncate_sha(merge_commit_sha) if merge_commit_sha + end + def can_be_reverted?(current_user) return false unless merge_commit diff --git a/app/models/project.rb b/app/models/project.rb index 0a549d843f3..f6ac1802846 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -657,8 +657,8 @@ class Project < ActiveRecord::Base } end - def ensure_import_state - return if self[:import_status] == 'none' || self[:import_status].nil? + def ensure_import_state(force: false) + return if !force && (self[:import_status] == 'none' || self[:import_status].nil?) return unless import_state.nil? create_import_state(import_state_args) @@ -667,39 +667,39 @@ class Project < ActiveRecord::Base end def import_schedule - ensure_import_state + ensure_import_state(force: true) - import_state&.schedule + import_state.schedule end def force_import_start - ensure_import_state + ensure_import_state(force: true) - import_state&.force_start + import_state.force_start end def import_start - ensure_import_state + ensure_import_state(force: true) - import_state&.start + import_state.start end def import_fail - ensure_import_state + ensure_import_state(force: true) - import_state&.fail_op + import_state.fail_op end def import_finish - ensure_import_state + ensure_import_state(force: true) - import_state&.finish + import_state.finish end def import_jid=(new_jid) - ensure_import_state + ensure_import_state(force: true) - import_state&.jid = new_jid + import_state.jid = new_jid end def import_jid @@ -709,9 +709,9 @@ class Project < ActiveRecord::Base end def import_error=(new_error) - ensure_import_state + ensure_import_state(force: true) - import_state&.last_error = new_error + import_state.last_error = new_error end def import_error @@ -721,9 +721,9 @@ class Project < ActiveRecord::Base end def import_status=(new_status) - ensure_import_state + ensure_import_state(force: true) - import_state&.status = new_status + import_state.status = new_status end def import_status diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index 099b4720fb6..cc2bce9862d 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -1,11 +1,21 @@ module Ci class PipelinePresenter < Gitlab::View::Presenter::Delegated + include Gitlab::Utils::StrongMemoize + FAILURE_REASONS = { config_error: 'CI/CD YAML configuration error!' }.freeze presents :pipeline + def failed_builds + return [] unless can?(current_user, :read_build, pipeline) + + strong_memoize(:failed_builds) do + pipeline.builds.latest.failed + end + end + def failure_reason return unless pipeline.failure_reason? diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 4a812e39ee1..d0165c148eb 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -2,6 +2,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :state expose :in_progress_merge_commit_sha expose :merge_commit_sha + expose :short_merge_commit_sha expose :merge_error expose :merge_params expose :merge_status @@ -207,6 +208,12 @@ class MergeRequestWidgetEntity < IssuableEntity commit_change_content_project_merge_request_path(merge_request.project, merge_request) end + expose :merge_commit_path do |merge_request| + if merge_request.merge_commit_sha + project_commit_path(merge_request.project, merge_request.merge_commit_sha) + end + end + private delegate :current_user, to: :request diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 218e7338c83..4dbf95be357 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,5 +1,3 @@ -- failed_builds = @pipeline.statuses.latest.failed - .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator %li.js-pipeline-tab-link @@ -9,11 +7,11 @@ = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do = _("Jobs") %span.badge.js-builds-counter= pipeline.total_size - - if failed_builds.present? + - if @pipeline.failed_builds.present? %li.js-failures-tab-link = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do = _("Failed Jobs") - %span.badge.js-failures-counter= failed_builds.count + %span.badge.js-failures-counter= @pipeline.failed_builds.count .tab-content #js-tab-pipeline.tab-pane @@ -43,9 +41,10 @@ %th Coverage %th = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - - if failed_builds.present? + + - if @pipeline.failed_builds.present? #js-tab-failures.build-failures.tab-pane - - failed_builds.each_with_index do |build, index| + - @pipeline.failed_builds.each_with_index do |build, index| .build-state %span.ci-status-icon-failed= custom_icon('icon_status_failed') %span.stage diff --git a/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml b/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml new file mode 100644 index 00000000000..a7128f7481e --- /dev/null +++ b/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml @@ -0,0 +1,5 @@ +--- +title: Display merge commit SHA in merge widget after merge +merge_request: 18722 +author: +type: added diff --git a/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml b/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml new file mode 100644 index 00000000000..4db0ff4f3a0 --- /dev/null +++ b/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml @@ -0,0 +1,5 @@ +--- +title: ShaAttribute no longer stops startup if database is missing +merge_request: 18726 +author: +type: fixed diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml new file mode 100644 index 00000000000..98c56cf2b57 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml @@ -0,0 +1,5 @@ +--- +title: 'Replace the `project/builds/artifacts.feature` spinach test with an rspec analog' +merge_request: 18729 +author: '@blackst0ne' +type: other diff --git a/changelogs/unreleased/fix-reactive-cache-retry-rate.yml b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml new file mode 100644 index 00000000000..044e7fe39c0 --- /dev/null +++ b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml @@ -0,0 +1,5 @@ +--- +title: Update commit status from external CI services less aggressively +merge_request: 18802 +author: +type: fixed diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature deleted file mode 100644 index 5abc24949cf..00000000000 --- a/features/project/builds/artifacts.feature +++ /dev/null @@ -1,65 +0,0 @@ -Feature: Project Builds Artifacts - Background: - Given I sign in as a user - And I own a project - And project has CI enabled - And project has a recent build - - Scenario: I download build artifacts - Given recent build has artifacts available - When I visit recent build details page - And I click artifacts download button - Then download of build artifacts archive starts - - Scenario: I browse build artifacts - Given recent build has artifacts available - And recent build has artifacts metadata available - When I visit recent build details page - And I click artifacts browse button - Then I should see content of artifacts archive - And I should see the build header - - Scenario: I browse subdirectory of build artifacts - Given recent build has artifacts available - And recent build has artifacts metadata available - When I visit recent build details page - And I click artifacts browse button - And I click link to subdirectory within build artifacts - Then I should see content of subdirectory within artifacts archive - And I should see the directory name in the breadcrumb - - Scenario: I browse directory with UTF-8 characters in name - Given recent build has artifacts available - And recent build has artifacts metadata available - And recent build artifacts contain directory with UTF-8 characters - When I visit recent build details page - And I click artifacts browse button - And I navigate to directory with UTF-8 characters in name - Then I should see content of directory with UTF-8 characters in name - - Scenario: I try to browse directory with invalid UTF-8 characters in name - Given recent build has artifacts available - And recent build has artifacts metadata available - And recent build artifacts contain directory with invalid UTF-8 characters - When I visit recent build details page - And I click artifacts browse button - And I navigate to parent directory of directory with invalid name - Then I should not see directory with invalid name on the list - - @javascript - Scenario: I download a single file from build artifacts - Given recent build has artifacts available - And recent build has artifacts metadata available - When I visit recent build details page - And I click artifacts browse button - And I click a link to file within build artifacts - Then I see a download link - - @javascript - Scenario: I click on a row in an artifacts table - Given recent build has artifacts available - And recent build has artifacts metadata available - When I visit recent build details page - And I click artifacts browse button - And I click a first row within build artifacts table - Then page with a coresponding path is loading diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb deleted file mode 100644 index 4b72355b125..00000000000 --- a/features/steps/project/builds/artifacts.rb +++ /dev/null @@ -1,98 +0,0 @@ -class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedBuilds - include RepoHelpers - include WaitForRequests - - step 'I click artifacts download button' do - click_link 'Download' - end - - step 'I click artifacts browse button' do - click_link 'Browse' - expect(page).not_to have_selector('.build-sidebar') - end - - step 'I should see content of artifacts archive' do - page.within('.tree-table') do - expect(page).to have_no_content '..' - expect(page).to have_content 'other_artifacts_0.1.2' - expect(page).to have_content 'ci_artifacts.txt' - expect(page).to have_content 'rails_sample.jpg' - end - end - - step 'I should see the build header' do - page.within('.build-header') do - expect(page).to have_content "Job ##{@build.id} in pipeline ##{@pipeline.id} for #{@pipeline.short_sha}" - end - end - - step 'I click link to subdirectory within build artifacts' do - page.within('.tree-table') { click_link 'other_artifacts_0.1.2' } - end - - step 'I should see content of subdirectory within artifacts archive' do - page.within('.tree-table') do - expect(page).to have_content '..' - expect(page).to have_content 'another-subdirectory' - expect(page).to have_content 'doc_sample.txt' - end - end - - step 'I should see the directory name in the breadcrumb' do - page.within('.repo-breadcrumb') do - expect(page).to have_content 'other_artifacts_0.1.2' - end - end - - step 'recent build artifacts contain directory with UTF-8 characters' do - # metadata fixture contains relevant directory - end - - step 'I navigate to directory with UTF-8 characters in name' do - page.within('.tree-table') { click_link 'tests_encoding' } - page.within('.tree-table') { click_link 'utf8 test dir ✓' } - end - - step 'I should see content of directory with UTF-8 characters in name' do - page.within('.tree-table') do - expect(page).to have_content '..' - expect(page).to have_content 'regular_file_2' - end - end - - step 'recent build artifacts contain directory with invalid UTF-8 characters' do - # metadata fixture contains relevant directory - end - - step 'I navigate to parent directory of directory with invalid name' do - page.within('.tree-table') { click_link 'tests_encoding' } - end - - step 'I should not see directory with invalid name on the list' do - page.within('.tree-table') do - expect(page).to have_no_content('non-utf8-dir') - end - end - - step 'I click a link to file within build artifacts' do - page.within('.tree-table') { find_link('ci_artifacts.txt').click } - wait_for_requests - end - - step 'I see a download link' do - expect(page).to have_link 'download it' - end - - step 'I click a first row within build artifacts table' do - row = first('tr[data-link]') - @row_path = row['data-link'] - row.click - end - - step 'page with a coresponding path is loading' do - expect(current_path).to eq @row_path - end -end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb deleted file mode 100644 index c2197584d8d..00000000000 --- a/features/steps/shared/builds.rb +++ /dev/null @@ -1,53 +0,0 @@ -module SharedBuilds - include Spinach::DSL - - step 'project has CI enabled' do - @project.enable_ci - end - - step 'project has coverage enabled' do - @project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/) - end - - step 'project has a recent build' do - @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') - @build = create(:ci_build, :running, :coverage, :trace_artifact, pipeline: @pipeline) - end - - step 'recent build is successful' do - @build.success - end - - step 'recent build failed' do - @build.drop - end - - step 'project has another build that is running' do - create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run') - end - - step 'I visit recent build details page' do - visit project_job_path(@project, @build) - end - - step 'recent build has artifacts available' do - artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip' - archive = fixture_file_upload(artifacts, 'application/zip') - @build.update_attributes(legacy_artifacts_file: archive) - end - - step 'recent build has artifacts metadata available' do - metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' - gzip = fixture_file_upload(metadata, 'application/x-gzip') - @build.update_attributes(legacy_artifacts_metadata: gzip) - end - - step 'recent build has a build trace' do - @build.trace.set('job trace') - end - - step 'download of build artifacts archive starts' do - expect(page.response_headers['Content-Type']).to eq 'application/zip' - expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary' - end -end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index d129815aeac..16e025618a6 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -69,43 +69,19 @@ FactoryBot.define do end trait :import_scheduled do - transient do - status :scheduled - end - - before(:create) do |project, evaluator| - project.create_import_state(status: evaluator.status) - end + import_status :scheduled end trait :import_started do - transient do - status :started - end - - before(:create) do |project, evaluator| - project.create_import_state(status: evaluator.status) - end + import_status :started end trait :import_finished do - transient do - status :finished - end - - before(:create) do |project, evaluator| - project.create_import_state(status: evaluator.status) - end + import_status :finished end trait :import_failed do - transient do - status :failed - end - - before(:create) do |project, evaluator| - project.create_import_state(status: evaluator.status) - end + import_status :failed end trait :archived do diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb deleted file mode 100644 index cb69aff8d5f..00000000000 --- a/spec/features/projects/artifacts/browse_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -feature 'Browse artifact', :js do - let(:project) { create(:project, :public) } - let(:pipeline) { create(:ci_empty_pipeline, project: project) } - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } - let(:browse_url) do - browse_path('other_artifacts_0.1.2') - end - - def browse_path(path) - browse_project_job_artifacts_path(project, job, path) - end - - context 'when visiting old URL' do - before do - visit browse_url.sub('/-/jobs', '/builds') - end - - it "redirects to new URL" do - expect(page.current_path).to eq(browse_url) - end - end - - context 'when browsing a directory with an text file' do - let(:txt_entry) { job.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') } - - before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(true) - allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true) - end - - context 'when the project is public' do - it "shows external link icon and styles" do - visit browse_url - - link = first('.tree-item-file-external-link') - - expect(page).to have_link('doc_sample.txt', href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path)) - expect(link[:target]).to eq('_blank') - expect(link[:rel]).to include('noopener') - expect(link[:rel]).to include('noreferrer') - expect(page).to have_selector('.js-artifact-tree-external-icon') - end - end - - context 'when the project is private' do - let!(:private_project) { create(:project, :private) } - let(:pipeline) { create(:ci_empty_pipeline, project: private_project) } - let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } - let(:user) { create(:user) } - - before do - private_project.add_developer(user) - - sign_in(user) - end - - it 'shows internal link styles' do - visit browse_project_job_artifacts_path(private_project, job, 'other_artifacts_0.1.2') - - expect(page).to have_link('doc_sample.txt') - expect(page).not_to have_selector('.js-artifact-tree-external-icon') - end - end - end -end diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb deleted file mode 100644 index 6f76c14910b..00000000000 --- a/spec/features/projects/artifacts/download_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -feature 'Download artifact' do - let(:project) { create(:project, :public) } - let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) } - let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) } - - shared_examples 'downloading' do - it 'downloads the zip' do - expect(page.response_headers['Content-Disposition']) - .to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"}) - - # Check the content does match, but don't print this as error message - expect(page.source.b == job.artifacts_file.file.read.b) - end - end - - context 'when downloading' do - before do - visit download_url - end - - context 'via job id' do - let(:download_url) do - download_project_job_artifacts_path(project, job) - end - - it_behaves_like 'downloading' - end - - context 'via branch name and job name' do - let(:download_url) do - latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) - end - - it_behaves_like 'downloading' - end - end - - context 'when visiting old URL' do - before do - visit download_url.sub('/-/jobs', '/builds') - end - - context 'via job id' do - let(:download_url) do - download_project_job_artifacts_path(project, job) - end - - it_behaves_like 'downloading' - end - - context 'via branch name and job name' do - let(:download_url) do - latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) - end - - it_behaves_like 'downloading' - end - end -end diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb new file mode 100644 index 00000000000..9ebbbaea911 --- /dev/null +++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb @@ -0,0 +1,110 @@ +require "spec_helper" + +describe "User browses artifacts" do + let(:project) { create(:project, :public) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:browse_url) { browse_project_job_artifacts_path(project, job, "other_artifacts_0.1.2") } + + context "when visiting old URL" do + it "redirects to new URL" do + visit(browse_url.sub("/-/jobs", "/builds")) + + expect(page.current_path).to eq(browse_url) + end + end + + context "when browsing artifacts root directory" do + before do + visit(browse_project_job_artifacts_path(project, job)) + end + + it "shows artifacts" do + expect(page).not_to have_selector(".build-sidebar") + + page.within(".tree-table") do + expect(page).to have_no_content("..") + .and have_content("other_artifacts_0.1.2") + .and have_content("ci_artifacts.txt") + .and have_content("rails_sample.jpg") + end + + page.within(".build-header") do + expect(page).to have_content("Job ##{job.id} in pipeline ##{pipeline.id} for #{pipeline.short_sha}") + end + end + + it "shows an artifact" do + click_link("ci_artifacts.txt") + + expect(page).to have_link("download it") + end + end + + context "when browsing a directory with UTF-8 characters in its name" do + before do + visit(browse_project_job_artifacts_path(project, job)) + end + + it "shows correct content", :js do + page.within(".tree-table") do + click_link("tests_encoding") + + expect(page).to have_no_content("non-utf8-dir") + + click_link("utf8 test dir ✓") + + expect(page).to have_content("..").and have_content("regular_file_2") + end + end + end + + context "when browsing a directory with a text file" do + let(:txt_entry) { job.artifacts_metadata_entry("other_artifacts_0.1.2/doc_sample.txt") } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true) + end + + context "when the project is public" do + before do + visit(browse_url) + end + + it "shows correct content" do + link = first(".tree-item-file-external-link") + + expect(link[:target]).to eq("_blank") + expect(link[:rel]).to include("noopener").and include("noreferrer") + expect(page).to have_link("doc_sample.txt", href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path)) + .and have_selector(".js-artifact-tree-external-icon") + + page.within(".tree-table") do + expect(page).to have_content("..").and have_content("another-subdirectory") + end + + page.within(".repo-breadcrumb") do + expect(page).to have_content("other_artifacts_0.1.2") + end + end + end + + context "when the project is private" do + let!(:private_project) { create(:project, :private) } + let(:pipeline) { create(:ci_empty_pipeline, project: private_project) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:user) { create(:user) } + + before do + private_project.add_developer(user) + + sign_in(user) + + visit(browse_project_job_artifacts_path(private_project, job, "other_artifacts_0.1.2")) + end + + it { expect(page).to have_link("doc_sample.txt").and have_no_selector(".js-artifact-tree-external-icon") } + end + end +end diff --git a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb new file mode 100644 index 00000000000..67ed2f18d76 --- /dev/null +++ b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb @@ -0,0 +1,44 @@ +require "spec_helper" + +describe "User downloads artifacts" do + set(:project) { create(:project, :public) } + set(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) } + set(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) } + + shared_examples "downloading" do + it "downloads the zip" do + expect(page.response_headers["Content-Disposition"]).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"}) + expect(page.response_headers['Content-Transfer-Encoding']).to eq("binary") + expect(page.response_headers['Content-Type']).to eq("application/zip") + expect(page.source.b).to eq(job.artifacts_file.file.read.b) + end + end + + context "when downloading" do + before do + visit(url) + end + + context "via job id" do + set(:url) { download_project_job_artifacts_path(project, job) } + + it_behaves_like "downloading" + end + + context "via branch name and job name" do + set(:url) { latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) } + + it_behaves_like "downloading" + end + + context "via clicking the `Download` button" do + set(:url) { project_job_path(project, job) } + + before do + click_link("Download") + end + + it_behaves_like "downloading" + end + end +end diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb index c35ba2d7016..01aeed93947 100644 --- a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb @@ -10,6 +10,15 @@ describe 'User accepts a merge request', :js do sign_in(user) end + it 'presents merged merge request content' do + visit(merge_request_path(merge_request)) + + click_button('Merge') + + expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with \ + #{merge_request.short_merge_commit_sha}") + end + context 'with removing the source branch' do before do visit(merge_request_path(merge_request)) diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 990e5c4d9df..a29c21f6fef 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -3,10 +3,11 @@ require 'spec_helper' describe 'Pipeline', :js do let(:project) { create(:project) } let(:user) { create(:user) } + let(:role) { :developer } before do sign_in(user) - project.add_developer(user) + project.add_role(user, role) end shared_context 'pipeline builds' do @@ -153,9 +154,10 @@ describe 'Pipeline', :js do end context 'page tabs' do - it 'shows Pipeline and Jobs tabs with link' do + it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do expect(page).to have_link('Pipeline') expect(page).to have_link('Jobs') + expect(page).to have_link('Failed Jobs') end it 'shows counter in Jobs tab' do @@ -165,6 +167,16 @@ describe 'Pipeline', :js do it 'shows Pipeline tab as active' do expect(page).to have_css('.js-pipeline-tab-link.active') end + + context 'without permission to access builds' do + let(:project) { create(:project, :public, :repository, public_builds: false) } + let(:role) { :guest } + + it 'does not show failed jobs tab pane' do + expect(page).to have_link('Pipeline') + expect(page).not_to have_content('Failed Jobs') + end + end end context 'retrying jobs' do @@ -308,8 +320,7 @@ describe 'Pipeline', :js do end describe 'GET /:project/pipelines/:id/failures' do - let(:project) { create(:project, :repository) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') } let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) } @@ -340,11 +351,39 @@ describe 'Pipeline', :js do visit pipeline_failures_page end - it 'includes failed jobs' do + it 'shows jobs tab pane as active' do + expect(page).to have_content('Failed Jobs') + expect(page).to have_css('#js-tab-failures.active') + end + + it 'lists failed builds' do + expect(page).to have_content(failed_build.name) + expect(page).to have_content(failed_build.stage) + end + + it 'does not show trace' do expect(page).to have_content('No job trace') end end + context 'without permission to access builds' do + let(:role) { :guest } + + before do + project.update(public_builds: false) + end + + context 'when accessing failed jobs page' do + before do + visit pipeline_failures_page + end + + it 'fails to access the page' do + expect(page).to have_content('Access Denied') + end + end + end + context 'without failures' do before do failed_build.update!(status: :success) diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index a622bf88b13..233102c4314 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -22,6 +22,7 @@ "in_progress_merge_commit_sha": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] }, "merge_commit_sha": { "type": ["string", "null"] }, + "short_merge_commit_sha": { "type": ["string", "null"] }, "merge_params": { "type": ["object", "null"] }, "merge_status": { "type": "string" }, "merge_user_id": { "type": ["integer", "null"] }, @@ -100,6 +101,7 @@ "merge_commit_message_with_description": { "type": "string" }, "diverged_commits_count": { "type": "integer" }, "commit_change_content_path": { "type": "string" }, + "merge_commit_path": { "type": ["string", "null"] }, "remove_wip_path": { "type": ["string", "null"] }, "commits_count": { "type": "integer" }, "remove_source_branch": { "type": ["boolean", "null"] }, diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 2d474e9092f..19278312b6d 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -22,15 +22,20 @@ const defaultValuesComponent = { graphHeightOffset: 120, showFlagContent: true, realPixelRatio: 1, - timeSeries: [{ - values: [{ - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }], - }], + timeSeries: [ + { + values: [ + { + time: new Date('2017-06-04T18:17:33.501Z'), + value: '1.49609375', + }, + ], + }, + ], unitOfDisplay: 'ms', currentDataIndex: 0, legendTitle: 'Average', + currentCoordinates: [], }; const deploymentFlagData = { @@ -113,7 +118,7 @@ describe('GraphFlag', () => { }); it('formatDate', () => { - expect(component.formatDate).toEqual('Sun, Jun 4'); + expect(component.formatDate).toEqual('04 Jun 2017, '); }); it('cursorStyle', () => { diff --git a/spec/javascripts/monitoring/graph/track_line_spec.js b/spec/javascripts/monitoring/graph/track_line_spec.js index 45106830a67..27602a861eb 100644 --- a/spec/javascripts/monitoring/graph/track_line_spec.js +++ b/spec/javascripts/monitoring/graph/track_line_spec.js @@ -39,14 +39,14 @@ describe('TrackLine component', () => { const svgEl = vm.$el.querySelector('svg'); const lineEl = vm.$el.querySelector('svg line'); - expect(svgEl.getAttribute('width')).toEqual('15'); - expect(svgEl.getAttribute('height')).toEqual('6'); + expect(svgEl.getAttribute('width')).toEqual('16'); + expect(svgEl.getAttribute('height')).toEqual('8'); expect(lineEl.getAttribute('stroke-width')).toEqual('4'); expect(lineEl.getAttribute('x1')).toEqual('0'); - expect(lineEl.getAttribute('x2')).toEqual('15'); - expect(lineEl.getAttribute('y1')).toEqual('2'); - expect(lineEl.getAttribute('y2')).toEqual('2'); + expect(lineEl.getAttribute('x2')).toEqual('16'); + expect(lineEl.getAttribute('y1')).toEqual('4'); + expect(lineEl.getAttribute('y2')).toEqual('4'); }); }); }); diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js index c83bd19345f..2515e2ad897 100644 --- a/spec/javascripts/monitoring/graph_path_spec.js +++ b/spec/javascripts/monitoring/graph_path_spec.js @@ -23,6 +23,7 @@ describe('Monitoring Paths', () => { generatedAreaPath: firstTimeSeries.areaPath, lineColor: firstTimeSeries.lineColor, areaColor: firstTimeSeries.areaColor, + showDot: false, }); const metricArea = component.$el.querySelector('.metric-area'); const metricLine = component.$el.querySelector('.metric-line'); @@ -40,6 +41,7 @@ describe('Monitoring Paths', () => { generatedAreaPath: firstTimeSeries.areaPath, lineColor: firstTimeSeries.lineColor, areaColor: firstTimeSeries.areaColor, + showDot: false, }); component.lineStyle = 'dashed'; diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index 1213c80ba3a..220228e5c08 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -30,7 +30,6 @@ describe('Graph', () => { it('has a title', () => { const component = createComponent({ graphData: convertedMetrics[1], - classType: 'col-md-6', updateAspectRatio: false, deploymentData, tagsPath, @@ -46,7 +45,6 @@ describe('Graph', () => { it('axisTransform translates an element Y position depending of its height', () => { const component = createComponent({ graphData: convertedMetrics[1], - classType: 'col-md-6', updateAspectRatio: false, deploymentData, tagsPath, @@ -62,7 +60,6 @@ describe('Graph', () => { it('outerViewBox gets a width and height property based on the DOM size of the element', () => { const component = createComponent({ graphData: convertedMetrics[1], - classType: 'col-md-6', updateAspectRatio: false, deploymentData, tagsPath, @@ -79,7 +76,6 @@ describe('Graph', () => { it('sends an event to the eventhub when it has finished resizing', done => { const component = createComponent({ graphData: convertedMetrics[1], - classType: 'col-md-6', updateAspectRatio: false, deploymentData, tagsPath, @@ -97,7 +93,6 @@ describe('Graph', () => { it('has a title for the y-axis and the chart legend that comes from the backend', () => { const component = createComponent({ graphData: convertedMetrics[1], - classType: 'col-md-6', updateAspectRatio: false, deploymentData, tagsPath, @@ -111,7 +106,6 @@ describe('Graph', () => { it('sets the currentData object based on the hovered data index', () => { const component = createComponent({ graphData: convertedMetrics[1], - classType: 'col-md-6', updateAspectRatio: false, deploymentData, graphIdentifier: 0, @@ -125,6 +119,5 @@ describe('Graph', () => { component.positionFlag(); expect(component.currentData).toBe(component.timeSeries[0].values[10]); - expect(component.currentDataIndex).toEqual(10); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index c2c92d8ac56..adeea03481f 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -6,6 +6,14 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetMerged', () => { let vm; const targetBranch = 'foo'; + const selectors = { + get copyMergeShaButton() { + return vm.$el.querySelector('button.js-mr-merged-copy-sha'); + }, + get mergeCommitShaLink() { + return vm.$el.querySelector('a.js-mr-merged-commit-sha'); + }, + }; beforeEach(() => { const Component = Vue.extend(mergedComponent); @@ -31,6 +39,9 @@ describe('MRWidgetMerged', () => { readableClosedAt: '', }, updatedAt: 'mergedUpdatedAt', + shortMergeCommitSha: 'asdf1234', + mergeCommitPath: 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d', + sourceBranch: 'bar', targetBranch, }; @@ -140,6 +151,17 @@ describe('MRWidgetMerged', () => { expect(vm.$el.textContent).toContain('Cherry-pick'); }); + it('shows button to copy commit SHA to clipboard', () => { + expect(selectors.copyMergeShaButton).toExist(); + expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.shortMergeCommitSha); + }); + + it('shows merge commit SHA link', () => { + expect(selectors.mergeCommitShaLink).toExist(); + expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha); + expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath); + }); + it('should not show source branch removed text', (done) => { vm.mr.sourceBranchRemoved = false; diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 3fc7663b9c2..9d2a15ff009 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -18,6 +18,7 @@ export default { human_total_time_spent: null, in_progress_merge_commit_sha: null, merge_commit_sha: '53027d060246c8f47e4a9310fb332aa52f221775', + short_merge_commit_sha: '53027d06', merge_error: null, merge_params: { force_remove_source_branch: null, @@ -215,4 +216,5 @@ export default { diverged_commits_count: 0, only_allow_merge_if_pipeline_succeeds: false, commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', + merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', }; diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index a5d505af001..4570dbb1d8e 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -29,12 +29,6 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end end - let(:now) { Time.now.utc } - - around do |example| - Timecop.freeze(now) { example.run } - end - let(:calculation) { -> { 2 + 2 } } let(:cache_key) { "foo:666" } let(:instance) { CacheTest.new(666, &calculation) } @@ -49,13 +43,15 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do context 'when cache is empty' do it { is_expected.to be_nil } - it 'queues a background worker' do + it 'enqueues a background worker to bootstrap the cache' do expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666) go! end it 'updates the cache lifespan' do + expect(reactive_cache_alive?(instance)).to be_falsy + go! expect(reactive_cache_alive?(instance)).to be_truthy @@ -69,6 +65,18 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do it { is_expected.to eq(2) } + it 'does not enqueue a background worker' do + expect(ReactiveCachingWorker).not_to receive(:perform_async) + + go! + end + + it 'updates the cache lifespan' do + expect(Rails.cache).to receive(:write).with(alive_reactive_cache_key(instance), true, expires_in: anything) + + go! + end + context 'and expired' do before do invalidate_reactive_cache(instance) diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb index 21893e0cbaa..592feddf1dc 100644 --- a/spec/models/concerns/sha_attribute_spec.rb +++ b/spec/models/concerns/sha_attribute_spec.rb @@ -13,33 +13,74 @@ describe ShaAttribute do end describe '#sha_attribute' do - context 'when the table exists' do + context 'when in non-production' do before do - allow(model).to receive(:table_exists?).and_return(true) + allow(Rails.env).to receive(:production?).and_return(false) end - it 'defines a SHA attribute for a binary column' do - expect(model).to receive(:attribute) - .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute)) + context 'when the table exists' do + before do + allow(model).to receive(:table_exists?).and_return(true) + end - model.sha_attribute(:sha1) + it 'defines a SHA attribute for a binary column' do + expect(model).to receive(:attribute) + .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute)) + + model.sha_attribute(:sha1) + end + + it 'raises ArgumentError when the column type is not :binary' do + expect { model.sha_attribute(:name) }.to raise_error(ArgumentError) + end + end + + context 'when the table does not exist' do + it 'allows the attribute to be added' do + allow(model).to receive(:table_exists?).and_return(false) + + expect(model).not_to receive(:columns) + expect(model).to receive(:attribute) + + model.sha_attribute(:name) + end end - it 'raises ArgumentError when the column type is not :binary' do - expect { model.sha_attribute(:name) }.to raise_error(ArgumentError) + context 'when the column does not exist' do + it 'raises ArgumentError' do + allow(model).to receive(:table_exists?).and_return(true) + + expect(model).to receive(:columns) + expect(model).not_to receive(:attribute) + + expect { model.sha_attribute(:no_name) }.to raise_error(ArgumentError) + end + end + + context 'when other execeptions are raised' do + it 'logs and re-rasises the error' do + allow(model).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError.new('does not exist')) + + expect(model).not_to receive(:columns) + expect(model).not_to receive(:attribute) + expect(Gitlab::AppLogger).to receive(:error) + + expect { model.sha_attribute(:name) }.to raise_error(ActiveRecord::NoDatabaseError) + end end end - context 'when the table does not exist' do + context 'when in production' do before do - allow(model).to receive(:table_exists?).and_return(false) + allow(Rails.env).to receive(:production?).and_return(true) end - it 'does nothing' do + it 'defines a SHA attribute' do + expect(model).not_to receive(:table_exists?) expect(model).not_to receive(:columns) - expect(model).not_to receive(:attribute) + expect(model).to receive(:attribute).with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute)) - model.sha_attribute(:name) + model.sha_attribute(:sha1) end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 5a9aa7c7d1b..04379e7d2c3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1069,6 +1069,22 @@ describe MergeRequest do end end + describe '#short_merge_commit_sha' do + let(:merge_request) { build_stubbed(:merge_request) } + + it 'returns short id when there is a merge_commit_sha' do + merge_request.merge_commit_sha = 'f7ce827c314c9340b075657fd61c789fb01cf74d' + + expect(merge_request.short_merge_commit_sha).to eq('f7ce827c') + end + + it 'returns nil when there is no merge_commit_sha' do + merge_request.merge_commit_sha = nil + + expect(merge_request.short_merge_commit_sha).to be_nil + end + end + describe '#can_be_reverted?' do context 'when there is no merge_commit for the MR' do before do diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 3b77055b644..020031af3cb 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -88,6 +88,14 @@ codequality: artifacts: paths: [codeclimate.json] +license_management: + image: registry.gitlab.com/gitlab-org/security-products/license-management:latest + allow_failure: true + script: + - license_management + artifacts: + paths: [gl-license-report.json] + performance: stage: performance image: docker:stable @@ -133,6 +141,7 @@ dependency_scanning: - dependency_scanning artifacts: paths: [gl-dependency-scanning-report.json] + sast:container: image: docker:stable variables: @@ -217,7 +226,7 @@ stop_review: # only manually promote to production, enable this job by removing the dot (.), # and uncomment the `when: manual` line in the `production` job. -.staging: +staging: stage: staging script: - check_kube_domain @@ -234,6 +243,11 @@ stop_review: refs: - master kubernetes: active + variables: + - $STAGING_ENABLED + except: + variables: + - $INCREMENTAL_ROLLOUT_ENABLED # Canaries are disabled by default, but if you want them, # and know what the downsides are, enable this job by removing the dot (.), @@ -263,7 +277,7 @@ stop_review: # or `canary` deploys, or you simply want more control over when you deploy # to production, uncomment the `when: manual` line in the `production` job. -production: +.production: &production_template stage: production script: - check_kube_domain @@ -274,17 +288,103 @@ production: - create_secret - deploy - delete canary + - delete rollout - persist_environment_url environment: name: production url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN artifacts: paths: [environment_url.txt] -# when: manual + +production: + <<: *production_template only: refs: - master kubernetes: active + except: + variables: + - $STAGING_ENABLED + - $INCREMENTAL_ROLLOUT_ENABLED + +production_manual: + <<: *production_template + when: manual + only: + refs: + - master + kubernetes: active + variables: + - $STAGING_ENABLED + except: + variables: + - $INCREMENTAL_ROLLOUT_ENABLED + +# This job implements incremental rollout on for every push to `master`. + +.rollout: &rollout_template + stage: production + script: + - check_kube_domain + - install_dependencies + - download_chart + - ensure_namespace + - install_tiller + - create_secret + - deploy rollout $ROLLOUT_PERCENTAGE + - scale stable $((100-ROLLOUT_PERCENTAGE)) + - delete canary + - persist_environment_url + environment: + name: production + url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN + artifacts: + paths: [environment_url.txt] + +rollout 10%: + <<: *rollout_template + variables: + ROLLOUT_PERCENTAGE: 10 + only: + refs: + - master + kubernetes: active + variables: + - $INCREMENTAL_ROLLOUT_ENABLED + +rollout 25%: + <<: *rollout_template + variables: + ROLLOUT_PERCENTAGE: 25 + when: manual + only: + refs: + - master + kubernetes: active + variables: + - $INCREMENTAL_ROLLOUT_ENABLED + +rollout 50%: + <<: *rollout_template + variables: + ROLLOUT_PERCENTAGE: 50 + when: manual + only: + refs: + - master + kubernetes: active + variables: + - $INCREMENTAL_ROLLOUT_ENABLED + +rollout 100%: + <<: *production_template + when: manual + only: + refs: + - master + kubernetes: active + variables: + - $INCREMENTAL_ROLLOUT_ENABLED # --------------------------------------------------------------------------- @@ -308,7 +408,7 @@ production: fi docker run -d --name db arminc/clair-db:latest - docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1 + docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.1 apk add -U wget ca-certificates docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 @@ -328,6 +428,14 @@ production: "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code } + function license_management() { + if echo $GITLAB_FEATURES |grep license_management > /dev/null ; then + /run.sh . + else + echo "License management is not available in your subscription" + fi + } + function sast() { case "$CI_SERVER_VERSION" in *-ee) @@ -363,30 +471,19 @@ production: esac } - function deploy() { - track="${1-stable}" - name="$CI_ENVIRONMENT_SLUG" - - if [[ "$track" != "stable" ]]; then - name="$name-$track" - fi - - replicas="1" - service_enabled="false" - postgres_enabled="$POSTGRES_ENABLED" - # canary uses stable db - [[ "$track" == "canary" ]] && postgres_enabled="false" + function get_replicas() { + track="${1:-stable}" + percentage="${2:-100}" env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' ) env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' ) - if [[ "$track" == "stable" ]]; then + if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then # for stable track get number of replicas from `PRODUCTION_REPLICAS` eval new_replicas=\$${env_slug}_REPLICAS if [[ -z "$new_replicas" ]]; then new_replicas=$REPLICAS fi - service_enabled="true" else # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS` eval new_replicas=\$${env_track}_${env_slug}_REPLICAS @@ -394,10 +491,37 @@ production: eval new_replicas=\${env_track}_REPLICAS fi fi - if [[ -n "$new_replicas" ]]; then - replicas="$new_replicas" + + replicas="${new_replicas:-1}" + replicas="$(($replicas * $percentage / 100))" + + # always return at least one replicas + if [[ $replicas -gt 0 ]]; then + echo "$replicas" + else + echo 1 + fi + } + + function deploy() { + track="${1-stable}" + percentage="${2:-100}" + name="$CI_ENVIRONMENT_SLUG" + + replicas="1" + service_enabled="true" + postgres_enabled="$POSTGRES_ENABLED" + + # if track is different than stable, + # re-use all attached resources + if [[ "$track" != "stable" ]]; then + name="$name-$track" + service_enabled="false" + postgres_enabled="false" fi + replicas=$(get_replicas "$track" "$percentage") + if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then secret_name='gitlab-registry' else @@ -427,6 +551,25 @@ production: chart/ } + function scale() { + track="${1-stable}" + percentage="${2-100}" + name="$CI_ENVIRONMENT_SLUG" + + if [[ "$track" != "stable" ]]; then + name="$name-$track" + fi + + replicas=$(get_replicas "$track" "$percentage") + + helm upgrade --reuse-values \ + --wait \ + --set replicaCount="$replicas" \ + --namespace="$KUBE_NAMESPACE" \ + "$name" \ + chart/ + } + function install_dependencies() { apk add -U openssl curl tar gzip bash ca-certificates git wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub @@ -548,8 +691,8 @@ production: kubectl create secret -n "$KUBE_NAMESPACE" \ docker-registry gitlab-registry \ --docker-server="$CI_REGISTRY" \ - --docker-username="$CI_REGISTRY_USER" \ - --docker-password="$CI_REGISTRY_PASSWORD" \ + --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \ + --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \ --docker-email="$GITLAB_USER_EMAIL" \ -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f - } |