diff --git a/CHANGELOG.md b/CHANGELOG.md index ea9b789ce05..d077ef86d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 42.1.1 (2021-02-12) + +### Security (2 changes) + +- Testing main branch. +- Testing main branch. + + ## 13.8.4 (2021-02-11) ### Security (9 changes) diff --git a/app/assets/images/auth_buttons/openid_64.png b/app/assets/images/auth_buttons/openid_64.png new file mode 100644 index 00000000000..7b53d129f95 Binary files /dev/null and b/app/assets/images/auth_buttons/openid_64.png differ diff --git a/app/assets/javascripts/pages/projects/tags/index/index.js b/app/assets/javascripts/pages/projects/tags/index/index.js index 96e52850936..98560c1193b 100644 --- a/app/assets/javascripts/pages/projects/tags/index/index.js +++ b/app/assets/javascripts/pages/projects/tags/index/index.js @@ -1,9 +1,7 @@ import { initRemoveTag } from '../remove_tag'; -document.addEventListener('DOMContentLoaded', () => { - initRemoveTag({ - onDelete: (path) => { - document.querySelector(`[data-path="${path}"]`).closest('.js-tag-list').remove(); - }, - }); +initRemoveTag({ + onDelete: (path) => { + document.querySelector(`[data-path="${path}"]`).closest('.js-tag-list').remove(); + }, }); diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue new file mode 100644 index 00000000000..e7cba312705 --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue @@ -0,0 +1,100 @@ + + diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue similarity index 94% rename from app/assets/javascripts/pipeline_editor/components/text_editor.vue rename to app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue index 4e9ba47727d..51bfb90e78d 100644 --- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue +++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue @@ -2,7 +2,7 @@ import EditorLite from '~/vue_shared/components/editor_lite.vue'; import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext'; import { EDITOR_READY_EVENT } from '~/editor/constants'; -import getCommitSha from '../graphql/queries/client/commit_sha.graphql'; +import getCommitSha from '../../graphql/queries/client/commit_sha.graphql'; export default { components: { diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue index 760a8b7e232..63173990710 100644 --- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue +++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue @@ -1,21 +1,41 @@ diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js index f15558363bf..e676fdeae02 100644 --- a/app/assets/javascripts/pipeline_editor/constants.js +++ b/app/assets/javascripts/pipeline_editor/constants.js @@ -7,3 +7,10 @@ export const COMMIT_SUCCESS = 'COMMIT_SUCCESS'; export const DEFAULT_FAILURE = 'DEFAULT_FAILURE'; export const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE'; export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; + +export const CREATE_TAB = 'CREATE_TAB'; +export const LINT_TAB = 'LINT_TAB'; +export const MERGED_TAB = 'MERGED_TAB'; +export const VISUALIZE_TAB = 'VISUALIZE_TAB'; + +export const TABS_WITH_COMMIT_FORM = [CREATE_TAB, LINT_TAB, VISUALIZE_TAB]; diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql index dfddb29701d..30c18a96536 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql @@ -3,6 +3,7 @@ query getCiConfigData($projectPath: ID!, $content: String!) { ciConfig(projectPath: $projectPath, content: $content) { errors + mergedYaml status stages { ...PipelineStagesConnection diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue index b7535cc0964..dc61bfa8993 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue @@ -2,6 +2,7 @@ import CommitSection from './components/commit/commit_section.vue'; import PipelineEditorTabs from './components/pipeline_editor_tabs.vue'; import PipelineEditorHeader from './components/header/pipeline_editor_header.vue'; +import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants'; export default { components: { @@ -23,6 +24,21 @@ export default { required: true, }, }, + data() { + return { + currentTab: CREATE_TAB, + }; + }, + computed: { + showCommitForm() { + return TABS_WITH_COMMIT_FORM.includes(this.currentTab); + }, + }, + methods: { + setCurrentTab(tabName) { + this.currentTab = tabName; + }, + }, }; @@ -37,7 +53,8 @@ export default { :ci-file-content="ciFileContent" :is-ci-config-data-loading="isCiConfigDataLoading" v-on="$listeners" + @set-current-tab="setCurrentTab" /> - + diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 6d46decf978..52ff4e7b100 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -123,7 +123,7 @@ Sidebar.prototype.todoUpdateDone = function (data) { .data('deletePath', deletePath); if ($el.hasClass('has-tooltip')) { - fixTitle($el); + fixTitle(el); } if (typeof $el.data('isCollapsed') !== 'undefined') { diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js index b216affc818..a4f74a63d81 100644 --- a/app/assets/javascripts/tooltips/index.js +++ b/app/assets/javascripts/tooltips/index.js @@ -1,6 +1,5 @@ import Vue from 'vue'; -import jQuery from 'jquery'; -import { toArray, isFunction, isElement } from 'lodash'; +import { toArray, isElement } from 'lodash'; import Tooltips from './components/tooltips.vue'; let app; @@ -60,72 +59,39 @@ const applyToElements = (elements, handler) => { toArray(iterable).forEach(handler); }; -const invokeBootstrapApi = (elements, method) => { - if (isFunction(elements.tooltip)) { - elements.tooltip(method); - } else { - jQuery(elements).tooltip(method); - } -}; - -const isGlTooltipsEnabled = () => Boolean(window.gon.features?.glTooltips); - -const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => { - if (isGlTooltipsEnabled()) { - applyToElements(elements, glHandler); - } else { - bsHandler(elements, ...params); - } +const createTooltipApiInvoker = (glHandler) => (elements) => { + applyToElements(elements, glHandler); }; export const initTooltips = (config = {}) => { - if (isGlTooltipsEnabled()) { - const triggers = config?.triggers || DEFAULT_TRIGGER; - const events = triggers.split(' ').map((trigger) => EVENTS_MAP[trigger]); + const triggers = config?.triggers || DEFAULT_TRIGGER; + const events = triggers.split(' ').map((trigger) => EVENTS_MAP[trigger]); - events.forEach((event) => { - document.addEventListener( - event, - (e) => handleTooltipEvent(document, e, config.selector, config), - true, - ); - }); + events.forEach((event) => { + document.addEventListener( + event, + (e) => handleTooltipEvent(document, e, config.selector, config), + true, + ); + }); - return tooltipsApp(); - } - - return invokeBootstrapApi(document.body, config); + return tooltipsApp(); }; -export const add = (elements, config = {}) => { - if (isGlTooltipsEnabled()) { - return addTooltips(elements, config); - } - return invokeBootstrapApi(elements, config); -}; -export const dispose = tooltipApiInvoker({ - glHandler: (element) => tooltipsApp().dispose(element), - bsHandler: (elements) => invokeBootstrapApi(elements, 'dispose'), -}); -export const fixTitle = tooltipApiInvoker({ - glHandler: (element) => tooltipsApp().fixTitle(element), - bsHandler: (elements) => invokeBootstrapApi(elements, '_fixTitle'), -}); -export const enable = tooltipApiInvoker({ - glHandler: (element) => tooltipsApp().triggerEvent(element, 'enable'), - bsHandler: (elements) => invokeBootstrapApi(elements, 'enable'), -}); -export const disable = tooltipApiInvoker({ - glHandler: (element) => tooltipsApp().triggerEvent(element, 'disable'), - bsHandler: (elements) => invokeBootstrapApi(elements, 'disable'), -}); -export const hide = tooltipApiInvoker({ - glHandler: (element) => tooltipsApp().triggerEvent(element, 'close'), - bsHandler: (elements) => invokeBootstrapApi(elements, 'hide'), -}); -export const show = tooltipApiInvoker({ - glHandler: (element) => tooltipsApp().triggerEvent(element, 'open'), - bsHandler: (elements) => invokeBootstrapApi(elements, 'show'), -}); +export const add = (elements, config = {}) => addTooltips(elements, config); +export const dispose = createTooltipApiInvoker((element) => tooltipsApp().dispose(element)); +export const fixTitle = createTooltipApiInvoker((element) => tooltipsApp().fixTitle(element)); +export const enable = createTooltipApiInvoker((element) => + tooltipsApp().triggerEvent(element, 'enable'), +); +export const disable = createTooltipApiInvoker((element) => + tooltipsApp().triggerEvent(element, 'disable'), +); +export const hide = createTooltipApiInvoker((element) => + tooltipsApp().triggerEvent(element, 'close'), +); +export const show = createTooltipApiInvoker((element) => + tooltipsApp().triggerEvent(element, 'open'), +); export const destroy = () => { tooltipsApp().$destroy(); app = null; diff --git a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb index d05ab1b4977..aabcb74cefa 100644 --- a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb +++ b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb @@ -40,7 +40,25 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati end def report_results - Ci::DailyBuildGroupReportResultsFinder.new(**finder_params).execute + if ::Gitlab::Ci::Features.use_coverage_data_new_finder?(project) + ::Ci::Testing::DailyBuildGroupReportResultsFinder.new( + params: new_finder_params, + current_user: current_user + ).execute + else + Ci::DailyBuildGroupReportResultsFinder.new(**finder_params).execute + end + end + + def new_finder_params + { + project: project, + coverage: true, + start_date: start_date, + end_date: end_date, + ref_path: params[:ref_path], + sort: true + } end def finder_params diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb index ef9025ae52f..3552915b561 100644 --- a/app/controllers/projects/ci/pipeline_editor_controller.rb +++ b/app/controllers/projects/ci/pipeline_editor_controller.rb @@ -4,6 +4,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController before_action :check_can_collaborate! before_action do push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml) + push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml) end feature_category :pipeline_authoring diff --git a/app/finders/ci/testing/daily_build_group_report_results_finder.rb b/app/finders/ci/testing/daily_build_group_report_results_finder.rb new file mode 100644 index 00000000000..e12b42c24d2 --- /dev/null +++ b/app/finders/ci/testing/daily_build_group_report_results_finder.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +# DailyBuildGroupReportResultsFinder +# +# Used to filter DailyBuildGroupReportResults by set of params +# +# Arguments: +# current_user +# params: +# project: integer +# group: integer +# coverage: boolean +# ref_path: string +# start_date: date +# end_date: date +# sort: boolean +# limit: integer + +module Ci + module Testing + class DailyBuildGroupReportResultsFinder + include Gitlab::Allowable + + MAX_ITEMS = 1_000 + + attr_reader :params, :current_user + + def initialize(params: {}, current_user: nil) + @params = params + @current_user = current_user + end + + def execute + return Ci::DailyBuildGroupReportResult.none unless query_allowed? + + collection = Ci::DailyBuildGroupReportResult.by_projects(params[:project]) + collection = filter_report_results(collection) + collection + end + + private + + def query_allowed? + can?(current_user, :read_build_report_results, params[:project]) + end + + def filter_report_results(collection) + collection = by_coverage(collection) + collection = by_ref_path(collection) + collection = by_dates(collection) + + collection = sort(collection) + collection = limit_by(collection) + collection + end + + def by_coverage(items) + params[:coverage].present? ? items.with_coverage : items + end + + def by_ref_path(items) + params[:ref_path].present? ? items.by_ref_path(params[:ref_path]) : items.with_default_branch + end + + def by_dates(items) + params[:start_date].present? && params[:end_date].present? ? items.by_dates(params[:start_date], params[:end_date]) : items + end + + def sort(items) + params[:sort].present? ? items.ordered_by_date_and_group_name : items + end + + # rubocop: disable CodeReuse/ActiveRecord + def limit_by(items) + items.limit(limit) + end + # rubocop: enable CodeReuse/ActiveRecord + + def limit + return MAX_ITEMS unless params[:limit].present? + + [params[:limit].to_i, MAX_ITEMS].min + end + end + end +end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 0b79d4c36a1..24c1d224c89 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce atlassian_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce atlassian_oauth2 openid_connect).freeze LDAP_PROVIDER = /\Aldap/.freeze def ldap_enabled? diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb index e9f3366b939..5a1862194f3 100644 --- a/app/models/ci/daily_build_group_report_result.rb +++ b/app/models/ci/daily_build_group_report_result.rb @@ -13,10 +13,13 @@ module Ci validates :data, json_schema: { filename: "daily_build_group_report_result_data" } scope :with_included_projects, -> { includes(:project) } + scope :by_ref_path, -> (ref_path) { where(ref_path: ref_path) } scope :by_projects, -> (ids) { where(project_id: ids) } scope :with_coverage, -> { where("(data->'coverage') IS NOT NULL") } scope :with_default_branch, -> { where(default_branch: true) } scope :by_date, -> (start_date) { where(date: report_window(start_date)..Date.current) } + scope :by_dates, -> (start_date, end_date) { where(date: start_date..end_date) } + scope :ordered_by_date_and_group_name, -> { order(date: :desc, group_name: :asc) } store_accessor :data, :coverage diff --git a/app/models/user_status.rb b/app/models/user_status.rb index 0e1ae0b7338..1c8634e47c3 100644 --- a/app/models/user_status.rb +++ b/app/models/user_status.rb @@ -7,6 +7,16 @@ class UserStatus < ApplicationRecord DEFAULT_EMOJI = 'speech_balloon' + CLEAR_STATUS_QUICK_OPTIONS = { + '30_minutes' => 30.minutes, + '3_hours' => 3.hours, + '8_hours' => 8.hours, + '1_day' => 1.day, + '3_days' => 3.days, + '7_days' => 7.days, + '30_days' => 30.days + }.freeze + belongs_to :user enum availability: { not_set: 0, busy: 1 } @@ -15,5 +25,11 @@ class UserStatus < ApplicationRecord validates :emoji, inclusion: { in: Gitlab::Emoji.emojis_names } validates :message, length: { maximum: 100 }, allow_blank: true + scope :scheduled_for_cleanup, -> { where(arel_table[:clear_status_at].lteq(Time.current)) } + cache_markdown_field :message, pipeline: :emoji + + def clear_status_after=(value) + self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now + end end diff --git a/app/models/wiki.rb b/app/models/wiki.rb index ab53515ec48..45747c0b03c 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -104,7 +104,7 @@ class Wiki end def empty? - list_pages(limit: 1).empty? + !repository_exists? || list_pages(limit: 1).empty? end def exists? diff --git a/app/services/users/batch_status_cleaner_service.rb b/app/services/users/batch_status_cleaner_service.rb new file mode 100644 index 00000000000..ea6142f13cc --- /dev/null +++ b/app/services/users/batch_status_cleaner_service.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Users + class BatchStatusCleanerService + BATCH_SIZE = 100.freeze + + # Cleanup BATCH_SIZE user_statuses records + # rubocop: disable CodeReuse/ActiveRecord + def self.execute(batch_size: BATCH_SIZE) + scope = UserStatus + .select(:user_id) + .scheduled_for_cleanup + .lock('FOR UPDATE SKIP LOCKED') + .limit(batch_size) + + deleted_rows = UserStatus.where(user_id: scope).delete_all + + { deleted_rows: deleted_rows } + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 8ef7ce53c46..d8fac947a1f 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -52,8 +52,8 @@ %span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)") - if show_status_emoji?(@user.status) - .cover-status - = emoji_icon(@user.status.emoji) + .cover-status.gl-display-inline-flex.gl-align-items-center + = emoji_icon(@user.status.emoji, class: 'gl-mr-2') = markdown_field(@user.status, :message) = render "users/profile_basic_info" .cover-desc.cgray.mb-1.mb-sm-2 diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 485d8eee000..6d4e6de30d9 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -467,6 +467,14 @@ :weight: 1 :idempotent: true :tags: [] +- :name: cronjob:user_status_cleanup_batch + :feature_category: :users + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:users_create_statistics :feature_category: :users :has_external_dependencies: diff --git a/app/workers/user_status_cleanup/batch_worker.rb b/app/workers/user_status_cleanup/batch_worker.rb new file mode 100644 index 00000000000..0c1087cc4d2 --- /dev/null +++ b/app/workers/user_status_cleanup/batch_worker.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module UserStatusCleanup + # This worker will run every minute to look for user status records to clean up. + class BatchWorker + include ApplicationWorker + # rubocop:disable Scalability/CronWorkerContext + include CronjobQueue + # rubocop:enable Scalability/CronWorkerContext + + feature_category :users + + idempotent! + + # Avoid running too many UPDATE queries at once + MAX_RUNTIME = 30.seconds + + def perform + return unless UserStatus.scheduled_for_cleanup.exists? + + start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + loop do + result = Users::BatchStatusCleanerService.execute + break if result[:deleted_rows] < Users::BatchStatusCleanerService::BATCH_SIZE + + current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + break if (current_time - start_time) > MAX_RUNTIME + end + end + end +end diff --git a/changelogs/unreleased/262086-schedule-user-unset-status.yml b/changelogs/unreleased/262086-schedule-user-unset-status.yml new file mode 100644 index 00000000000..4617f283de5 --- /dev/null +++ b/changelogs/unreleased/262086-schedule-user-unset-status.yml @@ -0,0 +1,5 @@ +--- +title: Add clear_status_at column to user_status table +merge_request: 53620 +author: +type: other diff --git a/changelogs/unreleased/jl-improve-profile-status-emoji-alignment.yml b/changelogs/unreleased/jl-improve-profile-status-emoji-alignment.yml new file mode 100644 index 00000000000..e2b7e592465 --- /dev/null +++ b/changelogs/unreleased/jl-improve-profile-status-emoji-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Improve profile status emoji alignment +merge_request: 54078 +author: +type: other diff --git a/changelogs/unreleased/remove-feature_flag_contextual_issue.yml b/changelogs/unreleased/remove-feature_flag_contextual_issue.yml new file mode 100644 index 00000000000..e99cdaf7a11 --- /dev/null +++ b/changelogs/unreleased/remove-feature_flag_contextual_issue.yml @@ -0,0 +1,5 @@ +--- +title: Support Markdown for Feature Flags +merge_request: 53816 +author: +type: added diff --git a/changelogs/unreleased/sh-add-openid-sso-icon.yml b/changelogs/unreleased/sh-add-openid-sso-icon.yml new file mode 100644 index 00000000000..7993ded80fa --- /dev/null +++ b/changelogs/unreleased/sh-add-openid-sso-icon.yml @@ -0,0 +1,5 @@ +--- +title: Add OpenID SSO icon +merge_request: 54026 +author: +type: changed diff --git a/config/feature_flags/development/ci_config_merged_tab.yml b/config/feature_flags/development/ci_config_merged_tab.yml new file mode 100644 index 00000000000..5cee3429af8 --- /dev/null +++ b/config/feature_flags/development/ci_config_merged_tab.yml @@ -0,0 +1,8 @@ +--- +name: ci_config_merged_tab +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53299 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/301103 +milestone: '13.9' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/clear_status_with_quick_options.yml b/config/feature_flags/development/clear_status_with_quick_options.yml new file mode 100644 index 00000000000..b4ce306fae0 --- /dev/null +++ b/config/feature_flags/development/clear_status_with_quick_options.yml @@ -0,0 +1,8 @@ +--- +name: clear_status_with_quick_options +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53620 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/320777 +milestone: '13.9' +type: development +group: group::optimize +default_enabled: false diff --git a/config/feature_flags/development/feature_flag_contextual_issue.yml b/config/feature_flags/development/coverage_data_new_finder.yml similarity index 70% rename from config/feature_flags/development/feature_flag_contextual_issue.yml rename to config/feature_flags/development/coverage_data_new_finder.yml index 1889a6c871c..a7c283ce3db 100644 --- a/config/feature_flags/development/feature_flag_contextual_issue.yml +++ b/config/feature_flags/development/coverage_data_new_finder.yml @@ -1,8 +1,8 @@ --- -name: feature_flag_contextual_issue -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53021 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/320741 +name: coverage_data_new_finder +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53670 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/301093 milestone: '13.9' type: development -group: group::release +group: group::testing default_enabled: false diff --git a/config/feature_flags/development/export_reduce_relation_batch_size.yml b/config/feature_flags/development/export_reduce_relation_batch_size.yml index 70b19dfe594..63164b6e9fe 100644 --- a/config/feature_flags/development/export_reduce_relation_batch_size.yml +++ b/config/feature_flags/development/export_reduce_relation_batch_size.yml @@ -1,8 +1,8 @@ --- name: export_reduce_relation_batch_size -introduced_by_url: -rollout_issue_url: -milestone: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34057 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282245 +milestone: '13.1' type: development group: group::import default_enabled: false diff --git a/config/feature_flags/development/gl_tooltips.yml b/config/feature_flags/development/gl_tooltips.yml deleted file mode 100644 index 22c67019c33..00000000000 --- a/config/feature_flags/development/gl_tooltips.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: gl_tooltips -introduced_by_url: -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292972 -milestone: '13.8' -type: development -group: group::editor -default_enabled: false diff --git a/config/feature_flags/development/group_import_export.yml b/config/feature_flags/development/group_import_export.yml index 59204f2d16e..0eb01340bef 100644 --- a/config/feature_flags/development/group_import_export.yml +++ b/config/feature_flags/development/group_import_export.yml @@ -1,8 +1,8 @@ --- name: group_import_export -introduced_by_url: -rollout_issue_url: -milestone: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22423 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282245 +milestone: '12.8' type: development group: group::import default_enabled: true diff --git a/config/feature_flags/development/log_import_export_relation_creation.yml b/config/feature_flags/development/log_import_export_relation_creation.yml index ca7223c52b0..04d1b1e5d4f 100644 --- a/config/feature_flags/development/log_import_export_relation_creation.yml +++ b/config/feature_flags/development/log_import_export_relation_creation.yml @@ -1,8 +1,8 @@ --- name: log_import_export_relation_creation -introduced_by_url: -rollout_issue_url: -milestone: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27605 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282245 +milestone: '12.10' type: development group: group::import default_enabled: false diff --git a/config/feature_flags/development/project_list_filter_bar.yml b/config/feature_flags/development/project_list_filter_bar.yml index 7f8ea867990..29d5d67af95 100644 --- a/config/feature_flags/development/project_list_filter_bar.yml +++ b/config/feature_flags/development/project_list_filter_bar.yml @@ -1,8 +1,8 @@ --- name: project_list_filter_bar -introduced_by_url: -rollout_issue_url: -milestone: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/11209 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321404 +milestone: '11.11' type: development -group: +group: group::access default_enabled: false diff --git a/config/feature_flags/development/user_time_settings.yml b/config/feature_flags/development/user_time_settings.yml index eaeb7f17794..098b96e97f0 100644 --- a/config/feature_flags/development/user_time_settings.yml +++ b/config/feature_flags/development/user_time_settings.yml @@ -1,8 +1,8 @@ --- name: user_time_settings -introduced_by_url: -rollout_issue_url: -milestone: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25381 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321407 +milestone: '11.11' type: development -group: +group: group::access default_enabled: false diff --git a/config/feature_flags/development/validate_import_decompressed_archive_size.yml b/config/feature_flags/development/validate_import_decompressed_archive_size.yml index 644a936a67c..675586bc9ee 100644 --- a/config/feature_flags/development/validate_import_decompressed_archive_size.yml +++ b/config/feature_flags/development/validate_import_decompressed_archive_size.yml @@ -1,8 +1,8 @@ --- name: validate_import_decompressed_archive_size -introduced_by_url: -rollout_issue_url: -milestone: +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39686 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282245 +milestone: '13.4' type: development group: group::import default_enabled: false diff --git a/config/feature_flags/development/vue_epics_list.yml b/config/feature_flags/development/vue_epics_list.yml new file mode 100644 index 00000000000..22e2a53aeee --- /dev/null +++ b/config/feature_flags/development/vue_epics_list.yml @@ -0,0 +1,8 @@ +--- +name: vue_epics_list +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46769 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276189 +milestone: '13.9' +type: development +group: group::product planning +default_enabled: false diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b53c36cf029..557ad272d72 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -553,6 +553,9 @@ Settings.cron_jobs['schedule_merge_request_cleanup_refs_worker']['job_class'] = Settings.cron_jobs['manage_evidence_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['manage_evidence_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['manage_evidence_worker']['job_class'] = 'Releases::ManageEvidenceWorker' +Settings.cron_jobs['user_status_cleanup_batch_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['user_status_cleanup_batch_worker']['cron'] ||= '* * * * *' +Settings.cron_jobs['user_status_cleanup_batch_worker']['job_class'] = 'UserStatusCleanup::BatchWorker' Gitlab.com do Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({}) diff --git a/db/migrate/20210208125050_add_status_expires_at_to_user_statuses.rb b/db/migrate/20210208125050_add_status_expires_at_to_user_statuses.rb new file mode 100644 index 00000000000..3ec1f6014a8 --- /dev/null +++ b/db/migrate/20210208125050_add_status_expires_at_to_user_statuses.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddStatusExpiresAtToUserStatuses < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_column(:user_statuses, :clear_status_at, :datetime_with_timezone, null: true) + end + end + + def down + with_lock_retries do + remove_column(:user_statuses, :clear_status_at) + end + end +end diff --git a/db/migrate/20210208125248_add_index_on_user_statuses_status_expires_at.rb b/db/migrate/20210208125248_add_index_on_user_statuses_status_expires_at.rb new file mode 100644 index 00000000000..98f3449c2e8 --- /dev/null +++ b/db/migrate/20210208125248_add_index_on_user_statuses_status_expires_at.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexOnUserStatusesStatusExpiresAt < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_user_statuses_on_clear_status_at_not_null' + + disable_ddl_transaction! + + def up + add_concurrent_index(:user_statuses, :clear_status_at, name: INDEX_NAME, where: 'clear_status_at IS NOT NULL') + end + + def down + remove_concurrent_index_by_name(:user_statuses, INDEX_NAME) + end +end diff --git a/db/schema_migrations/20210208125050 b/db/schema_migrations/20210208125050 new file mode 100644 index 00000000000..35877cfc029 --- /dev/null +++ b/db/schema_migrations/20210208125050 @@ -0,0 +1 @@ +b9200d6c754f7c450ba0c718171806e8f4f9720d870e532f4800640ca707f24f \ No newline at end of file diff --git a/db/schema_migrations/20210208125248 b/db/schema_migrations/20210208125248 new file mode 100644 index 00000000000..91d5c4bb950 --- /dev/null +++ b/db/schema_migrations/20210208125248 @@ -0,0 +1 @@ +3a7fb1b7959f09b9ba464253a72d52bcb744e7f78aac4f44e1d9201fa3c8387d \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 4691ff0fa62..973f753fc43 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17842,7 +17842,8 @@ CREATE TABLE user_statuses ( emoji character varying DEFAULT 'speech_balloon'::character varying NOT NULL, message character varying(100), message_html character varying, - availability smallint DEFAULT 0 NOT NULL + availability smallint DEFAULT 0 NOT NULL, + clear_status_at timestamp with time zone ); CREATE SEQUENCE user_statuses_user_id_seq @@ -23521,6 +23522,8 @@ CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING CREATE UNIQUE INDEX index_user_preferences_on_user_id ON user_preferences USING btree (user_id); +CREATE INDEX index_user_statuses_on_clear_status_at_not_null ON user_statuses USING btree (clear_status_at) WHERE (clear_status_at IS NOT NULL); + CREATE INDEX index_user_statuses_on_user_id ON user_statuses USING btree (user_id); CREATE UNIQUE INDEX index_user_synced_attributes_metadata_on_user_id ON user_synced_attributes_metadata USING btree (user_id); diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index 3e9fb9d29e7..e2c06c2025f 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -396,6 +396,69 @@ In GitLab 13.4, a seed project is added when GitLab is first installed. This mak on a new Geo secondary node. There is an [issue to account for seed projects](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5618) when checking the database. +### Message: `Synchronization failed - Error syncing repository` + +WARNING: +If large repositories are affected by this problem, +their resync may take a long time and cause significant load on your Geo nodes, +storage and network systems. + +If you get the error `Synchronization failed - Error syncing repository` along with the following log messages, this indicates that the expected `geo` remote is not present in the `.git/config` file +of a repository on the secondary Geo node's filesystem: + +```json +{ + "created": "@1603481145.084348757", + "description": "Error received from peer unix:/var/opt/gitlab/gitaly/gitaly.socket", + … + "grpc_message": "exit status 128", + "grpc_status": 13 +} +{ … + "grpc.request.fullMethod": "/gitaly.RemoteService/FindRemoteRootRef", + "grpc.request.glProjectPath": "/", + … + "level": "error", + "msg": "fatal: 'geo' does not appear to be a git repository + fatal: Could not read from remote repository. …", +} +``` + +To solve this: + +1. Log into the secondary Geo node. + +1. Back up [the `.git` folder](../../repository_storage_types.md#translating-hashed-storage-paths). + +1. Optional: [Spot-check](../../troubleshooting/log_parsing.md#find-all-projects-affected-by-a-fatal-git-problem)) + a few of those IDs whether they indeed correspond + to a project with known Geo replication failures. + Use `fatal: 'geo'` as the `grep` term and the following API call: + + ```shell + curl --request GET --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/" + ``` + +1. Enter the [Rails console](../../troubleshooting/navigating_gitlab_via_rails_console.md) and run: + + ```ruby + failed_geo_syncs = Geo::ProjectRegistry.failed.pluck(:id) + failed_geo_syncs.each do |fgs| + puts Geo::ProjectRegistry.failed.find(fgs).project_id + end + ``` + +1. Run the following commands to reset each project's + Geo-related attributes and execute a new sync: + + ```ruby + failed_geo_syncs.each do |fgs| + registry = Geo::ProjectRegistry.failed.find(fgs) + registry.update(resync_repository: true, force_to_redownload_repository: false, repository_retry_count: 0) + Geo::RepositorySyncService.new(registry.project).execute + end + ``` + ### Very large repositories never successfully synchronize on the **secondary** node GitLab places a timeout on all repository clones, including project imports diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index ecc2c924254..3cad18dc497 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -75,8 +75,7 @@ types. If you want to use local storage for specific object types, you can Most types of objects, such as CI artifacts, LFS files, upload attachments, and so on can be saved in object storage by specifying a single -credential for object storage with multiple buckets. A [different bucket -for each type must be used](#use-separate-buckets). +credential for object storage with multiple buckets. When the consolidated form is: @@ -571,22 +570,13 @@ See the following additional guides: ## Warnings, limitations, and known issues -### Use separate buckets +### Separate buckets required when using Helm -Using separate buckets for each data type is the recommended approach for GitLab. +Generally, using the same bucket for your Object Storage is fine to do +for convenience. -A limitation of our configuration is that each use of object storage is separately configured. -[We have an issue for improving this](https://gitlab.com/gitlab-org/gitlab/-/issues/23345) -and easily using one bucket with separate folders is one improvement that this might bring. - -There is at least one specific issue with using the same bucket: -when GitLab is deployed with the Helm chart restore from backup -[will not properly function](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer) -unless separate buckets are used. - -One risk of using a single bucket would be that if your organisation decided to -migrate GitLab to the Helm deployment in the future. GitLab would run, but the situation with -backups might not be realised until the organisation had a critical requirement for the backups to work. +However, if you're using or planning to use Helm, separate buckets will +be required as there is a [known limitation with restorations of Helm chart backups](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer). ### S3 API compatibility issues diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md index ff1443a4ae3..d4d522ab1b8 100644 --- a/doc/administration/reference_architectures/10k_users.md +++ b/doc/administration/reference_architectures/10k_users.md @@ -1812,8 +1812,9 @@ To configure Praefect with TLS: ## Configure Sidekiq -Sidekiq requires connections to the Redis, PostgreSQL and Gitaly instances. -The following IPs will be used as an example: +Sidekiq requires connection to the [Redis](#configure-redis), +[PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. +[Object storage](#configure-the-object-storage) is also required to be configured. - `10.6.0.101`: Sidekiq 1 - `10.6.0.102`: Sidekiq 2 @@ -1927,6 +1928,25 @@ To configure the Sidekiq nodes, on each one: # Rails Status for prometheus gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and replace @@ -1947,6 +1967,7 @@ You can also run [multiple Sidekiq processes](../operations/extra_sidekiq_proces ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. +[Object storage](#configure-the-object-storage) is also required to be configured. The following IPs will be used as an example: @@ -2036,6 +2057,25 @@ On each node perform the following: # scrape the NGINX metrics gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] nginx['status']['options']['allow'] = ['10.6.0.121/32', '127.0.0.0/8'] + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md index 12519f47cce..48c72bb930d 100644 --- a/doc/administration/reference_architectures/25k_users.md +++ b/doc/administration/reference_architectures/25k_users.md @@ -1512,8 +1512,9 @@ To configure Gitaly with TLS: ## Configure Sidekiq -Sidekiq requires connections to the Redis, PostgreSQL and Gitaly instances. -The following IPs will be used as an example: +Sidekiq requires connection to the [Redis](#configure-redis), +[PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. +[Object storage](#configure-the-object-storage) is also required to be configured. - `10.6.0.101`: Sidekiq 1 - `10.6.0.102`: Sidekiq 2 @@ -1624,6 +1625,25 @@ To configure the Sidekiq nodes, on each one: # Rails Status for prometheus gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and replace @@ -1644,6 +1664,7 @@ You can also run [multiple Sidekiq processes](../operations/extra_sidekiq_proces ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. +[Object storage](#configure-the-object-storage) is also required to be configured. The following IPs will be used as an example: @@ -1736,6 +1757,25 @@ On each node perform the following: # scrape the NGINX metrics gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] nginx['status']['options']['allow'] = ['10.6.0.121/32', '127.0.0.0/8'] + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md index 353160bea9a..9ad6054104a 100644 --- a/doc/administration/reference_architectures/2k_users.md +++ b/doc/administration/reference_architectures/2k_users.md @@ -657,6 +657,25 @@ On each node perform the following: gitlab_rails['monitoring_whitelist'] = ['/32', '127.0.0.0/8'] nginx['status']['options']['allow'] = ['/32', '127.0.0.0/8'] + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" + ## Uncomment and edit the following options if you have set up NFS ## ## Prevent GitLab from starting if NFS data mounts are not available diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md index d6344d0fa4e..175c4318d78 100644 --- a/doc/administration/reference_architectures/3k_users.md +++ b/doc/administration/reference_architectures/3k_users.md @@ -1212,7 +1212,10 @@ To configure Gitaly with TLS: ## Configure Sidekiq -Sidekiq requires connection to the Redis, PostgreSQL and Gitaly instance. +Sidekiq requires connection to the [Redis](#configure-redis), +[PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. +[Object storage](#configure-the-object-storage) is also required to be configured. + The following IPs will be used as an example: - `10.6.0.71`: Sidekiq 1 @@ -1307,6 +1310,25 @@ To configure the Sidekiq nodes, one each one: # Rails Status for prometheus gitlab_rails['monitoring_whitelist'] = ['10.6.0.81/32', '127.0.0.0/8'] gitlab_rails['prometheus_address'] = '10.6.0.81:9090' + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). @@ -1337,6 +1359,7 @@ You can also run [multiple Sidekiq processes](../operations/extra_sidekiq_proces ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. +[Object storage](#configure-the-object-storage) is also required to be configured. On each node perform the following: @@ -1454,6 +1477,25 @@ On each node perform the following: #web_server['gid'] = 9001 #registry['uid'] = 9002 #registry['gid'] = 9002 + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. If you're using [Gitaly with TLS support](#gitaly-tls-support), make sure the diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md index cd23d1d82c6..0b22a2d3602 100644 --- a/doc/administration/reference_architectures/50k_users.md +++ b/doc/administration/reference_architectures/50k_users.md @@ -1512,8 +1512,9 @@ To configure Gitaly with TLS: ## Configure Sidekiq -Sidekiq requires connections to the Redis, PostgreSQL and Gitaly instances. -The following IPs will be used as an example: +Sidekiq requires connection to the [Redis](#configure-redis), +[PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. +[Object storage](#configure-the-object-storage) is also required to be configured. - `10.6.0.101`: Sidekiq 1 - `10.6.0.102`: Sidekiq 2 @@ -1624,6 +1625,25 @@ To configure the Sidekiq nodes, on each one: # Rails Status for prometheus gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Copy the `/etc/gitlab/gitlab-secrets.json` file from your Consul server, and replace @@ -1644,6 +1664,7 @@ You can also run [multiple Sidekiq processes](../operations/extra_sidekiq_proces ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. +[Object storage](#configure-the-object-storage) is also required to be configured. The following IPs will be used as an example: @@ -1736,6 +1757,25 @@ On each node perform the following: # scrape the NGINX metrics gitlab_rails['monitoring_whitelist'] = ['10.6.0.121/32', '127.0.0.0/8'] nginx['status']['options']['allow'] = ['10.6.0.121/32', '127.0.0.0/8'] + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md index e00456b9be1..37d35c299fa 100644 --- a/doc/administration/reference_architectures/5k_users.md +++ b/doc/administration/reference_architectures/5k_users.md @@ -1211,8 +1211,9 @@ To configure Gitaly with TLS: ## Configure Sidekiq -Sidekiq requires connection to the Redis, PostgreSQL and Gitaly instance. -The following IPs will be used as an example: +Sidekiq requires connection to the [Redis](#configure-redis), +[PostgreSQL](#configure-postgresql) and [Gitaly](#configure-gitaly) instances. +[Object storage](#configure-the-object-storage) is also required to be configured. - `10.6.0.71`: Sidekiq 1 - `10.6.0.72`: Sidekiq 2 @@ -1306,6 +1307,25 @@ To configure the Sidekiq nodes, one each one: # Rails Status for prometheus gitlab_rails['monitoring_whitelist'] = ['10.6.0.81/32', '127.0.0.0/8'] gitlab_rails['prometheus_address'] = '10.6.0.81:9090' + + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" ``` 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). @@ -1336,6 +1356,7 @@ You can also run [multiple Sidekiq processes](../operations/extra_sidekiq_proces ## Configure GitLab Rails This section describes how to configure the GitLab application (Rails) component. +[Object storage](#configure-the-object-storage) is also required to be configured. On each node perform the following: @@ -1439,6 +1460,25 @@ On each node perform the following: nginx['status']['options']['allow'] = ['10.6.0.81/32', '127.0.0.0/8'] gitlab_rails['prometheus_address'] = '10.6.0.81:9090' + ############################# + ### Object storage ### + ############################# + + # This is an example for configuring Object Storage on GCP + # Replace this config with your chosen Object Storage provider as desired + gitlab_rails['object_store']['connection'] = { + 'provider' => 'Google', + 'google_project' => '', + 'google_json_key_location' => '' + } + gitlab_rails['object_store']['objects']['artifacts']['bucket'] = "" + gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = "" + gitlab_rails['object_store']['objects']['lfs']['bucket'] = "" + gitlab_rails['object_store']['objects']['uploads']['bucket'] = "" + gitlab_rails['object_store']['objects']['packages']['bucket'] = "" + gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = "" + gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = "" + ## Uncomment and edit the following options if you have set up NFS ## ## Prevent GitLab from starting if NFS data mounts are not available diff --git a/doc/administration/troubleshooting/log_parsing.md b/doc/administration/troubleshooting/log_parsing.md index 144aa0f6d3b..25300d036ed 100644 --- a/doc/administration/troubleshooting/log_parsing.md +++ b/doc/administration/troubleshooting/log_parsing.md @@ -179,3 +179,11 @@ jq --raw-output --slurp ' 663 106 ms, 96 ms, 94 ms 'groupABC/project123' ... ``` + +#### Find all projects affected by a fatal Git problem + +```shell +grep "fatal: " /var/log/gitlab/gitaly/current | \ + jq '."grpc.request.glProjectPath"' | \ + sort | uniq +``` diff --git a/doc/ci/pipeline_editor/index.md b/doc/ci/pipeline_editor/index.md index 430ef9c83c0..0dbf6797b99 100644 --- a/doc/ci/pipeline_editor/index.md +++ b/doc/ci/pipeline_editor/index.md @@ -25,6 +25,7 @@ From the pipeline editor page you can: - Do a deeper [lint](#lint-ci-configuration) of your configuration, that verifies it with any configuration added with the [`include`](../yaml/README.md#include) keyword. - See a [visualization](#visualize-ci-configuration) of the current configuration. +- View an [expanded](#view-expanded-configuration) version of your configuration. - [Commit](#commit-changes-to-ci-configuration) the changes to a specific branch. NOTE: @@ -101,6 +102,40 @@ To enable it: Feature.enable(:ci_config_visualization_tab) ``` +## View expanded configuration + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/246801) in GitLab 13.9. +> - It is [deployed behind a feature flag](../../user/feature_flags.md), disabled by default. +> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-expanded-configuration). **(FREE SELF)** + +To view the fully expanded CI/CD configuration as one combined file, go to the +pipeline editor's **View merged YAML** tab. This tab displays an expanded configuration +where: + +- Configuration imported with [`include`](../yaml/README.md#include) is copied into the view. +- Jobs that use [`extends`](../yaml/README.md#extends) display with the + [extended configuration merged into the job](../yaml/README.md#merge-details). +- YAML anchors are [replaced with the linked configuration](../yaml/README.md#anchors). + +### Enable or disable expanded configuration **(FREE SELF)** + +Expanded CI/CD configuration is under development and not ready for production use. +It is deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) +can opt to enable it. + +To enable it: + +```ruby +Feature.enable(:ci_config_visualization_tab) +``` + +To disable it: + +```ruby +Feature.disable(:ci_config_visualization_tab) +``` + ## Commit changes to CI configuration The commit form appears at the bottom of each tab in the editor so you can commit diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 81de680710b..166f7b350bf 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -65,9 +65,9 @@ request is as follows: template already provided in the "Description" field. 1. If you are contributing documentation, choose `Documentation` from the "Choose a template" menu and fill in the description according to the template. - 1. Mention the issue(s) your merge request solves, using the `Solves #XXX` or - `Closes #XXX` syntax to [auto-close](../../user/project/issues/managing_issues.md#closing-issues-automatically) - the issue(s) once the merge request is merged. + 1. Use the syntax `Solves #XXX`, `Closes #XXX`, or `Refs #XXX` to mention the issue(s) your merge + request addresses. Referenced issues do not [close automatically](../../user/project/issues/managing_issues.md#closing-issues-automatically). + You must close them manually once the merge request is merged. 1. If you're allowed to, set a relevant milestone and [labels](issue_workflow.md). 1. UI changes should use available components from the GitLab Design System, [Pajamas](https://design.gitlab.com/). The MR must include *Before* and diff --git a/doc/integration/security_partners/index.md b/doc/integration/security_partners/index.md index 1053c612782..1cd14947e74 100644 --- a/doc/integration/security_partners/index.md +++ b/doc/integration/security_partners/index.md @@ -13,5 +13,11 @@ each security partner: - [Anchore](https://docs.anchore.com/current/docs/using/integration/ci_cd/gitlab/) +- [Bridgecrew](https://docs.bridgecrew.io/docs/integrate-with-gitlab-self-managed) +- [Checkmarx](https://checkmarx.atlassian.net/wiki/spaces/SD/pages/1929937052/GitLab+Integration) +- [Indeni](https://indeni.com/doc-indeni-cloudrail/integrate-with-ci-cd/gitlab-instructions/) +- [JScrambler](https://docs.jscrambler.com/code-integrity/documentation/gitlab-ci-integration) +- [StackHawk](https://docs.stackhawk.com/continuous-integration/gitlab.html) +- [WhiteSource](https://www.whitesourcesoftware.com/gitlab/) diff --git a/doc/operations/incident_management/integrations.md b/doc/operations/incident_management/integrations.md index 9aff2e6e462..b3e7d9c544d 100644 --- a/doc/operations/incident_management/integrations.md +++ b/doc/operations/incident_management/integrations.md @@ -90,7 +90,7 @@ parameters. All fields are optional. If the incoming alert does not contain a va | `service` | String | The affected service. | | `monitoring_tool` | String | The name of the associated monitoring tool. | | `hosts` | String or Array | One or more hosts, as to where this incident occurred. | -| `severity` | String | The severity of the alert. Must be one of `critical`, `high`, `medium`, `low`, `info`, `unknown`. Default is `critical`. | +| `severity` | String | The severity of the alert. Case-insensitive. Can be one of: `critical`, `high`, `medium`, `low`, `info`, `unknown`. Defaults to `critical` if missing or value is not in this list. | | `fingerprint` | String or Array | The unique identifier of the alert. This can be used to group occurrences of the same alert. | | `gitlab_environment_name` | String | The name of the associated GitLab [environment](../../ci/environments/index.md). Required to [display alerts on a dashboard](../../user/operations_dashboard/index.md#adding-a-project-to-the-dashboard). | diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 6268b525755..9320dbba1b8 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -7,9 +7,9 @@ type: reference, howto # Threads **(FREE)** -You can use words to communicate with other users all over GitLab. +GitLab encourages communication through comments, threads, and suggestions. -For example, you can leave a comment in the following places: +For example, you can create a comment in the following places: - Issues - Epics diff --git a/doc/user/markdown.md b/doc/user/markdown.md index afad68c5554..5f974d75522 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -444,6 +444,7 @@ GFM recognizes the following: | snippet | `$123` | `namespace/project$123` | `project$123` | | epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | | | vulnerability **(ULTIMATE)** (1)| `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` | +| feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` | | label by ID | `~123` | `namespace/project~123` | `project~123` | | one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` | | multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` | diff --git a/lib/api/entities/user_status.rb b/lib/api/entities/user_status.rb index 1d5cc27e5ef..ef4772f60c6 100644 --- a/lib/api/entities/user_status.rb +++ b/lib/api/entities/user_status.rb @@ -9,6 +9,7 @@ module API expose :message_html do |entity| MarkupHelper.markdown_field(entity, :message) end + expose :clear_status_at end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 36368adf2f0..0352ddfb214 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1004,11 +1004,15 @@ module API optional :emoji, type: String, desc: "The emoji to set on the status" optional :message, type: String, desc: "The status message to set" optional :availability, type: String, desc: "The availability of user to set" + optional :clear_status_after, type: String, desc: "Automatically clear emoji, message and availability fields after a certain time", values: UserStatus::CLEAR_STATUS_QUICK_OPTIONS.keys end put "status", feature_category: :users do forbidden! unless can?(current_user, :update_user_status, current_user) - if ::Users::SetStatusService.new(current_user, declared_params).execute + update_params = declared_params + update_params.delete(:clear_status_after) if Feature.disabled?(:clear_status_with_quick_options, current_user) + + if ::Users::SetStatusService.new(current_user, update_params).execute present current_user.status, with: Entities::UserStatus else render_validation_error!(current_user.status) diff --git a/lib/banzai/filter/feature_flag_reference_filter.rb b/lib/banzai/filter/feature_flag_reference_filter.rb index 343a715b27a..c11576901ce 100644 --- a/lib/banzai/filter/feature_flag_reference_filter.rb +++ b/lib/banzai/filter/feature_flag_reference_filter.rb @@ -14,8 +14,6 @@ module Banzai end def parent_records(parent, ids) - return self.class.object_class.none unless Feature.enabled?(:feature_flag_contextual_issue, parent) - parent.operations_feature_flags.where(iid: ids.to_a) end diff --git a/lib/bulk_imports/groups/pipelines/labels_pipeline.rb b/lib/bulk_imports/groups/pipelines/labels_pipeline.rb index 6b48106e5b8..40dab9b444c 100644 --- a/lib/bulk_imports/groups/pipelines/labels_pipeline.rb +++ b/lib/bulk_imports/groups/pipelines/labels_pipeline.rb @@ -13,7 +13,7 @@ module BulkImports loader BulkImports::Groups::Loaders::LabelsLoader - def after_run(context, extracted_data) + def after_run(extracted_data) context.entity.update_tracker_for( relation: :labels, has_next_page: extracted_data.has_next_page?, @@ -21,7 +21,7 @@ module BulkImports ) if extracted_data.has_next_page? - run(context) + run end end end diff --git a/lib/bulk_imports/groups/pipelines/members_pipeline.rb b/lib/bulk_imports/groups/pipelines/members_pipeline.rb index ddc2cb124db..b00c4c1a659 100644 --- a/lib/bulk_imports/groups/pipelines/members_pipeline.rb +++ b/lib/bulk_imports/groups/pipelines/members_pipeline.rb @@ -14,7 +14,7 @@ module BulkImports loader BulkImports::Groups::Loaders::MembersLoader - def after_run(context, extracted_data) + def after_run(extracted_data) context.entity.update_tracker_for( relation: :group_members, has_next_page: extracted_data.has_next_page?, @@ -22,7 +22,7 @@ module BulkImports ) if extracted_data.has_next_page? - run(context) + run end end end diff --git a/lib/bulk_imports/importers/group_importer.rb b/lib/bulk_imports/importers/group_importer.rb index 4888734087f..f967b7ad7ab 100644 --- a/lib/bulk_imports/importers/group_importer.rb +++ b/lib/bulk_imports/importers/group_importer.rb @@ -10,7 +10,7 @@ module BulkImports def execute context = BulkImports::Pipeline::Context.new(entity) - pipelines.each { |pipeline| pipeline.new.run(context) } + pipelines.each { |pipeline| pipeline.new(context).run } entity.finish! end diff --git a/lib/bulk_imports/pipeline.rb b/lib/bulk_imports/pipeline.rb index 3d2a74cd4b3..1d55ad95887 100644 --- a/lib/bulk_imports/pipeline.rb +++ b/lib/bulk_imports/pipeline.rb @@ -4,12 +4,17 @@ module BulkImports module Pipeline extend ActiveSupport::Concern include Gitlab::ClassAttributes + include Runner + + def initialize(context) + @context = context + end included do - include Runner - private + attr_reader :context + def extractor @extractor ||= instantiate(self.class.get_extractor) end diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb index 1c4ee154874..c9841a51d9b 100644 --- a/lib/bulk_imports/pipeline/runner.rb +++ b/lib/bulk_imports/pipeline/runner.rb @@ -7,78 +7,78 @@ module BulkImports MarkedAsFailedError = Class.new(StandardError) - def run(context) - raise MarkedAsFailedError if marked_as_failed?(context) + def run + raise MarkedAsFailedError if marked_as_failed? - info(context, message: 'Pipeline started') + info(message: 'Pipeline started') - extracted_data = extracted_data_from(context) + extracted_data = extracted_data_from extracted_data&.each do |entry| transformers.each do |transformer| - entry = run_pipeline_step(:transformer, transformer.class.name, context) do + entry = run_pipeline_step(:transformer, transformer.class.name) do transformer.transform(context, entry) end end - run_pipeline_step(:loader, loader.class.name, context) do + run_pipeline_step(:loader, loader.class.name) do loader.load(context, entry) end end - after_run(context, extracted_data) if respond_to?(:after_run) + after_run(extracted_data) if respond_to?(:after_run) - info(context, message: 'Pipeline finished') + info(message: 'Pipeline finished') rescue MarkedAsFailedError - log_skip(context) + log_skip end private # rubocop:disable Lint/UselessAccessModifier - def run_pipeline_step(step, class_name, context) - raise MarkedAsFailedError if marked_as_failed?(context) + def run_pipeline_step(step, class_name) + raise MarkedAsFailedError if marked_as_failed? - info(context, pipeline_step: step, step_class: class_name) + info(pipeline_step: step, step_class: class_name) yield rescue MarkedAsFailedError - log_skip(context, step => class_name) + log_skip(step => class_name) rescue => e - log_import_failure(e, step, context) + log_import_failure(e, step) - mark_as_failed(context) if abort_on_failure? + mark_as_failed if abort_on_failure? nil end - def extracted_data_from(context) - run_pipeline_step(:extractor, extractor.class.name, context) do + def extracted_data_from + run_pipeline_step(:extractor, extractor.class.name) do extractor.extract(context) end end - def mark_as_failed(context) - warn(context, message: 'Pipeline failed', pipeline_class: pipeline) + def mark_as_failed + warn(message: 'Pipeline failed', pipeline_class: pipeline) context.entity.fail_op! end - def marked_as_failed?(context) + def marked_as_failed? return true if context.entity.failed? false end - def log_skip(context, extra = {}) + def log_skip(extra = {}) log = { message: 'Skipping due to failed pipeline status', pipeline_class: pipeline }.merge(extra) - info(context, log) + info(log) end - def log_import_failure(exception, step, context) + def log_import_failure(exception, step) attributes = { bulk_import_entity_id: context.entity.id, pipeline_class: pipeline, @@ -91,15 +91,15 @@ module BulkImports BulkImports::Failure.create(attributes) end - def warn(context, extra = {}) - logger.warn(log_base_params(context).merge(extra)) + def warn(extra = {}) + logger.warn(log_base_params.merge(extra)) end - def info(context, extra = {}) - logger.info(log_base_params(context).merge(extra)) + def info(extra = {}) + logger.info(log_base_params.merge(extra)) end - def log_base_params(context) + def log_base_params { bulk_import_entity_id: context.entity.id, bulk_import_entity_type: context.entity.source_type, diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index f054f03b7b4..d1a366125ef 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -75,6 +75,10 @@ module Gitlab def self.display_codequality_backend_comparison?(project) ::Feature.enabled?(:codequality_backend_comparison, project, default_enabled: :yaml) end + + def self.use_coverage_data_new_finder?(record) + ::Feature.enabled?(:coverage_data_new_finder, record, default_enabled: :yaml) + end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index a64bb08fe3a..3dd317c5a64 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -46,7 +46,6 @@ module Gitlab push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false) push_frontend_feature_flag(:usage_data_api, default_enabled: true) push_frontend_feature_flag(:security_auto_fix, default_enabled: false) - push_frontend_feature_flag(:gl_tooltips, default_enabled: :yaml) end # Exposes the state of a feature flag to the frontend code. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3b948e7fdda..cf3ae6af138 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11544,6 +11544,12 @@ msgstr "" msgid "Epics, Issues, and Merge Requests" msgstr "" +msgid "Epics|%{startDate} – %{dueDate}" +msgstr "" + +msgid "Epics|%{startDate} – No due date" +msgstr "" + msgid "Epics|Add a new epic" msgstr "" @@ -11571,6 +11577,9 @@ msgstr "" msgid "Epics|Leave empty to inherit from milestone dates" msgstr "" +msgid "Epics|No start date – %{dueDate}" +msgstr "" + msgid "Epics|Remove epic" msgstr "" @@ -11598,6 +11607,9 @@ msgstr "" msgid "Epics|Something went wrong while fetching child epics." msgstr "" +msgid "Epics|Something went wrong while fetching epics list." +msgstr "" + msgid "Epics|Something went wrong while fetching group epics." msgstr "" @@ -21672,6 +21684,9 @@ msgstr "" msgid "Pipelines|Copy trigger token" msgstr "" +msgid "Pipelines|Could not load merged YAML content" +msgstr "" + msgid "Pipelines|Description" msgstr "" @@ -21708,6 +21723,9 @@ msgstr "" msgid "Pipelines|Loading Pipelines" msgstr "" +msgid "Pipelines|Merged YAML is view only" +msgstr "" + msgid "Pipelines|More Information" msgstr "" @@ -21780,6 +21798,9 @@ msgstr "" msgid "Pipelines|Validating GitLab CI configuration…" msgstr "" +msgid "Pipelines|View merged YAML" +msgstr "" + msgid "Pipelines|Visualize" msgstr "" @@ -29404,6 +29425,9 @@ msgstr "" msgid "There are no charts configured for this page" msgstr "" +msgid "There are no closed epics" +msgstr "" + msgid "There are no closed issues" msgstr "" @@ -29428,6 +29452,9 @@ msgstr "" msgid "There are no matching files" msgstr "" +msgid "There are no open epics" +msgstr "" + msgid "There are no open issues" msgstr "" diff --git a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb index 594c24bb7e3..81318b49cd9 100644 --- a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb +++ b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do let(:end_date) { '2020-03-09' } let(:allowed_to_read) { true } let(:user) { create(:user) } + let(:feature_enabled?) { true } before do create_daily_coverage('rspec', 79.0, '2020-03-09') @@ -24,6 +25,8 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).with(user, :read_build_report_results, project).and_return(allowed_to_read) + stub_feature_flags(coverage_data_new_finder: feature_enabled?) + get :index, params: { namespace_id: project.namespace, project_id: project, @@ -55,9 +58,7 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do end end - context 'when format is CSV' do - let(:format) { :csv } - + shared_examples 'CSV results' do it 'serves the results in CSV' do expect(response).to have_gitlab_http_status(:ok) expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8') @@ -88,9 +89,7 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do it_behaves_like 'ensuring policy' end - context 'when format is JSON' do - let(:format) { :json } - + shared_examples 'JSON results' do it 'serves the results in JSON' do expect(response).to have_gitlab_http_status(:ok) @@ -137,6 +136,38 @@ RSpec.describe Projects::Ci::DailyBuildGroupReportResultsController do it_behaves_like 'validating param_type' it_behaves_like 'ensuring policy' end + + context 'when format is JSON' do + let(:format) { :json } + + context 'when coverage_data_new_finder flag is enabled' do + let(:feature_enabled?) { true } + + it_behaves_like 'JSON results' + end + + context 'when coverage_data_new_finder flag is disabled' do + let(:feature_enabled?) { false } + + it_behaves_like 'JSON results' + end + end + + context 'when format is CSV' do + let(:format) { :csv } + + context 'when coverage_data_new_finder flag is enabled' do + let(:feature_enabled?) { true } + + it_behaves_like 'CSV results' + end + + context 'when coverage_data_new_finder flag is disabled' do + let(:feature_enabled?) { false } + + it_behaves_like 'CSV results' + end + end end def create_daily_coverage(group_name, coverage, date) diff --git a/spec/finders/ci/testing/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/testing/daily_build_group_report_results_finder_spec.rb new file mode 100644 index 00000000000..a703f3b800c --- /dev/null +++ b/spec/finders/ci/testing/daily_build_group_report_results_finder_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Testing::DailyBuildGroupReportResultsFinder do + describe '#execute' do + let_it_be(:project) { create(:project, :private) } + let(:user_without_permission) { create(:user) } + let_it_be(:user_with_permission) { project.owner } + let_it_be(:ref_path) { 'refs/heads/master' } + let(:limit) { nil } + let_it_be(:default_branch) { false } + let(:start_date) { '2020-03-09' } + let(:end_date) { '2020-03-10' } + let(:sort) { true } + + let_it_be(:rspec_coverage_1) { create_daily_coverage('rspec', 79.0, '2020-03-09') } + let_it_be(:karma_coverage_1) { create_daily_coverage('karma', 89.0, '2020-03-09') } + let_it_be(:rspec_coverage_2) { create_daily_coverage('rspec', 95.0, '2020-03-10') } + let_it_be(:karma_coverage_2) { create_daily_coverage('karma', 92.0, '2020-03-10') } + let_it_be(:rspec_coverage_3) { create_daily_coverage('rspec', 97.0, '2020-03-11') } + let_it_be(:karma_coverage_3) { create_daily_coverage('karma', 99.0, '2020-03-11') } + + let(:finder) { described_class.new(params: params, current_user: current_user) } + + let(:params) do + { + project: project, + coverage: true, + ref_path: ref_path, + start_date: start_date, + end_date: end_date, + limit: limit, + sort: sort + } + end + + subject(:coverages) { finder.execute } + + context 'when params are provided' do + context 'when current user is not allowed to read data' do + let(:current_user) { user_without_permission } + + it 'returns an empty collection' do + expect(coverages).to be_empty + end + end + + context 'when current user is allowed to read data' do + let(:current_user) { user_with_permission } + + it 'returns matching coverages within the given date range' do + expect(coverages).to match_array([ + karma_coverage_2, + rspec_coverage_2, + karma_coverage_1, + rspec_coverage_1 + ]) + end + + context 'when ref_path is nil' do + let(:default_branch) { true } + let(:ref_path) { nil } + + it 'returns coverages for the default branch' do + rspec_coverage_4 = create_daily_coverage('rspec', 66.0, '2020-03-10') + + expect(coverages).to contain_exactly(rspec_coverage_4) + end + end + + context 'when limit is specified' do + let(:limit) { 2 } + + it 'returns limited number of matching coverages within the given date range' do + expect(coverages).to match_array([ + karma_coverage_2, + rspec_coverage_2 + ]) + end + end + end + end + end + + private + + def create_daily_coverage(group_name, coverage, date) + create( + :ci_daily_build_group_report_result, + project: project, + ref_path: ref_path || 'feature-branch', + group_name: group_name, + data: { 'coverage' => coverage }, + date: date, + default_branch: default_branch + ) + end +end diff --git a/spec/frontend/collapsed_sidebar_todo_spec.js b/spec/frontend/collapsed_sidebar_todo_spec.js index cc89a3c68f0..52447939ac4 100644 --- a/spec/frontend/collapsed_sidebar_todo_spec.js +++ b/spec/frontend/collapsed_sidebar_todo_spec.js @@ -5,6 +5,9 @@ import { TEST_HOST } from 'spec/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import Sidebar from '~/right_sidebar'; +import { fixTitle } from '~/tooltips'; + +jest.mock('~/tooltips'); describe('Issuable right sidebar collapsed todo toggle', () => { const fixtureName = 'issues/open-issue.html'; @@ -96,11 +99,10 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); setImmediate(() => { - expect( - document - .querySelector('.js-issuable-todo.sidebar-collapsed-icon') - .getAttribute('data-original-title'), - ).toBe('Mark as done'); + const el = document.querySelector('.js-issuable-todo.sidebar-collapsed-icon'); + + expect(el.getAttribute('title')).toBe('Mark as done'); + expect(fixTitle).toHaveBeenCalledWith(el); done(); }); diff --git a/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js new file mode 100644 index 00000000000..8ec2b6d8bef --- /dev/null +++ b/spec/frontend/pipeline_editor/components/editor/ci_config_merged_preview_spec.js @@ -0,0 +1,88 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlIcon } from '@gitlab/ui'; + +import { EDITOR_READY_EVENT } from '~/editor/constants'; +import { INVALID_CI_CONFIG } from '~/pipelines/constants'; +import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; +import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; +import { mockLintResponse, mockCiConfigPath } from '../../mock_data'; + +describe('Text editor component', () => { + let wrapper; + + const MockEditorLite = { + template: '
', + props: ['value', 'fileName', 'editorOptions'], + mounted() { + this.$emit(EDITOR_READY_EVENT); + }, + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMount(CiConfigMergedPreview, { + propsData: { + ciConfigData: mockLintResponse, + ...props, + }, + provide: { + ciConfigPath: mockCiConfigPath, + }, + stubs: { + EditorLite: MockEditorLite, + }, + }); + }; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findIcon = () => wrapper.findComponent(GlIcon); + const findEditor = () => wrapper.findComponent(MockEditorLite); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when status is invalid', () => { + beforeEach(() => { + createComponent({ props: { ciConfigData: { status: CI_CONFIG_STATUS_INVALID } } }); + }); + + it('show an error message', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[INVALID_CI_CONFIG]); + }); + + it('hides the editor', () => { + expect(findEditor().exists()).toBe(false); + }); + }); + + describe('when status is valid', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows an information message that the section is not editable', () => { + expect(findIcon().exists()).toBe(true); + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.viewOnlyMessage); + }); + + it('contains an editor', () => { + expect(findEditor().exists()).toBe(true); + }); + + it('editor contains the value provided', () => { + expect(findEditor().props('value')).toBe(mockLintResponse.mergedYaml); + }); + + it('editor is configured for the CI config path', () => { + expect(findEditor().props('fileName')).toBe(mockCiConfigPath); + }); + + it('editor is readonly', () => { + expect(findEditor().props('editorOptions')).toMatchObject({ + readOnly: true, + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js similarity index 96% rename from spec/frontend/pipeline_editor/components/text_editor_spec.js rename to spec/frontend/pipeline_editor/components/editor/text_editor_spec.js index 9bb4bb6c210..3bf5a291c69 100644 --- a/spec/frontend/pipeline_editor/components/text_editor_spec.js +++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js @@ -1,14 +1,14 @@ import { shallowMount } from '@vue/test-utils'; import { EDITOR_READY_EVENT } from '~/editor/constants'; -import TextEditor from '~/pipeline_editor/components/text_editor.vue'; +import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; import { mockCiConfigPath, mockCiYml, mockCommitSha, mockProjectPath, mockProjectNamespace, -} from '../mock_data'; +} from '../../mock_data'; describe('Pipeline Editor | Text editor component', () => { let wrapper; diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 275e2987f38..5f4760054ec 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -1,9 +1,10 @@ import { nextTick } from 'vue'; import { shallowMount, mount } from '@vue/test-utils'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; -import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; +import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; import { mockLintResponse, mockCiYml } from '../mock_data'; @@ -15,6 +16,7 @@ describe('Pipeline editor tabs component', () => { const mockProvide = { glFeatures: { ciConfigVisualizationTab: true, + ciConfigMergedTab: true, }, }; @@ -35,72 +37,102 @@ describe('Pipeline editor tabs component', () => { const findEditorTab = () => wrapper.find('[data-testid="editor-tab"]'); const findLintTab = () => wrapper.find('[data-testid="lint-tab"]'); + const findMergedTab = () => wrapper.find('[data-testid="merged-tab"]'); const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]'); + + const findAlert = () => wrapper.findComponent(GlAlert); const findCiLint = () => wrapper.findComponent(CiLint); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); const findTextEditor = () => wrapper.findComponent(MockTextEditor); + const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview); afterEach(() => { wrapper.destroy(); wrapper = null; }); - describe('tabs', () => { - describe('editor tab', () => { - it('displays editor only after the tab is mounted', async () => { - createComponent({ mountFn: mount }); + describe('editor tab', () => { + it('displays editor only after the tab is mounted', async () => { + createComponent({ mountFn: mount }); - expect(findTextEditor().exists()).toBe(false); + expect(findTextEditor().exists()).toBe(false); - await nextTick(); + await nextTick(); - expect(findTextEditor().exists()).toBe(true); - expect(findEditorTab().exists()).toBe(true); - }); + expect(findTextEditor().exists()).toBe(true); + expect(findEditorTab().exists()).toBe(true); }); + }); - describe('visualization tab', () => { - describe('with feature flag on', () => { - describe('while loading', () => { - beforeEach(() => { - createComponent({ props: { isCiConfigDataLoading: true } }); - }); - - it('displays a loading icon if the lint query is loading', () => { - expect(findLoadingIcon().exists()).toBe(true); - expect(findPipelineGraph().exists()).toBe(false); - }); - }); - describe('after loading', () => { - beforeEach(() => { - createComponent(); - }); - - it('display the tab and visualization', () => { - expect(findVisualizationTab().exists()).toBe(true); - expect(findPipelineGraph().exists()).toBe(true); - }); - }); - }); - - describe('with feature flag off', () => { + describe('visualization tab', () => { + describe('with feature flag on', () => { + describe('while loading', () => { beforeEach(() => { - createComponent({ - provide: { - glFeatures: { ciConfigVisualizationTab: false }, - }, - }); + createComponent({ props: { isCiConfigDataLoading: true } }); }); - it('does not display the tab or component', () => { - expect(findVisualizationTab().exists()).toBe(false); + it('displays a loading icon if the lint query is loading', () => { + expect(findLoadingIcon().exists()).toBe(true); expect(findPipelineGraph().exists()).toBe(false); }); }); + describe('after loading', () => { + beforeEach(() => { + createComponent(); + }); + + it('display the tab and visualization', () => { + expect(findVisualizationTab().exists()).toBe(true); + expect(findPipelineGraph().exists()).toBe(true); + }); + }); }); - describe('lint tab', () => { + describe('with feature flag off', () => { + beforeEach(() => { + createComponent({ + provide: { + glFeatures: { ciConfigVisualizationTab: false }, + }, + }); + }); + + it('does not display the tab or component', () => { + expect(findVisualizationTab().exists()).toBe(false); + expect(findPipelineGraph().exists()).toBe(false); + }); + }); + }); + + describe('lint tab', () => { + describe('while loading', () => { + beforeEach(() => { + createComponent({ props: { isCiConfigDataLoading: true } }); + }); + + it('displays a loading icon if the lint query is loading', () => { + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('does not display the lint component', () => { + expect(findCiLint().exists()).toBe(false); + }); + }); + describe('after loading', () => { + beforeEach(() => { + createComponent(); + }); + + it('display the tab and the lint component', () => { + expect(findLintTab().exists()).toBe(true); + expect(findCiLint().exists()).toBe(true); + }); + }); + }); + + describe('merged tab', () => { + describe('with feature flag on', () => { describe('while loading', () => { beforeEach(() => { createComponent({ props: { isCiConfigDataLoading: true } }); @@ -109,21 +141,43 @@ describe('Pipeline editor tabs component', () => { it('displays a loading icon if the lint query is loading', () => { expect(findLoadingIcon().exists()).toBe(true); }); + }); - it('does not display the lint component', () => { - expect(findCiLint().exists()).toBe(false); + describe('when `mergedYaml` is undefined', () => { + beforeEach(() => { + createComponent({ props: { ciConfigData: {} } }); + }); + + it('show an error message', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts.loadMergedYaml); + }); + + it('does not render the `meged_preview` component', () => { + expect(findMergedPreview().exists()).toBe(false); }); }); + describe('after loading', () => { beforeEach(() => { createComponent(); }); - it('display the tab and the lint component', () => { - expect(findLintTab().exists()).toBe(true); - expect(findCiLint().exists()).toBe(true); + it('display the tab and the merged preview component', () => { + expect(findMergedTab().exists()).toBe(true); + expect(findMergedPreview().exists()).toBe(true); }); }); }); + describe('with feature flag off', () => { + beforeEach(() => { + createComponent({ provide: { glFeatures: { ciConfigMergedTab: false } } }); + }); + + it('does not display the merged tab', () => { + expect(findMergedTab().exists()).toBe(false); + expect(findMergedPreview().exists()).toBe(false); + }); + }); }); }); diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index 3eacc467c51..8e248c11b87 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -54,6 +54,7 @@ export const mockCiConfigQueryResponse = { data: { ciConfig: { errors: [], + mergedYaml: mockCiYml, status: CI_CONFIG_STATUS_VALID, stages: { __typename: 'CiConfigStageConnection', @@ -139,6 +140,8 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => { export const mockLintResponse = { valid: true, + mergedYaml: mockCiYml, + status: CI_CONFIG_STATUS_VALID, errors: [], warnings: [], jobs: [ diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index 7ffdf1815ba..207d1ad9664 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -3,7 +3,7 @@ import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; -import TextEditor from '~/pipeline_editor/components/text_editor.vue'; +import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue'; import httpStatusCodes from '~/lib/utils/http_status'; diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js index 6ef1b3cdd1c..b1c6198659a 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -1,9 +1,11 @@ +import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue'; +import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants'; import { mockLintResponse, mockCiYml } from './mock_data'; @@ -21,9 +23,9 @@ describe('Pipeline editor home wrapper', () => { }); }; - const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorTabs); - const findPipelineEditorTabs = () => wrapper.findComponent(CommitSection); - const findCommitSection = () => wrapper.findComponent(PipelineEditorHeader); + const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); + const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); + const findCommitSection = () => wrapper.findComponent(CommitSection); afterEach(() => { wrapper.destroy(); @@ -43,7 +45,33 @@ describe('Pipeline editor home wrapper', () => { expect(findPipelineEditorTabs().exists()).toBe(true); }); - it('shows the commit section', () => { + it('shows the commit section by default', () => { + expect(findCommitSection().exists()).toBe(true); + }); + }); + + describe('commit form toggle', () => { + beforeEach(() => { + createComponent(); + }); + + it('hides the commit form when in the merged tab', async () => { + expect(findCommitSection().exists()).toBe(true); + + findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB); + await nextTick(); + expect(findCommitSection().exists()).toBe(false); + }); + + it('shows the form again when leaving the merged tab', async () => { + expect(findCommitSection().exists()).toBe(true); + + findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB); + await nextTick(); + expect(findCommitSection().exists()).toBe(false); + + findPipelineEditorTabs().vm.$emit('set-current-tab', VISUALIZE_TAB); + await nextTick(); expect(findCommitSection().exists()).toBe(true); }); }); diff --git a/spec/frontend/tooltips/index_spec.js b/spec/frontend/tooltips/index_spec.js index bff9ee0c7f2..9c03ca8f4c9 100644 --- a/spec/frontend/tooltips/index_spec.js +++ b/spec/frontend/tooltips/index_spec.js @@ -1,4 +1,3 @@ -import jQuery from 'jquery'; import { add, initTooltips, @@ -146,29 +145,4 @@ describe('tooltips/index.js', () => { expect(tooltipsApp.fixTitle).toHaveBeenCalledWith(target); }); - - describe('when glTooltipsEnabled feature flag is disabled', () => { - beforeEach(() => { - window.gon.features.glTooltips = false; - }); - - it.each` - method | methodName | bootstrapParams - ${dispose} | ${'dispose'} | ${'dispose'} - ${fixTitle} | ${'fixTitle'} | ${'_fixTitle'} - ${enable} | ${'enable'} | ${'enable'} - ${disable} | ${'disable'} | ${'disable'} - ${hide} | ${'hide'} | ${'hide'} - ${show} | ${'show'} | ${'show'} - ${add} | ${'init'} | ${{ title: 'the title' }} - `('delegates $methodName to bootstrap tooltip API', ({ method, bootstrapParams }) => { - const elements = jQuery(createTooltipTarget()); - - jest.spyOn(jQuery.fn, 'tooltip'); - - method(elements, bootstrapParams); - - expect(elements.tooltip).toHaveBeenCalledWith(bootstrapParams); - }); - }); }); diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index 00c4a1880de..b5d70af1336 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -73,12 +73,12 @@ RSpec.describe AuthHelper do describe 'enabled_button_based_providers' do before do - allow(helper).to receive(:auth_providers) { [:twitter, :github, :google_oauth2] } + allow(helper).to receive(:auth_providers) { [:twitter, :github, :google_oauth2, :openid_connect] } end context 'all providers are enabled to sign in' do it 'returns all the enabled providers from settings' do - expect(helper.enabled_button_based_providers).to include('twitter', 'github', 'google_oauth2') + expect(helper.enabled_button_based_providers).to include('twitter', 'github', 'google_oauth2', 'openid_connect') end it 'puts google and github in the beginning' do diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb index adc0db2b3ae..2d7089853cf 100644 --- a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb @@ -82,18 +82,6 @@ RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do expect(link).not_to match %r(https?://) expect(link).to eq urls.edit_project_feature_flag_url(project, feature_flag.iid, only_path: true) end - - context 'when feature_flag_contextual_issue feture flag is disabled' do - before do - stub_feature_flags(feature_flag_contextual_issue: false) - end - - it 'does not link the reference' do - doc = reference_filter("See #{reference}") - - expect(doc.css('a').first).to be_nil - end - end end context 'with cross-project / cross-namespace complete reference' do diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb index 1a6a955544a..61950cdd9b0 100644 --- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -33,6 +33,8 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do } end + subject { described_class.new(context) } + before do allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor).to receive(:extract).and_return([group_data]) @@ -44,7 +46,7 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do it 'imports new group into destination group' do group_path = 'my-destination-group' - subject.run(context) + subject.run imported_group = Group.find_by_path(group_path) diff --git a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb index 7c96967971f..63f28916d9a 100644 --- a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb @@ -18,6 +18,8 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do let(:context) { BulkImports::Pipeline::Context.new(entity) } + subject { described_class.new(context) } + def extractor_data(title:, has_next_page:, cursor: nil) data = [ { @@ -46,7 +48,7 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do .and_return(first_page, last_page) end - expect { subject.run(context) }.to change(Label, :count).by(2) + expect { subject.run }.to change(Label, :count).by(2) label = group.labels.order(:created_at).last @@ -61,9 +63,9 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do it 'updates tracker information and runs pipeline again' do data = extractor_data(title: 'label', has_next_page: true, cursor: cursor) - expect(subject).to receive(:run).with(context) + expect(subject).to receive(:run) - subject.after_run(context, data) + subject.after_run(data) tracker = entity.trackers.find_by(relation: :labels) @@ -76,9 +78,9 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do it 'updates tracker information and does not run pipeline' do data = extractor_data(title: 'label', has_next_page: false) - expect(subject).not_to receive(:run).with(context) + expect(subject).not_to receive(:run) - subject.after_run(context, data) + subject.after_run(data) tracker = entity.trackers.find_by(relation: :labels) diff --git a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb index 52208e2b852..9f498f8154f 100644 --- a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb @@ -13,6 +13,8 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) } + subject { described_class.new(context) } + describe '#run' do it 'maps existing users to the imported group' do first_page = member_data(email: member_user1.email, has_next_page: true, cursor: cursor) @@ -24,7 +26,7 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do .and_return(first_page, last_page) end - expect { subject.run(context) }.to change(GroupMember, :count).by(2) + expect { subject.run }.to change(GroupMember, :count).by(2) members = group.members.map { |m| m.slice(:user_id, :access_level) } diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb index 71d1ffdeba3..0404c52b895 100644 --- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb @@ -25,7 +25,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do ] end - subject { described_class.new } + subject { described_class.new(context) } before do allow_next_instance_of(BulkImports::Groups::Extractors::SubgroupsExtractor) do |extractor| @@ -36,7 +36,7 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do end it 'creates entities for the subgroups' do - expect { subject.run(context) }.to change(BulkImports::Entity, :count).by(1) + expect { subject.run }.to change(BulkImports::Entity, :count).by(1) subgroup_entity = BulkImports::Entity.last diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb index 43e12e6e3d7..83d29bb051d 100644 --- a/spec/lib/bulk_imports/importers/group_importer_spec.rb +++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb @@ -42,8 +42,8 @@ RSpec.describe BulkImports::Importers::GroupImporter do end def expect_to_run_pipeline(klass, context:) - expect_next_instance_of(klass) do |pipeline| - expect(pipeline).to receive(:run).with(context) + expect_next_instance_of(klass, context) do |pipeline| + expect(pipeline).to receive(:run) end end end diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index 3f6a172beee..b3a6833de02 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -112,7 +112,7 @@ RSpec.describe BulkImports::Pipeline::Runner do ) end - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run end context 'when exception is raised' do @@ -126,7 +126,7 @@ RSpec.describe BulkImports::Pipeline::Runner do end it 'logs import failure' do - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run failure = entity.failures.first @@ -143,7 +143,7 @@ RSpec.describe BulkImports::Pipeline::Runner do end it 'marks entity as failed' do - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run expect(entity.failed?).to eq(true) end @@ -159,13 +159,13 @@ RSpec.describe BulkImports::Pipeline::Runner do ) end - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run end end context 'when pipeline is not marked to abort on failure' do it 'marks entity as failed' do - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run expect(entity.failed?).to eq(false) end @@ -190,7 +190,7 @@ RSpec.describe BulkImports::Pipeline::Runner do ) end - BulkImports::MyPipeline.new.run(context) + BulkImports::MyPipeline.new(context).run end end end diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index f16396d62c9..692e3236822 100644 --- a/spec/models/ci/daily_build_group_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -97,6 +97,35 @@ RSpec.describe Ci::DailyBuildGroupReportResult do end end + describe '.by_ref_path' do + subject(:coverages) { described_class.by_ref_path(recent_build_group_report_result.ref_path) } + + it 'returns coverages by ref_path' do + expect(coverages).to contain_exactly(recent_build_group_report_result, old_build_group_report_result) + end + end + + describe '.ordered_by_date_and_group_name' do + subject(:coverages) { described_class.ordered_by_date_and_group_name } + + it 'returns coverages ordered by data and group name' do + expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result) + end + end + + describe '.by_dates' do + subject(:coverages) { described_class.by_dates(start_date, end_date) } + + context 'when daily coverages exist during those dates' do + let(:start_date) { 1.day.ago.to_date.to_s } + let(:end_date) { Date.current.to_s } + + it 'returns coverages' do + expect(coverages).to contain_exactly(recent_build_group_report_result) + end + end + end + describe '.with_coverage' do subject { described_class.with_coverage } diff --git a/spec/models/user_status_spec.rb b/spec/models/user_status_spec.rb index 2c0664bd165..51dd91149cc 100644 --- a/spec/models/user_status_spec.rb +++ b/spec/models/user_status_spec.rb @@ -17,4 +17,34 @@ RSpec.describe UserStatus do expect { status.user.destroy }.to change { described_class.count }.from(1).to(0) end + + describe '#clear_status_after=' do + it 'sets clear_status_at' do + status = build(:user_status) + + freeze_time do + status.clear_status_after = '8_hours' + + expect(status.clear_status_at).to be_like_time(8.hours.from_now) + end + end + + it 'unsets clear_status_at' do + status = build(:user_status, clear_status_at: 8.hours.from_now) + + status.clear_status_after = nil + + expect(status.clear_status_at).to be_nil + end + + context 'when unknown clear status is given' do + it 'unsets clear_status_at' do + status = build(:user_status, clear_status_at: 8.hours.from_now) + + status.clear_status_after = 'unknown' + + expect(status.clear_status_at).to be_nil + end + end + end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 66d6c29cd4d..bd8e4a59195 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -2866,6 +2866,47 @@ RSpec.describe API::Users do expect(response).to have_gitlab_http_status(:success) expect(user.reload.status).to be_nil end + + context 'when clear_status_after is given' do + it 'sets the clear_status_at column' do + freeze_time do + expected_clear_status_at = 3.hours.from_now + + put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: '3_hours' } + + expect(response).to have_gitlab_http_status(:success) + expect(user.status.reload.clear_status_at).to be_within(1.minute).of(expected_clear_status_at) + expect(Time.parse(json_response["clear_status_at"])).to be_within(1.minute).of(expected_clear_status_at) + end + end + + it 'unsets the clear_status_at column' do + user.create_status!(clear_status_at: 5.hours.ago) + + put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: nil } + + expect(response).to have_gitlab_http_status(:success) + expect(user.status.reload.clear_status_at).to be_nil + end + + it 'raises error when unknown status value is given' do + put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: 'wrong' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + context 'when the clear_status_with_quick_options feature flag is disabled' do + before do + stub_feature_flags(clear_status_with_quick_options: false) + end + + it 'does not persist clear_status_at' do + put api('/user/status', user), params: { emoji: 'smirk', message: 'hello world', clear_status_after: '3_hours' } + + expect(user.status.reload.clear_status_at).to be_nil + end + end + end end describe 'POST /users/:user_id/personal_access_tokens' do diff --git a/spec/services/users/batch_status_cleaner_service_spec.rb b/spec/services/users/batch_status_cleaner_service_spec.rb new file mode 100644 index 00000000000..46a004542d8 --- /dev/null +++ b/spec/services/users/batch_status_cleaner_service_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::BatchStatusCleanerService do + let_it_be(:user_status_1) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 1.year.ago) } + let_it_be(:user_status_2) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 1.year.from_now) } + let_it_be(:user_status_3) { create(:user_status, emoji: 'coffee', message: 'msg1', clear_status_at: 2.years.ago) } + let_it_be(:user_status_4) { create(:user_status, emoji: 'coffee', message: 'msg1') } + + subject(:result) { described_class.execute } + + it 'cleans up scheduled user statuses' do + expect(result[:deleted_rows]).to eq(2) + + deleted_statuses = UserStatus.where(user_id: [user_status_1.user_id, user_status_3.user_id]) + expect(deleted_statuses).to be_empty + end + + it 'does not affect rows with future clear_status_at' do + expect { result }.not_to change { user_status_2.reload } + end + + it 'does not affect rows without clear_status_at' do + expect { result }.not_to change { user_status_4.reload } + end + + describe 'batch_size' do + it 'clears status in batches' do + result = described_class.execute(batch_size: 1) + + expect(result[:deleted_rows]).to eq(1) + + result = described_class.execute(batch_size: 1) + + expect(result[:deleted_rows]).to eq(1) + + result = described_class.execute(batch_size: 1) + + expect(result[:deleted_rows]).to eq(0) + end + end +end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 62da9e15259..89d30688b5c 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -154,6 +154,15 @@ RSpec.shared_examples 'wiki model' do it 'returns true' do expect(subject.empty?).to be(true) end + + context 'when the repository does not exist' do + let(:wiki_container) { wiki_container_without_repo } + + it 'returns true and does not create the repo' do + expect(subject.empty?).to be(true) + expect(wiki.repository_exists?).to be false + end + end end context 'when the wiki has pages' do diff --git a/spec/workers/user_status_cleanup/batch_worker_spec.rb b/spec/workers/user_status_cleanup/batch_worker_spec.rb new file mode 100644 index 00000000000..2fd84d0e085 --- /dev/null +++ b/spec/workers/user_status_cleanup/batch_worker_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe UserStatusCleanup::BatchWorker do + include_examples 'an idempotent worker' do + subject do + perform_multiple([], worker: described_class.new) + end + end + + describe '#perform' do + subject(:run_worker) { described_class.new.perform } + + context 'when no records are scheduled for cleanup' do + let(:user_status) { create(:user_status) } + + it 'does nothing' do + expect { run_worker }.not_to change { user_status.reload } + end + end + + it 'cleans up the records' do + user_status_1 = create(:user_status, clear_status_at: 1.year.ago) + user_status_2 = create(:user_status, clear_status_at: 2.years.ago) + + run_worker + + deleted_statuses = UserStatus.where(user_id: [user_status_1.user_id, user_status_2.user_id]) + expect(deleted_statuses).to be_empty + end + end +end