diff options
132 files changed, 1023 insertions, 982 deletions
diff --git a/.dockerignore b/.dockerignore index e145f368cb1..0782627230a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -49,7 +49,6 @@ /lib/registry/ /lib/policy/ /lib/feature/ -/lib/flowdock/ /lib/generators/ /lib/gitaly/ /lib/api/ diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml index 63379b6f550..c1ba066a181 100644 --- a/.rubocop_todo/gitlab/strong_memoize_attr.yml +++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml @@ -565,7 +565,6 @@ Gitlab/StrongMemoizeAttr: - 'lib/container_registry/client.rb' - 'lib/container_registry/gitlab_api_client.rb' - 'lib/container_registry/tag.rb' - - 'lib/flowdock/git/builder.rb' - 'lib/gitlab/alert_management/alert_status_counts.rb' - 'lib/gitlab/alert_management/payload/base.rb' - 'lib/gitlab/alert_management/payload/managed_prometheus.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 8fdae5e8660..b6056ae4f50 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -391,7 +391,6 @@ Layout/LineLength: - 'app/models/integrations/emails_on_push.rb' - 'app/models/integrations/ewm.rb' - 'app/models/integrations/external_wiki.rb' - - 'app/models/integrations/flowdock.rb' - 'app/models/integrations/hangouts_chat.rb' - 'app/models/integrations/harbor.rb' - 'app/models/integrations/jenkins.rb' diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml index 70d0786173c..73b8a354a58 100644 --- a/.rubocop_todo/layout/space_in_lambda_literal.yml +++ b/.rubocop_todo/layout/space_in_lambda_literal.yml @@ -3,23 +3,6 @@ Layout/SpaceInLambdaLiteral: Details: grace period Exclude: - - 'app/controllers/concerns/issuable_actions.rb' - - 'app/controllers/projects/ci/daily_build_group_report_results_controller.rb' - - 'app/controllers/projects/merge_requests_controller.rb' - - 'app/finders/releases/group_releases_finder.rb' - - 'app/graphql/mutations/ci/runner/update.rb' - - 'app/models/abuse_report.rb' - - 'app/models/alert_management/alert.rb' - - 'app/models/alert_management/http_integration.rb' - - 'app/models/analytics/cycle_analytics/aggregation.rb' - - 'app/models/analytics/usage_trends/measurement.rb' - - 'app/models/application_setting.rb' - - 'app/models/audit_event.rb' - - 'app/models/award_emoji.rb' - - 'app/models/board_group_recent_visit.rb' - - 'app/models/board_project_recent_visit.rb' - - 'app/models/bulk_import.rb' - - 'app/models/bulk_imports/entity.rb' - 'app/models/bulk_imports/tracker.rb' - 'app/models/ci/build.rb' - 'app/models/ci/daily_build_group_report_result.rb' diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index 1e4356aed17..c1ba754edca 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -105,7 +105,6 @@ Style/FormatString: - 'app/models/integrations/emails_on_push.rb' - 'app/models/integrations/ewm.rb' - 'app/models/integrations/external_wiki.rb' - - 'app/models/integrations/flowdock.rb' - 'app/models/integrations/hangouts_chat.rb' - 'app/models/integrations/irker.rb' - 'app/models/integrations/jenkins.rb' @@ -281,7 +280,6 @@ Style/FormatString: - 'lib/api/helpers/packages/conan/api_helpers.rb' - 'lib/bulk_imports/network_error.rb' - 'lib/bulk_imports/users_mapper.rb' - - 'lib/flowdock/git/builder.rb' - 'lib/gitlab/bitbucket_server_import/importer.rb' - 'lib/gitlab/checks/push_file_count_check.rb' - 'lib/gitlab/ci/ansi2json/line.rb' diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml index fee5decab22..2f042829e35 100644 --- a/.rubocop_todo/style/percent_literal_delimiters.yml +++ b/.rubocop_todo/style/percent_literal_delimiters.yml @@ -87,7 +87,6 @@ Style/PercentLiteralDelimiters: - 'app/models/integrations/emails_on_push.rb' - 'app/models/integrations/external_wiki.rb' - 'app/models/integrations/field.rb' - - 'app/models/integrations/flowdock.rb' - 'app/models/integrations/jenkins.rb' - 'app/models/integrations/jira.rb' - 'app/models/integrations/packagist.rb' @@ -485,7 +484,6 @@ Style/PercentLiteralDelimiters: - 'lib/bitbucket/representation/issue.rb' - 'lib/container_registry/path.rb' - 'lib/feature.rb' - - 'lib/flowdock/git/builder.rb' - 'lib/generators/gitlab/usage_metric_definition_generator.rb' - 'lib/generators/gitlab/usage_metric_generator.rb' - 'lib/gitlab.rb' @@ -264,9 +264,6 @@ gem 'discordrb-webhooks', '~> 3.4', require: false gem 'jira-ruby', '~> 2.1.4' gem 'atlassian-jwt', '~> 0.2.0' -# Flowdock integration -gem 'flowdock', '~> 0.7' - # Slack integration gem 'slack-messenger', '~> 2.3.4' diff --git a/Gemfile.checksum b/Gemfile.checksum index 6c03431e3b0..20cc921205c 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -178,7 +178,6 @@ {"name":"flipper","version":"0.25.0","platform":"ruby","checksum":"ccb2776752b8378bc994c9d873ccde290c090341940761b873494695ee697add"}, {"name":"flipper-active_record","version":"0.25.0","platform":"ruby","checksum":"85a5c99465e2cc6a09e91931a9998b0dbd463cd6c80dd513129377132e3eb67f"}, {"name":"flipper-active_support_cache_store","version":"0.25.0","platform":"ruby","checksum":"7282bf994b08d1a076b65c6f3b51e3dc04fcb00fa6e7b20089e60db25c7b531b"}, -{"name":"flowdock","version":"0.7.1","platform":"ruby","checksum":"cfa95b2ac96e5f883f6e419d7a891f76cfcc17a28c416b6b714bbdffc8dbd912"}, {"name":"fog-aliyun","version":"0.3.3","platform":"ruby","checksum":"d0aa317f7c1473a1d684fff51699f216bb9cb78b9ee9ce55a81c9bcc93fb85ee"}, {"name":"fog-aws","version":"3.15.0","platform":"ruby","checksum":"09752931ea0c6165b018e1a89253248d86b246645086ccf19bc44fabe3381e8c"}, {"name":"fog-core","version":"2.1.0","platform":"ruby","checksum":"53e5d793554d7080d015ef13cd44b54027e421d924d9dba4ce3d83f95f37eda9"}, diff --git a/Gemfile.lock b/Gemfile.lock index 4c3dc0067cf..6d478b08a0e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -484,9 +484,6 @@ GEM flipper-active_support_cache_store (0.25.0) activesupport (>= 4.2, < 8) flipper (~> 0.25.0) - flowdock (0.7.1) - httparty (~> 0.7) - multi_json fog-aliyun (0.3.3) fog-core fog-json @@ -1649,7 +1646,6 @@ DEPENDENCIES flipper (~> 0.25.0) flipper-active_record (~> 0.25.0) flipper-active_support_cache_store (~> 0.25.0) - flowdock (~> 0.7) fog-aliyun (~> 0.3) fog-aws (~> 3.15) fog-core (= 2.1.0) diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue index 794bf4cc51c..5bb310afac7 100644 --- a/app/assets/javascripts/batch_comments/components/draft_note.vue +++ b/app/assets/javascripts/batch_comments/components/draft_note.vue @@ -85,32 +85,25 @@ export default { }; </script> <template> - <article - class="draft-note-component note-wrapper" - @mouseenter="handleMouseEnter(draft)" - @mouseleave="handleMouseLeave(draft)" + <noteable-note + :note="draft" + :line="line" + :discussion-root="true" + :class="{ 'gl-mb-0!': glFeatures.mrReviewSubmitComment }" + class="draft-note-component draft-note" + @handleEdit="handleEditing" + @cancelForm="handleNotEditing" + @updateSuccess="handleNotEditing" + @handleDeleteNote="deleteDraft" + @handleUpdateNote="update" + @toggleResolveStatus="toggleResolveDiscussion(draft.id)" + @mouseenter.native="handleMouseEnter(draft)" + @mouseleave.native="handleMouseLeave(draft)" > - <ul class="notes draft-notes"> - <noteable-note - :note="draft" - :line="line" - :discussion-root="true" - :class="{ 'gl-mb-0!': glFeatures.mrReviewSubmitComment }" - class="draft-note" - @handleEdit="handleEditing" - @cancelForm="handleNotEditing" - @updateSuccess="handleNotEditing" - @handleDeleteNote="deleteDraft" - @handleUpdateNote="update" - @toggleResolveStatus="toggleResolveDiscussion(draft.id)" - > - <template #note-header-info> - <gl-badge variant="warning" class="gl-mr-2">{{ __('Pending') }}</gl-badge> - </template> - </noteable-note> - </ul> - - <template v-if="!isEditingDraft"> + <template #note-header-info> + <gl-badge variant="warning" class="gl-mr-2">{{ __('Pending') }}</gl-badge> + </template> + <template v-if="!isEditingDraft" #after-note-body> <div v-if="draftCommands" v-safe-html:[$options.safeHtmlConfig]="draftCommands" @@ -134,5 +127,5 @@ export default { </gl-button> </p> </template> - </article> + </noteable-note> </template> diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue index 1e44d5fccc2..2d0f702c636 100644 --- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue +++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue @@ -6,6 +6,7 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import RunnerName from '../runner_name.vue'; import RunnerTags from '../runner_tags.vue'; import RunnerTypeBadge from '../runner_type_badge.vue'; +import RunnerJobStatusBadge from '../runner_job_status_badge.vue'; import { formatJobCount } from '../../utils'; import { @@ -25,6 +26,7 @@ export default { RunnerName, RunnerTags, RunnerTypeBadge, + RunnerJobStatusBadge, RunnerUpgradeStatusIcon: () => import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'), TooltipOnTruncate, @@ -81,6 +83,8 @@ export default { </div> <div> + <runner-job-status-badge :job-status="runner.jobExecutionStatus" /> + <runner-summary-field icon="clock"> <gl-sprintf :message="$options.i18n.I18N_LAST_CONTACT_LABEL"> <template #timeAgo> diff --git a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue new file mode 100644 index 00000000000..176fe57eebb --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue @@ -0,0 +1,54 @@ +<script> +import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; +import { + I18N_JOB_STATUS_RUNNING, + I18N_JOB_STATUS_IDLE, + JOB_STATUS_RUNNING, + JOB_STATUS_IDLE, +} from '../constants'; + +export default { + components: { + GlBadge, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + jobStatus: { + required: false, + default: null, + type: String, + }, + }, + computed: { + badge() { + switch (this.jobStatus) { + case JOB_STATUS_RUNNING: + return { + classes: 'gl-text-blue-600! gl-border gl-border-blue-600!', + label: I18N_JOB_STATUS_RUNNING, + }; + case JOB_STATUS_IDLE: + return { + classes: 'gl-text-gray-700! gl-border gl-border-gray-500!', + label: I18N_JOB_STATUS_IDLE, + }; + default: + return null; + } + }, + }, +}; +</script> +<template> + <gl-badge + v-if="badge" + size="sm" + class="gl-mr-3 gl-bg-transparent!" + variant="muted" + :class="badge.classes" + > + {{ badge.label }} + </gl-badge> +</template> diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index dfc5f0c4152..686f0fde9d7 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -32,6 +32,10 @@ export const I18N_STATUS_NEVER_CONTACTED = s__('Runners|Never contacted'); export const I18N_STATUS_OFFLINE = s__('Runners|Offline'); export const I18N_STATUS_STALE = s__('Runners|Stale'); +// Executor Status +export const I18N_JOB_STATUS_RUNNING = s__('Runners|Running'); +export const I18N_JOB_STATUS_IDLE = s__('Runners|Idle'); + // Status help popover export const I18N_STATUS_POPOVER_TITLE = s__('Runners|Runner statuses'); @@ -134,6 +138,11 @@ export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED'; export const STATUS_OFFLINE = 'OFFLINE'; export const STATUS_STALE = 'STALE'; +// CiRunnerJobExecutionStatus + +export const JOB_STATUS_RUNNING = 'RUNNING'; +export const JOB_STATUS_IDLE = 'IDLE'; + // CiRunnerAccessLevel export const ACCESS_LEVEL_NOT_PROTECTED = 'NOT_PROTECTED'; diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql index 0dff011daaa..6f72509f599 100644 --- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql @@ -12,6 +12,7 @@ fragment ListItemShared on CiRunner { createdAt contactedAt status(legacyMode: null) + jobExecutionStatus userPermissions { updateRunner deleteRunner diff --git a/app/assets/javascripts/clusters_list/components/agent_token.vue b/app/assets/javascripts/clusters_list/components/agent_token.vue index 4dd6d84566c..93c37226a09 100644 --- a/app/assets/javascripts/clusters_list/components/agent_token.vue +++ b/app/assets/javascripts/clusters_list/components/agent_token.vue @@ -1,22 +1,24 @@ <script> -import { GlAlert, GlFormInputGroup, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlAlert, GlFormInputGroup, GlLink, GlSprintf, GlIcon } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue'; import { generateAgentRegistrationCommand } from '../clusters_util'; -import { I18N_AGENT_TOKEN } from '../constants'; +import { I18N_AGENT_TOKEN, HELM_VERSION_POLICY_URL } from '../constants'; export default { i18n: I18N_AGENT_TOKEN, advancedInstallPath: helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation-method', }), + HELM_VERSION_POLICY_URL, components: { GlAlert, CodeBlock, GlFormInputGroup, GlLink, GlSprintf, + GlIcon, ModalCopyButton, }, inject: ['kasAddress', 'kasVersion'], @@ -77,6 +79,11 @@ export default { <p> {{ $options.i18n.basicInstallBody }} + <gl-sprintf :message="$options.i18n.helmVersionText"> + <template #link="{ content }" + ><gl-link :href="$options.HELM_VERSION_POLICY_URL" target="_blank" + >{{ content }} <gl-icon name="external-link" :size="12" /></gl-link></template + ></gl-sprintf> </p> <p class="gl-display-flex gl-align-items-flex-start"> diff --git a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue index bde76c46b4b..365e0384d87 100644 --- a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue +++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue @@ -1,23 +1,13 @@ <script> -import { - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlDropdownText, - GlSearchBoxByType, - GlSprintf, -} from '@gitlab/ui'; +import { GlCollapsibleListbox, GlButton, GlSprintf } from '@gitlab/ui'; import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants'; export default { name: 'AvailableAgentsDropdown', i18n: I18N_AVAILABLE_AGENTS_DROPDOWN, components: { - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlDropdownText, - GlSearchBoxByType, + GlCollapsibleListbox, + GlButton, GlSprintf, }, props: { @@ -46,13 +36,21 @@ export default { return this.selectedAgent; }, + dropdownItems() { + return this.availableAgents.map((agent) => { + return { + value: agent, + text: agent, + }; + }); + }, shouldRenderCreateButton() { return this.searchTerm && !this.availableAgents.includes(this.searchTerm); }, filteredResults() { const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.availableAgents.filter((resultString) => - resultString.toLowerCase().includes(lowerCasedSearchTerm), + return this.dropdownItems.filter((item) => + item.value.toLowerCase().includes(lowerCasedSearchTerm), ); }, }, @@ -60,59 +58,48 @@ export default { selectAgent(agent) { this.$emit('agentSelected', agent); this.selectedAgent = agent; - this.clearSearch(); - }, - isSelected(agent) { - return this.selectedAgent === agent; - }, - clearSearch() { - this.searchTerm = ''; - }, - focusSearch() { - this.$refs.searchInput.focusInput(); - }, - handleShow() { - this.clearSearch(); - this.focusSearch(); + + this.$refs.dropdown.closeAndFocus(); }, onKeyEnter() { if (!this.searchTerm?.length) { return; } - this.$refs.dropdown.hide(); this.selectAgent(this.searchTerm); }, + searchAgent(searchQuery) { + this.searchTerm = searchQuery; + }, }, }; </script> <template> - <gl-dropdown ref="dropdown" :text="dropdownText" :loading="isRegistering" @shown="handleShow"> - <template #header> - <gl-search-box-by-type - ref="searchInput" - v-model.trim="searchTerm" - @keydown.enter.stop.prevent="onKeyEnter" - /> - </template> - <gl-dropdown-item - v-for="agent in filteredResults" - :key="agent" - :is-checked="isSelected(agent)" - is-check-item - @click="selectAgent(agent)" + <div @keydown.enter.stop.prevent="onKeyEnter"> + <gl-collapsible-listbox + ref="dropdown" + v-model="selectedAgent" + class="gl-w-full" + toggle-class="select-agent-dropdown" + :items="filteredResults" + :toggle-text="dropdownText" + :loading="isRegistering" + :searchable="true" + :no-results-text="$options.i18n.noResults" + @search="searchAgent" + @select="selectAgent" > - {{ agent }} - </gl-dropdown-item> - <gl-dropdown-text v-if="!filteredResults.length" ref="noMatchingResults">{{ - $options.i18n.noResults - }}</gl-dropdown-text> - <template v-if="shouldRenderCreateButton"> - <gl-dropdown-divider /> - <gl-dropdown-item data-testid="create-config-button" @click="selectAgent(searchTerm)"> - <gl-sprintf :message="$options.i18n.createButton"> - <template #searchTerm>{{ searchTerm }}</template> - </gl-sprintf> - </gl-dropdown-item> - </template> - </gl-dropdown> + <template v-if="shouldRenderCreateButton" #footer> + <gl-button + category="tertiary" + class="gl-justify-content-start! gl-border-t-1! gl-border-t-solid gl-border-t-gray-200 gl-pl-7! gl-rounded-top-left-none! gl-rounded-top-right-none!" + :class="{ 'gl-mt-3': !filteredResults.length }" + @click="selectAgent(searchTerm)" + > + <gl-sprintf :message="$options.i18n.createButton"> + <template #searchTerm>{{ searchTerm }}</template> + </gl-sprintf> + </gl-button> + </template> + </gl-collapsible-listbox> + </div> </template> diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js index 03530a0314b..615754459d6 100644 --- a/app/assets/javascripts/clusters_list/constants.js +++ b/app/assets/javascripts/clusters_list/constants.js @@ -101,6 +101,9 @@ export const I18N_AGENT_TOKEN = { basicInstallBody: s__( 'ClusterAgents|From a terminal, connect to your cluster and run this command. The token is included in the command.', ), + helmVersionText: s__( + 'ClusterAgents|Use a Helm version compatible with your Kubernetes version (see %{linkStart}Helm version support policy%{linkEnd}).', + ), advancedInstallTitle: s__('ClusterAgents|Advanced installation methods'), advancedInstallBody: s__( @@ -108,6 +111,8 @@ export const I18N_AGENT_TOKEN = { ), }; +export const HELM_VERSION_POLICY_URL = 'https://helm.sh/docs/topics/version_skew/'; + export const I18N_AGENT_MODAL = { registerAgentButton: s__('ClusterAgents|Register'), close: __('Close'), diff --git a/app/assets/javascripts/diffs/components/diff_code_quality.vue b/app/assets/javascripts/diffs/components/diff_code_quality.vue index 78f34bf355e..11aa856619b 100644 --- a/app/assets/javascripts/diffs/components/diff_code_quality.vue +++ b/app/assets/javascripts/diffs/components/diff_code_quality.vue @@ -1,8 +1,12 @@ <script> import { GlButton, GlIcon } from '@gitlab/ui'; import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants'; +import { NEW_CODE_QUALITY_FINDINGS } from '../i18n'; export default { + i18n: { + newFindings: NEW_CODE_QUALITY_FINDINGS, + }, components: { GlButton, GlIcon }, props: { codeQuality: { @@ -22,22 +26,33 @@ export default { </script> <template> - <div data-testid="diff-codequality" class="gl-relative"> - <ul - class="gl-list-style-none gl-mb-0 gl-p-0 codequality-findings-list gl-border-top-1 gl-border-bottom-1 gl-bg-gray-10" + <div + data-testid="diff-codequality" + class="gl-relative codequality-findings-list gl-border-top-1 gl-border-bottom-1 gl-bg-gray-10 gl-pl-5 gl-pt-4 gl-pb-4" + > + <h4 + data-testid="diff-codequality-findings-heading" + class="gl-mt-0 gl-mb-0 gl-font-base gl-font-regular" > + {{ $options.i18n.newFindings }} + </h4> + <ul class="gl-list-style-none gl-mb-0 gl-p-0"> <li v-for="finding in codeQuality" :key="finding.description" - class="gl-pt-1 gl-pb-1 gl-pl-3 gl-border-solid gl-border-bottom-0 gl-border-right-0 gl-border-1 gl-border-gray-100 gl-font-regular" + class="gl-pt-1 gl-pb-1 gl-font-regular gl-display-flex" > - <gl-icon - :size="12" - :name="severityIcon(finding.severity)" - :class="severityClass(finding.severity)" - class="codequality-severity-icon" - /> - {{ finding.description }} + <span class="gl-mr-3"> + <gl-icon + :size="12" + :name="severityIcon(finding.severity)" + :class="severityClass(finding.severity)" + class="codequality-severity-icon" + /> + </span> + <span> + <span class="severity-copy">{{ finding.severity }}</span> - {{ finding.description }} + </span> </li> </ul> <gl-button diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue index 4a5a626af8d..aa9a17d18e3 100644 --- a/app/assets/javascripts/diffs/components/diff_view.vue +++ b/app/assets/javascripts/diffs/components/diff_view.vue @@ -303,7 +303,11 @@ export default { class="diff-td notes-content parallel old" > <div v-for="draft in lineDrafts(line, 'left')" :key="draft.id" class="content"> - <draft-note :draft="draft" :line="line.left" /> + <article class="note-wrapper"> + <ul class="notes draft-notes"> + <draft-note :draft="draft" :line="line.left" /> + </ul> + </article> </div> </div> <div @@ -311,7 +315,11 @@ export default { class="diff-td notes-content parallel new" > <div v-for="draft in lineDrafts(line, 'right')" :key="draft.id" class="content"> - <draft-note :draft="draft" :line="line.right" /> + <article class="note-wrapper"> + <ul class="notes draft-notes"> + <draft-note :draft="draft" :line="line.right" /> + </ul> + </article> </div> </div> </div> diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js index f7f4aad3ad0..b6978689f7f 100644 --- a/app/assets/javascripts/diffs/i18n.js +++ b/app/assets/javascripts/diffs/i18n.js @@ -49,3 +49,5 @@ export const CONFLICT_TEXT = { }; export const HIDE_COMMENTS = __('Hide comments'); + +export const NEW_CODE_QUALITY_FINDINGS = __('New code quality findings'); diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index b668d6ec182..fe1a2f0da2a 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -307,7 +307,7 @@ export default { :draft="draftForDiscussion(discussion.reply_id)" :line="line" /> - <div + <li v-else-if="canShowReplyActions && showReplies" :class="{ 'is-replying': isReplying }" class="discussion-reply-holder gl-border-t-0! clearfix" @@ -334,7 +334,7 @@ export default { @cancelForm="cancelReplyForm" /> <note-signed-out-widget v-if="!isLoggedIn" /> - </div> + </li> </template> </discussion-notes> </component> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index b3c124e511f..99809013bf6 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -443,7 +443,7 @@ export default { </gl-avatar-link> </div> - <div v-else-if="!isDraft" class="timeline-avatar gl-float-left"> + <div v-else class="timeline-avatar gl-float-left"> <gl-avatar-link :href="author.path"> <gl-avatar :src="author.avatar_url" @@ -516,6 +516,9 @@ export default { @handleFormUpdate="formUpdateHandler" @cancelForm="formCancelHandler" /> + <div class="timeline-discussion-body-footer"> + <slot name="after-note-body"></slot> + </div> </div> </div> </timeline-entry-item> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue index 5e1172ad835..7af8dcb4e3e 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue @@ -58,11 +58,21 @@ export default { <collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" /> <div class="value hide-collapsed"> - <template v-if="hasNoUsers"> - <span class="no-value"> - {{ __('None') }} - </span> - </template> + <span v-if="hasNoUsers" class="no-value" data-testid="no-value"> + {{ __('None') }} + <template v-if="editable"> + - + <button + type="button" + class="gl-button btn-link gl-reset-color!" + data-testid="assign-yourself" + data-qa-selector="assign_yourself_button" + @click="assignSelf" + > + {{ __('assign yourself') }} + </button> + </template> + </span> <uncollapsed-reviewer-list v-else diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index 9d55daaddd8..faa36f3d8d2 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -143,6 +143,13 @@ export default { eventHub.$off('sidebar.saveReviewers', this.saveReviewers); }, methods: { + reviewBySelf() { + // Notify gl dropdown that we are now assigning to current user + this.$el.parentElement.dispatchEvent(new Event('assignYourself')); + + this.mediator.addSelfReview(); + this.saveReviewers(); + }, saveReviewers() { this.loading = true; @@ -181,6 +188,7 @@ export default { :editable="canUpdate" :issuable-type="issuableType" @request-review="requestReview" + @assign-self="reviewBySelf" /> </div> </template> diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 159d3eff276..c6a66ab2275 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -31,6 +31,9 @@ export default class SidebarMediator { assignYourself() { this.store.addAssignee(this.store.currentUser); } + addSelfReview() { + this.store.addReviewer(this.store.currentUser); + } async saveAssignees(field) { const selected = this.store.assignees.map((u) => u.id); diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 9822a999ff3..c871489fc07 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -15,7 +15,6 @@ import { s__ } from '~/locale'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/url_utility'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import { i18n, @@ -25,7 +24,6 @@ import { WIDGET_TYPE_START_AND_DUE_DATE, WIDGET_TYPE_WEIGHT, WIDGET_TYPE_HIERARCHY, - WORK_ITEM_VIEWED_STORAGE_KEY, WIDGET_TYPE_MILESTONE, WIDGET_TYPE_ITERATION, WORK_ITEM_TYPE_VALUE_ISSUE, @@ -49,7 +47,6 @@ import WorkItemDueDate from './work_item_due_date.vue'; import WorkItemAssignees from './work_item_assignees.vue'; import WorkItemLabels from './work_item_labels.vue'; import WorkItemMilestone from './work_item_milestone.vue'; -import WorkItemInformation from './work_item_information.vue'; export default { i18n, @@ -72,8 +69,6 @@ export default { WorkItemTitle, WorkItemState, WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'), - WorkItemInformation, - LocalStorageSync, WorkItemTypeIcon, WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'), WorkItemMilestone, @@ -108,7 +103,6 @@ export default { error: undefined, updateError: undefined, workItem: {}, - showInfoBanner: true, updateInProgress: false, }; }, @@ -276,18 +270,10 @@ export default { }; }, }, - beforeDestroy() { - /** make sure that if the user has not even dismissed the alert , - * should no be able to see the information next time and update the local storage * */ - this.dismissBanner(); - }, methods: { isWidgetPresent(type) { return this.workItem?.widgets?.find((widget) => widget.type === type); }, - dismissBanner() { - this.showInfoBanner = false; - }, toggleConfidentiality(confidentialStatus) { this.updateInProgress = true; let updateMutation = updateWorkItemMutation; @@ -341,7 +327,6 @@ export default { document.title = s__('404|Not found'); }, }, - WORK_ITEM_VIEWED_STORAGE_KEY, WORK_ITEM_TYPE_VALUE_OBJECTIVE, }; </script> @@ -431,16 +416,6 @@ export default { @click="$emit('close')" /> </div> - <local-storage-sync - v-model="showInfoBanner" - :storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY" - > - <work-item-information - v-if="showInfoBanner && !error" - :show-info-banner="showInfoBanner" - @work-item-banner-dismissed="dismissBanner" - /> - </local-storage-sync> <work-item-title v-if="workItem.title" :work-item-id="workItem.id" @@ -485,7 +460,7 @@ export default { :work-item-type="workItemType" @error="updateError = $event" /> - <template v-if="workItemsMvc2Enabled"> + <template v-if="workItemsMvcEnabled"> <work-item-milestone v-if="workItemMilestone" :work-item-id="workItem.id" diff --git a/app/assets/javascripts/work_items/components/work_item_information.vue b/app/assets/javascripts/work_items/components/work_item_information.vue deleted file mode 100644 index ce75cc98a75..00000000000 --- a/app/assets/javascripts/work_items/components/work_item_information.vue +++ /dev/null @@ -1,53 +0,0 @@ -<script> -import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { helpPagePath } from '~/helpers/help_page_helper'; - -export default { - i18n: { - learnTasksLinkText: s__('WorkItem|Learn about tasks.'), - tasksInformationTitle: s__('WorkItem|Introducing tasks'), - tasksInformationBody: s__( - 'WorkItem|Use tasks to break down your work in an issue into smaller pieces. %{learnMoreLink}', - ), - }, - helpPageLinks: { - tasksDocLinkPath: helpPagePath('user/tasks'), - }, - components: { - GlAlert, - GlSprintf, - GlLink, - }, - props: { - showInfoBanner: { - type: Boolean, - required: false, - default: true, - }, - }, - emits: ['work-item-banner-dismissed'], -}; -</script> - -<template> - <section class="gl-display-block gl-mb-2"> - <gl-alert - v-if="showInfoBanner" - variant="tip" - :title="$options.i18n.tasksInformationTitle" - data-testid="work-item-information" - class="gl-mt-3" - @dismiss="$emit('work-item-banner-dismissed')" - > - <gl-sprintf :message="$options.i18n.tasksInformationBody"> - <template #learnMoreLink> - <gl-link :href="$options.helpPageLinks.tasksDocLinkPath">{{ - $options.i18n.learnTasksLinkText - }}</gl-link> - </template> - ></gl-sprintf - > - </gl-alert> - </section> -</template> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index be1a1584a91..4fcdd19784d 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -168,7 +168,7 @@ export default { return this.parentMilestone?.id; }, associateMilestone() { - return this.parentMilestoneId && this.workItemsMvc2Enabled; + return this.parentMilestoneId && this.workItemsMvcEnabled; }, isSubmitButtonDisabled() { return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0; diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index e4a50de88e9..939cc416b9e 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -20,8 +20,6 @@ export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY'; export const WIDGET_TYPE_MILESTONE = 'MILESTONE'; export const WIDGET_TYPE_ITERATION = 'ITERATION'; -export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner'; - export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT'; export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE'; export const WORK_ITEM_TYPE_ENUM_TASK = 'TASK'; diff --git a/app/assets/stylesheets/page_bundles/clusters.scss b/app/assets/stylesheets/page_bundles/clusters.scss index 2f5c40de6e4..4d75159e87a 100644 --- a/app/assets/stylesheets/page_bundles/clusters.scss +++ b/app/assets/stylesheets/page_bundles/clusters.scss @@ -24,3 +24,9 @@ .cluster-button-container:focus-within { @include gl-focus; } + +.select-agent-dropdown { + .gl-button-text { + @include gl-flex-grow-1; + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 28bb1c7a5e7..75d52424fd9 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -60,6 +60,10 @@ $system-note-svg-size: 1rem; padding: $gl-padding-4 $gl-padding-8; } + &.draft-note .timeline-content:not(.flash-container) { + border: 0; + } + .note-header-info { min-height: 2rem; display: flex; @@ -94,6 +98,7 @@ $system-note-svg-size: 1rem; } &.draft-note .timeline-content:not(.flash-container) { + margin-left: 0; border-top-left-radius: 0; border-top-right-radius: 0; } @@ -104,10 +109,14 @@ $system-note-svg-size: 1rem; border-right: 1px solid $border-color; background-color: $white; - .timeline-content { + .timeline-content:not(.flash-container) { padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; } + .timeline-discussion-body-footer { + padding: 0 $gl-padding-8 0; + } + .timeline-avatar { margin: $gl-padding-8 0 0 $gl-padding; } @@ -116,6 +125,12 @@ $system-note-svg-size: 1rem; margin-left: 2rem; } } + + &:last-of-type .timeline-entry-inner { + border-bottom: 1px solid $border-color; + border-bottom-left-radius: $gl-border-radius-base; + border-bottom-right-radius: $gl-border-radius-base; + } } .diff-content { @@ -1055,7 +1070,7 @@ $system-note-svg-size: 1rem; padding-left: 0; ul.notes li.note-wrapper { - .timeline-content { + .timeline-content:not(.flash-container) { padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; } @@ -1071,7 +1086,7 @@ $system-note-svg-size: 1rem; border-right: 0; } - div.discussion-reply-holder { + .discussion-reply-holder { margin-left: 0; } } @@ -1102,7 +1117,7 @@ $system-note-svg-size: 1rem; } } - .draft-note-component .draft-note.timeline-entry { + .draft-note-component.draft-note.timeline-entry { .timeline-content:not(.flash-container) { padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a119c815cf4..eb47ad7efe6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -247,7 +247,7 @@ .repository-languages-bar { height: 8px; - margin-bottom: $gl-padding-8; + margin-bottom: $gl-padding; background-color: $white; border-radius: $border-radius-default; diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index bea184e44b9..a3adeff3d33 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -173,7 +173,7 @@ module IssuableActions def render_cached_discussions(discussions, serializer, cache_context) render_cached(discussions, with: serializer, - cache_context: -> (_) { cache_context }, + cache_context: ->(_) { cache_context }, context: self) end 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 b2b5e096105..37138afc719 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 @@ -25,7 +25,7 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati { date: 'date', group_name: 'group_name', - param_type => -> (record) { record.data[param_type] } + param_type => ->(record) { record.data[param_type] } } ).render end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4ba79d43f27..f32ddee00e7 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -147,7 +147,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo render_cached(@merge_request, with: serializer, - cache_context: -> (_) { [Digest::SHA256.hexdigest(cache_context.to_s)] }, + cache_context: ->(_) { [Digest::SHA256.hexdigest(cache_context.to_s)] }, serializer: params[:serializer]) else render json: serializer.represent(@merge_request, serializer: params[:serializer]) diff --git a/app/finders/releases/group_releases_finder.rb b/app/finders/releases/group_releases_finder.rb index 08530f63ea6..67784d6579c 100644 --- a/app/finders/releases/group_releases_finder.rb +++ b/app/finders/releases/group_releases_finder.rb @@ -33,8 +33,8 @@ module Releases Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new( scope: releases_scope, array_scope: Project.for_group_and_its_subgroups(parent).select(:id), - array_mapping_scope: -> (project_id_expression) { Release.where(Release.arel_table[:project_id].eq(project_id_expression)) }, - finder_query: -> (order_by, id_expression) { Release.where(Release.arel_table[:id].eq(id_expression)) } + array_mapping_scope: ->(project_id_expression) { Release.where(Release.arel_table[:project_id].eq(project_id_expression)) }, + finder_query: ->(order_by, id_expression) { Release.where(Release.arel_table[:id].eq(id_expression)) } ) .execute end diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb index 3c99cde60a4..4f0bf19f09c 100644 --- a/app/graphql/mutations/ci/runner/update.rb +++ b/app/graphql/mutations/ci/runner/update.rb @@ -54,7 +54,7 @@ module Mutations argument :associated_projects, [::Types::GlobalIDType[::Project]], required: false, description: 'Projects associated with the runner. Available only for project runners.', - prepare: -> (global_ids, ctx) { global_ids&.filter_map { |gid| gid.model_id.to_i } } + prepare: ->(global_ids, ctx) { global_ids&.filter_map { |gid| gid.model_id.to_i } } field :runner, Types::Ci::RunnerType, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d44ba35bb44..849cb646025 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -142,6 +142,8 @@ module ProjectsHelper end def project_search_tabs?(tab) + return false unless @project.present? + abilities = Array(search_tab_ability_map[tab]) abilities.any? { |ability| can?(current_user, ability, @project) } diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb index dce0517690d..63e2b377fef 100644 --- a/app/helpers/routing/pseudonymization_helper.rb +++ b/app/helpers/routing/pseudonymization_helper.rb @@ -12,6 +12,7 @@ module Routing tab glm_source glm_content + _gl ].freeze def initialize(request_object, group, project) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 3696721a062..37b213a3185 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -447,20 +447,38 @@ module SearchHelper result end + def code_tab_condition + return true if project_search_tabs?(:blobs) + + @project.nil? && search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_code_tab) + end + + def wiki_tab_condition + return true if project_search_tabs?(:wiki) + + @project.nil? && search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_wiki_tab) + end + + def commits_tab_condition + return true if project_search_tabs?(:commits) + + @project.nil? && search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_commits_tab) + end + # search page scope navigation def search_navigation { projects: { sort: 1, label: _("Projects"), data: { qa_selector: 'projects_tab' }, condition: @project.nil? }, - blobs: { sort: 2, label: _("Code"), data: { qa_selector: 'code_tab' }, condition: project_search_tabs?(:blobs) || (search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_code_tab)) }, + blobs: { sort: 2, label: _("Code"), data: { qa_selector: 'code_tab' }, condition: code_tab_condition }, # sort: 3 is reserved for EE items issues: { sort: 4, label: _("Issues"), condition: project_search_tabs?(:issues) || feature_flag_tab_enabled?(:global_search_issues_tab) }, merge_requests: { sort: 5, label: _("Merge requests"), condition: project_search_tabs?(:merge_requests) || feature_flag_tab_enabled?(:global_search_merge_requests_tab) }, - wiki_blobs: { sort: 6, label: _("Wiki"), condition: project_search_tabs?(:wiki) || search_service.show_elasticsearch_tabs? }, - commits: { sort: 7, label: _("Commits"), condition: project_search_tabs?(:commits) || (search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_commits_tab)) }, + wiki_blobs: { sort: 6, label: _("Wiki"), condition: wiki_tab_condition }, + commits: { sort: 7, label: _("Commits"), condition: commits_tab_condition }, notes: { sort: 8, label: _("Comments"), condition: project_search_tabs?(:notes) || search_service.show_elasticsearch_tabs? }, milestones: { sort: 9, label: _("Milestones"), condition: project_search_tabs?(:milestones) || @project.nil? }, - users: { sort: 10, label: _("Users"), condition: show_user_search_tab? }, - snippet_titles: { sort: 11, label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: @show_snippets.present? && @project.nil? } + users: { sort: 10, label: _("Users"), condition: show_user_search_tab? }, + snippet_titles: { sort: 11, label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: @show_snippets.present? && @project.nil? } } end @@ -584,7 +602,7 @@ module SearchHelper end def feature_flag_tab_enabled?(flag) - @group || Feature.enabled?(flag, current_user, type: :ops) + @group.present? || Feature.enabled?(flag, current_user, type: :ops) end def sanitized_search_params diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 7cfebf0473f..f1f22d94061 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -14,7 +14,7 @@ class AbuseReport < ApplicationRecord validates :message, presence: true validates :user_id, uniqueness: { message: 'has already been reported' } - scope :by_user, -> (user) { where(user_id: user) } + scope :by_user, ->(user) { where(user_id: user) } scope :with_users, -> { includes(:reporter, :user) } # For CacheMarkdownField diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index 9f05c87018d..a5a539eae75 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -53,7 +53,7 @@ module AlertManagement validates :fingerprint, allow_blank: true, uniqueness: { scope: :project, conditions: -> { not_resolved }, - message: -> (object, data) { _('Cannot have multiple unresolved alerts') } + message: ->(object, data) { _('Cannot have multiple unresolved alerts') } }, unless: :resolved? validate :hosts_format @@ -74,23 +74,23 @@ module AlertManagement delegate :iid, to: :issue, prefix: true, allow_nil: true delegate :details_url, to: :present - scope :for_iid, -> (iid) { where(iid: iid) } - scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) } - scope :for_environment, -> (environment) { where(environment: environment) } - scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) } - scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) } + scope :for_iid, ->(iid) { where(iid: iid) } + scope :for_fingerprint, ->(project, fingerprint) { where(project: project, fingerprint: fingerprint) } + scope :for_environment, ->(environment) { where(environment: environment) } + scope :for_assignee_username, ->(assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) } + scope :search, ->(query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) } scope :not_resolved, -> { without_status(:resolved) } scope :with_prometheus_alert, -> { includes(:prometheus_alert) } scope :with_operations_alerts, -> { where(domain: :operations) } - scope :order_start_time, -> (sort_order) { order(started_at: sort_order) } - scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) } - scope :order_event_count, -> (sort_order) { order(events: sort_order) } + scope :order_start_time, ->(sort_order) { order(started_at: sort_order) } + scope :order_end_time, ->(sort_order) { order(ended_at: sort_order) } + scope :order_event_count, ->(sort_order) { order(events: sort_order) } # Ascending sort order sorts severity from less critical to more critical. # Descending sort order sorts severity from more critical to less critical. # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior - scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) } + scope :order_severity, ->(sort_order) { order(severity: sort_order == :asc ? :desc : :asc) } scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) } scope :counts_by_project_id, -> { group(:project_id).count } diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb index b2686924363..906855d6dfc 100644 --- a/app/models/alert_management/http_integration.rb +++ b/app/models/alert_management/http_integration.rb @@ -28,7 +28,7 @@ module AlertManagement before_validation :ensure_token before_validation :ensure_payload_example_not_nil - scope :for_endpoint_identifier, -> (endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) } + scope :for_endpoint_identifier, ->(endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) } scope :active, -> { where(active: true) } scope :ordered_by_id, -> { order(:id) } diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb index 880b3a7e310..a888422a6b4 100644 --- a/app/models/analytics/cycle_analytics/aggregation.rb +++ b/app/models/analytics/cycle_analytics/aggregation.rb @@ -7,7 +7,7 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord validates :incremental_runtimes_in_seconds, :incremental_processed_records, :full_runtimes_in_seconds, :full_processed_records, presence: true, length: { maximum: 10 }, allow_blank: true - scope :priority_order, -> (column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) } + scope :priority_order, ->(column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) } scope :enabled, -> { where('enabled IS TRUE') } def cursor_for(mode, model) diff --git a/app/models/analytics/usage_trends/measurement.rb b/app/models/analytics/usage_trends/measurement.rb index 02e239ca0ef..c1245d8dce7 100644 --- a/app/models/analytics/usage_trends/measurement.rb +++ b/app/models/analytics/usage_trends/measurement.rb @@ -23,9 +23,9 @@ module Analytics validates :recorded_at, uniqueness: { scope: :identifier } scope :order_by_latest, -> { order(recorded_at: :desc) } - scope :with_identifier, -> (identifier) { where(identifier: identifier) } - scope :recorded_after, -> (date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? } - scope :recorded_before, -> (date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? } + scope :with_identifier, ->(identifier) { where(identifier: identifier) } + scope :recorded_after, ->(date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? } + scope :recorded_before, ->(date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? } def self.identifier_query_mapping { diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 797e2632d84..3a67e1bc276 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -464,7 +464,7 @@ class ApplicationSetting < ApplicationRecord validates :external_auth_client_key, presence: true, - if: -> (setting) { setting.external_auth_client_cert.present? } + if: ->(setting) { setting.external_auth_client_cert.present? } validates :lets_encrypt_notification_email, devise_email: true, @@ -486,17 +486,17 @@ class ApplicationSetting < ApplicationRecord validates :eks_access_key_id, length: { in: 16..128 }, - if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } + if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } validates :eks_secret_access_key, presence: true, - if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } + if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? } validates_with X509CertificateCredentialsValidator, certificate: :external_auth_client_cert, pkey: :external_auth_client_key, pass: :external_auth_client_key_pass, - if: -> (setting) { setting.external_auth_client_cert.present? } + if: ->(setting) { setting.external_auth_client_cert.present? } validates :default_ci_config_path, format: { without: %r{(\.{2}|\A/)}, diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 0ad17cd8869..5cc87be388f 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -28,11 +28,11 @@ class AuditEvent < ApplicationRecord validates :entity_type, presence: true validates :ip_address, ip_address: true - scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) } - scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) } - scope :by_author_id, -> (author_id) { where(author_id: author_id) } - scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) } - scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) } + scope :by_entity_type, ->(entity_type) { where(entity_type: entity_type) } + scope :by_entity_id, ->(entity_id) { where(entity_id: entity_id) } + scope :by_author_id, ->(author_id) { where(author_id: author_id) } + scope :by_entity_username, ->(username) { where(entity_id: find_user_id(username)) } + scope :by_author_username, ->(username) { where(author_id: find_user_id(username)) } after_initialize :initialize_details diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index e9530a80d9f..9d758cf75d8 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -23,8 +23,8 @@ class AwardEmoji < ApplicationRecord scope :downvotes, -> { named(DOWNVOTE_NAME) } scope :upvotes, -> { named(UPVOTE_NAME) } - scope :named, -> (names) { where(name: names) } - scope :awarded_by, -> (users) { where(user: users) } + scope :named, ->(names) { where(name: names) } + scope :awarded_by, ->(users) { where(user: users) } after_save :expire_cache after_destroy :expire_cache diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb index dc273e256a8..65299d6dd12 100644 --- a/app/models/board_group_recent_visit.rb +++ b/app/models/board_group_recent_visit.rb @@ -12,7 +12,7 @@ class BoardGroupRecentVisit < ApplicationRecord validates :group, presence: true validates :board, presence: true - scope :by_user_parent, -> (user, group) { where(user: user, group: group) } + scope :by_user_parent, ->(user, group) { where(user: user, group: group) } def self.board_parent_relation :group diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb index 723afd6feab..c5122392b91 100644 --- a/app/models/board_project_recent_visit.rb +++ b/app/models/board_project_recent_visit.rb @@ -12,7 +12,7 @@ class BoardProjectRecentVisit < ApplicationRecord validates :project, presence: true validates :board, presence: true - scope :by_user_parent, -> (user, project) { where(user: user, project: project) } + scope :by_user_parent, ->(user, project) { where(user: user, project: project) } def self.board_parent_relation :project diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index 2200a66b3c2..2565ad5f2b8 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -17,7 +17,7 @@ class BulkImport < ApplicationRecord enum source_type: { gitlab: 0 } scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) } - scope :order_by_created_at, -> (direction) { order(created_at: direction) } + scope :order_by_created_at, ->(direction) { order(created_at: direction) } state_machine :status, initial: :created do state :created, value: 0 diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index a2542e669e1..e49c4e09a50 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -53,7 +53,7 @@ class BulkImports::Entity < ApplicationRecord scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) } scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) } scope :by_bulk_import_id, ->(bulk_import_id) { where(bulk_import_id: bulk_import_id) } - scope :order_by_created_at, -> (direction) { order(created_at: direction) } + scope :order_by_created_at, ->(direction) { order(created_at: direction) } alias_attribute :destination_slug, :destination_name diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb index 62a428a0c1e..43214b0c336 100644 --- a/app/models/ci/running_build.rb +++ b/app/models/ci/running_build.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true module Ci + # This model represents metadata for a running build. + # Despite the generic RunningBuild name, in this first iteration it applies only to shared runners + # (see Ci::RunningBuild.upsert_shared_runner_build!). + # The decision to insert all of the running builds here was deferred to avoid the pressure on the database as + # at this time that was not necessary. + # We can reconsider the decision to limit this only to shared runners when there is more evidence that inserting all + # of the running builds there is worth the additional pressure. class RunningBuild < Ci::ApplicationRecord include Ci::Partitionable diff --git a/app/models/integration.rb b/app/models/integration.rb index 41278dce22d..dfa3642cfe4 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -19,7 +19,7 @@ class Integration < ApplicationRecord INTEGRATION_NAMES = %w[ asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord - drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat harbor irker jira + drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao ].freeze diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb index 52efb29f2c1..d7625cfb3d2 100644 --- a/app/models/integrations/flowdock.rb +++ b/app/models/integrations/flowdock.rb @@ -1,28 +1,12 @@ # frozen_string_literal: true +# This integration is scheduled for removal. +# All records must be deleted before the class can be removed. +# https://gitlab.com/gitlab-org/gitlab/-/issues/379197 module Integrations class Flowdock < Integration - validates :token, presence: true, if: :activated? - - field :token, - type: 'password', - help: -> { s_('FlowdockService|Enter your Flowdock token.') }, - non_empty_password_title: -> { s_('ProjectService|Enter new token') }, - non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, - placeholder: '1b609b52537...', - required: true - - def title - 'Flowdock' - end - - def description - s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.') - end - - def help - docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer' - s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } + def readonly? + true end def self.to_param @@ -30,22 +14,7 @@ module Integrations end def self.supported_events - %w(push) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - ::Flowdock::Git.post( - data[:ref], - data[:before], - data[:after], - token: token, - repo: project.repository, - repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", - commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/%s", - diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" - ) + %w[] end end end diff --git a/app/models/project.rb b/app/models/project.rb index 8d66c0d6ea5..5d66e7d2854 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -196,7 +196,6 @@ class Project < ApplicationRecord has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush' has_one :ewm_integration, class_name: 'Integrations::Ewm' has_one :external_wiki_integration, class_name: 'Integrations::ExternalWiki' - has_one :flowdock_integration, class_name: 'Integrations::Flowdock' has_one :hangouts_chat_integration, class_name: 'Integrations::HangoutsChat' has_one :harbor_integration, class_name: 'Integrations::Harbor' has_one :irker_integration, class_name: 'Integrations::Irker' diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index f8e7a912896..41c924029d7 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -68,6 +68,8 @@ class BasePolicy < DeclarativePolicy::Base rule { default }.enable :read_cross_project condition(:is_gitlab_com, score: 0, scope: :global) { ::Gitlab.com? } + + condition(:is_bot?) { @user&.bot? } end BasePolicy.prepend_mod_with('BasePolicy') diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb index bda327cb661..62840b0129f 100644 --- a/app/policies/merge_request_policy.rb +++ b/app/policies/merge_request_policy.rb @@ -18,6 +18,10 @@ class MergeRequestPolicy < IssuablePolicy enable :approve_merge_request end + rule { can?(:approve_merge_request) & is_bot? }.policy do + enable :reset_merge_request_approvals + end + rule { ~anonymous & can?(:read_merge_request) }.policy do enable :create_todo enable :update_subscription diff --git a/app/services/projects/container_repository/cleanup_tags_base_service.rb b/app/services/projects/container_repository/cleanup_tags_base_service.rb index 5393c2c080d..45557d03502 100644 --- a/app/services/projects/container_repository/cleanup_tags_base_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_base_service.rb @@ -6,6 +6,8 @@ module Projects private def filter_out_latest!(tags) + return unless keep_latest + tags.reject!(&:latest?) end @@ -84,6 +86,10 @@ module Projects params['keep_n'] end + def keep_latest + params.fetch('keep_latest', true) + end + def project container_repository.project end diff --git a/app/services/projects/container_repository/destroy_service.rb b/app/services/projects/container_repository/destroy_service.rb index ee5bb20b7e0..fb32fdfa911 100644 --- a/app/services/projects/container_repository/destroy_service.rb +++ b/app/services/projects/container_repository/destroy_service.rb @@ -5,7 +5,8 @@ module Projects class DestroyService < BaseService CLEANUP_TAGS_SERVICE_PARAMS = { 'name_regex_delete' => '.*', - 'container_expiration_policy' => true # to avoid permissions checks + 'container_expiration_policy' => true, # to avoid permissions checks + 'keep_latest' => false }.freeze def execute(container_repository, disable_timeout: true) diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml index c53f63e124b..b07db09d06c 100644 --- a/app/views/admin/application_settings/_terminal.html.haml +++ b/app/views/admin/application_settings/_terminal.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form', id: 'terminal-settings' } do |f| += gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form', id: 'terminal-settings' } do |f| = form_errors(@application_setting) %fieldset @@ -7,4 +7,4 @@ = f.number_field :terminal_max_session_time, class: 'form-control gl-form-input' .form-text.text-muted = _('Maximum time, in seconds, for a web terminal websocket connection. 0 for unlimited.') - = f.submit _('Save changes'), class: "gl-button btn btn-confirm" + = f.submit _('Save changes'), pajamas_button: true diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml index c3b881df98d..af4c934fd72 100644 --- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml @@ -6,5 +6,5 @@ = c.body do = s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } = c.actions do - %a.gl-button.btn-confirm.text-decoration-none{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' } + = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', button_options: { rel: 'noopener noreferrer' }) do = s_("ClusterIntegration|Apply for credit") diff --git a/app/views/layouts/_google_tag_manager_head.html.haml b/app/views/layouts/_google_tag_manager_head.html.haml index 97e118aba93..21b9a604a35 100644 --- a/app/views/layouts/_google_tag_manager_head.html.haml +++ b/app/views/layouts/_google_tag_manager_head.html.haml @@ -20,6 +20,17 @@ 'wait_for_update': 500 }); + window.geofeed = (options) => { + dataLayer.push({ + 'event' : 'OneTrustCountryLoad', + 'oneTrustCountryId': options.country.toString() + }) + } + + const json = document.createElement('script'); + json.setAttribute('src', 'https://geolocation.onetrust.com/cookieconsentpub/v1/geo/location/geofeed'); + document.head.appendChild(json); + - if Feature.enabled?(:gtm_nonce, type: :ops) = javascript_tag nonce: content_security_policy_nonce do :plain diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml index 283659875ef..f4e9a597fe2 100644 --- a/app/views/registrations/welcome/show.html.haml +++ b/app/views/registrations/welcome/show.html.haml @@ -18,22 +18,24 @@ %p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text } = gitlab_ui_form_for(current_user, url: users_sign_up_welcome_path(glm_tracking_params), - html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome', + html: { class: 'gl-w-full! gl-p-5 js-users-signup-welcome', 'aria-live' => 'assertive', data: { testid: 'welcome-form' } }) do |f| - .devise-errors - = render 'devise/shared/error_messages', resource: current_user - .row - .form-group.col-sm-12 - = f.label :role, _('Role'), class: 'label-bold' - = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', autofocus: true, required: true, data: { qa_selector: 'role_dropdown' } - = render_if_exists "registrations/welcome/jobs_to_be_done", f: f - = render_if_exists "registrations/welcome/setup_for_company", f: f - = render_if_exists "registrations/welcome/joining_project" - = render 'devise/shared/email_opted_in', f: f - .row - .form-group.col-sm-12.gl-mb-0 - - if partial_exists? "registrations/welcome/button" - = render "registrations/welcome/button" - - else - = f.submit _('Get started!'), class: 'btn-confirm gl-button btn btn-block gl-mb-0 gl-p-3', data: { qa_selector: 'get_started_button' } + = render Pajamas::CardComponent.new do |c| + - c.body do + .devise-errors + = render 'devise/shared/error_messages', resource: current_user + .row + .form-group.col-sm-12 + = f.label :role, _('Role'), class: 'label-bold' + = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', autofocus: true, required: true, data: { qa_selector: 'role_dropdown' } + = render_if_exists "registrations/welcome/jobs_to_be_done", f: f + = render_if_exists "registrations/welcome/setup_for_company", f: f + = render_if_exists "registrations/welcome/joining_project" + = render 'devise/shared/email_opted_in', f: f + .row + .form-group.col-sm-12.gl-mb-0 + - if partial_exists? "registrations/welcome/button" + = render "registrations/welcome/button" + - else + = f.submit _('Get started!'), class: 'btn-confirm gl-button btn btn-block gl-mb-0 gl-p-3', data: { qa_selector: 'get_started_button' } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0fd128df997..327461b25fd 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -20,7 +20,7 @@ .js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } } = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| - .block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container' } } + .block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container', testid: 'assignee-block-container' } } = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in - if issuable_sidebar[:supports_severity] diff --git a/app/workers/container_registry/delete_container_repository_worker.rb b/app/workers/container_registry/delete_container_repository_worker.rb index 1f94b1b9e71..25df3b6dd5f 100644 --- a/app/workers/container_registry/delete_container_repository_worker.rb +++ b/app/workers/container_registry/delete_container_repository_worker.rb @@ -17,10 +17,12 @@ module ContainerRegistry MAX_CAPACITY = 2 CLEANUP_TAGS_SERVICE_PARAMS = { 'name_regex_delete' => '.*', + 'keep_latest' => false, 'container_expiration_policy' => true # to avoid permissions checks }.freeze def perform_work + return unless Feature.enabled?(:container_registry_delete_repository_with_cron_worker) return unless next_container_repository result = delete_tags @@ -38,6 +40,8 @@ module ContainerRegistry end def remaining_work_count + return 0 unless Feature.enabled?(:container_registry_delete_repository_with_cron_worker) + ::ContainerRepository.delete_scheduled.limit(max_running_jobs + 1).count end diff --git a/config/metrics/counts_all/20210216175837_projects_flowdock_active.yml b/config/metrics/counts_all/20210216175837_projects_flowdock_active.yml index 46db9f97e85..b88351eb4dc 100644 --- a/config/metrics/counts_all/20210216175837_projects_flowdock_active.yml +++ b/config/metrics/counts_all/20210216175837_projects_flowdock_active.yml @@ -7,7 +7,9 @@ product_stage: manage product_group: integrations product_category: integrations value_type: number -status: active +status: removed +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102394 +milestone_removed: '15.7' time_frame: all data_source: database distribution: diff --git a/config/metrics/counts_all/20210216175839_groups_flowdock_active.yml b/config/metrics/counts_all/20210216175839_groups_flowdock_active.yml index d5da36978b6..f77fe5ec728 100644 --- a/config/metrics/counts_all/20210216175839_groups_flowdock_active.yml +++ b/config/metrics/counts_all/20210216175839_groups_flowdock_active.yml @@ -7,7 +7,9 @@ product_stage: manage product_group: integrations product_category: integrations value_type: number -status: active +status: removed +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102394 +milestone_removed: '15.7' time_frame: all data_source: database distribution: diff --git a/config/metrics/counts_all/20210216175842_instances_flowdock_active.yml b/config/metrics/counts_all/20210216175842_instances_flowdock_active.yml index 198af43a99d..f22e6e6bc77 100644 --- a/config/metrics/counts_all/20210216175842_instances_flowdock_active.yml +++ b/config/metrics/counts_all/20210216175842_instances_flowdock_active.yml @@ -7,7 +7,9 @@ product_stage: manage product_group: integrations product_category: integrations value_type: number -status: active +status: removed +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102394 +milestone_removed: '15.7' time_frame: all data_source: database distribution: diff --git a/config/metrics/counts_all/20210216175844_projects_inheriting_flowdock_active.yml b/config/metrics/counts_all/20210216175844_projects_inheriting_flowdock_active.yml index f094f894ded..a291191eaeb 100644 --- a/config/metrics/counts_all/20210216175844_projects_inheriting_flowdock_active.yml +++ b/config/metrics/counts_all/20210216175844_projects_inheriting_flowdock_active.yml @@ -7,7 +7,9 @@ product_stage: manage product_group: integrations product_category: integrations value_type: number -status: active +status: removed +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102394 +milestone_removed: '15.7' time_frame: all data_source: database distribution: diff --git a/config/metrics/counts_all/20210216175846_groups_inheriting_flowdock_active.yml b/config/metrics/counts_all/20210216175846_groups_inheriting_flowdock_active.yml index fb7931ddf09..c3c4a01a809 100644 --- a/config/metrics/counts_all/20210216175846_groups_inheriting_flowdock_active.yml +++ b/config/metrics/counts_all/20210216175846_groups_inheriting_flowdock_active.yml @@ -7,7 +7,9 @@ product_stage: manage product_group: integrations product_category: integrations value_type: number -status: active +status: removed +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102394 +milestone_removed: '15.7' time_frame: all data_source: database distribution: diff --git a/data/removals/15_7/15-7-remove-flowdock-integration.yml b/data/removals/15_7/15-7-remove-flowdock-integration.yml new file mode 100644 index 00000000000..46f8ed6bdf9 --- /dev/null +++ b/data/removals/15_7/15-7-remove-flowdock-integration.yml @@ -0,0 +1,18 @@ +- title: "Flowdock integration" # (required) Actionable title. e.g., The `confidential` field for a `Note` is deprecated. Use `internal` instead. + announcement_milestone: "15.7" # (required) The milestone when this feature was deprecated. + announcement_date: "2022-12-22" # (required) The date of the milestone release when this feature was deprecated. This should almost always be the 22nd of a month (YYYY-MM-DD), unless you did an out of band blog post. + removal_milestone: "15.7" # (required) The milestone when this feature is being removed. + removal_date: "2022-12-22" # (required) This should almost always be the 22nd of a month (YYYY-MM-DD), the date of the milestone release when this feature will be removed. + breaking_change: false # (required) Change to true if this removal is a breaking change. + reporter: arturoherrero # (required) GitLab username of the person reporting the removal + stage: manage # (required) String value of the stage that the feature was created in. e.g., Growth + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/379197 # (required) Link to the deprecation issue in GitLab + body: | # (required) Do not modify this line, instead modify the lines below. + As of December 22, 2022, we are removing the Flowdock integration because the service was shut down on August 15, 2022. +# +# OPTIONAL FIELDS +# + tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate] + documentation_url: # (optional) This is a link to the current documentation page + image_url: # (optional) This is a link to a thumbnail image depicting the feature + video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg diff --git a/db/docs/ci_pending_builds.yml b/db/docs/ci_pending_builds.yml index 4abcb77a499..5622df4feab 100644 --- a/db/docs/ci_pending_builds.yml +++ b/db/docs/ci_pending_builds.yml @@ -4,7 +4,7 @@ classes: - Ci::PendingBuild feature_categories: - continuous_integration -description: TODO +description: Pending builds metadata introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61581 milestone: '14.0' gitlab_schema: gitlab_ci diff --git a/db/docs/ci_running_builds.yml b/db/docs/ci_running_builds.yml index de337d628eb..72e941a8665 100644 --- a/db/docs/ci_running_builds.yml +++ b/db/docs/ci_running_builds.yml @@ -4,7 +4,13 @@ classes: - Ci::RunningBuild feature_categories: - continuous_integration -description: TODO +description: > + Running builds metadata. + Despite the generic `RunningBuild` name, in this first iteration it applies only to shared runners. + The decision to insert all of the running builds here was deferred to avoid the pressure on the database as + at this time that was not necessary. + We can reconsider the decision to limit this only to shared runners when there is more evidence that inserting all + of the running builds there is worth the additional pressure. introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62912 milestone: '14.0' gitlab_schema: gitlab_ci diff --git a/db/docs/integrations.yml b/db/docs/integrations.yml index 9c3d97492fb..52d719e19da 100644 --- a/db/docs/integrations.yml +++ b/db/docs/integrations.yml @@ -21,7 +21,6 @@ classes: - Integrations::EmailsOnPush - Integrations::Ewm - Integrations::ExternalWiki -- Integrations::Flowdock - Integrations::Github - Integrations::GitlabSlackApplication - Integrations::HangoutsChat diff --git a/db/migrate/20221114145103_add_last_seat_refresh_at_to_gitlab_subscriptions.rb b/db/migrate/20221114145103_add_last_seat_refresh_at_to_gitlab_subscriptions.rb new file mode 100644 index 00000000000..77d6bb42f02 --- /dev/null +++ b/db/migrate/20221114145103_add_last_seat_refresh_at_to_gitlab_subscriptions.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddLastSeatRefreshAtToGitlabSubscriptions < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + TABLE_NAME = 'gitlab_subscriptions' + COLUMN_NAME = 'last_seat_refresh_at' + + def up + add_column(TABLE_NAME, COLUMN_NAME, :datetime_with_timezone) + end + + def down + remove_column(TABLE_NAME, COLUMN_NAME) + end +end diff --git a/db/schema_migrations/20221114145103 b/db/schema_migrations/20221114145103 new file mode 100644 index 00000000000..da49d8f76b1 --- /dev/null +++ b/db/schema_migrations/20221114145103 @@ -0,0 +1 @@ +1621f0ac141f24c15beef34f5f411158c1eb8a89f5022dd426533d705aa859fe
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c0da93eccbf..065c1db2f77 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -15982,6 +15982,7 @@ CREATE TABLE gitlab_subscriptions ( seats_owed integer DEFAULT 0 NOT NULL, trial_extension_type smallint, max_seats_used_changed_at timestamp with time zone, + last_seat_refresh_at timestamp with time zone, CONSTRAINT check_77fea3f0e7 CHECK ((namespace_id IS NOT NULL)) ); diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index 7835270d4bd..3c76e35826d 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -348,7 +348,6 @@ Flamegraph flamegraphs Flawfinder Flickr -Flowdock Fluentd Flutterwave Flycheck diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 963dbda0231..b35beec5ea6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -16885,7 +16885,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="pipelinesecurityreportfindingsolution"></a>`solution` | [`String`](#string) | URL to the vulnerability's details page. | | <a id="pipelinesecurityreportfindingstate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | Finding status. | | <a id="pipelinesecurityreportfindingtitle"></a>`title` | [`String`](#string) | Title of the vulnerability finding. | -| <a id="pipelinesecurityreportfindinguuid"></a>`uuid` | [`String`](#string) | Name of the vulnerability finding. | +| <a id="pipelinesecurityreportfindinguuid"></a>`uuid` | [`String`](#string) | UUIDv5 digest based on the vulnerability's report type, primary identifier, location, fingerprint, project identifier. | ### `PreviewBillableUserChange` @@ -22509,7 +22509,6 @@ State of a Sentry error. | <a id="servicetypeemails_on_push_service"></a>`EMAILS_ON_PUSH_SERVICE` | EmailsOnPushService type. | | <a id="servicetypeewm_service"></a>`EWM_SERVICE` | EwmService type. | | <a id="servicetypeexternal_wiki_service"></a>`EXTERNAL_WIKI_SERVICE` | ExternalWikiService type. | -| <a id="servicetypeflowdock_service"></a>`FLOWDOCK_SERVICE` | FlowdockService type. | | <a id="servicetypegithub_service"></a>`GITHUB_SERVICE` | GithubService type. | | <a id="servicetypegitlab_slack_application_service"></a>`GITLAB_SLACK_APPLICATION_SERVICE` | GitlabSlackApplicationService type (Gitlab.com only). | | <a id="servicetypehangouts_chat_service"></a>`HANGOUTS_CHAT_SERVICE` | HangoutsChatService type. | diff --git a/doc/api/integrations.md b/doc/api/integrations.md index a6090228af2..c71f136f132 100644 --- a/doc/api/integrations.md +++ b/doc/api/integrations.md @@ -755,42 +755,6 @@ Get External wiki integration settings for a project. GET /projects/:id/integrations/external-wiki ``` -## Flowdock - -Flowdock is a ChatOps application for collaboration in software engineering -companies. You can send notifications from GitLab events to Flowdock flows. -For integration instructions, see the [Flowdock documentation](https://www.flowdock.com/help/gitlab). - -### Create/Edit Flowdock integration - -Set Flowdock integration for a project. - -```plaintext -PUT /projects/:id/integrations/flowdock -``` - -Parameters: - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `token` | string | true | Flowdock Git source token | - -### Disable Flowdock integration - -Disable the Flowdock integration for a project. Integration settings are preserved. - -```plaintext -DELETE /projects/:id/integrations/flowdock -``` - -### Get Flowdock integration settings - -Get Flowdock integration settings for a project. - -```plaintext -GET /projects/:id/integrations/flowdock -``` - ## GitHub **(PREMIUM)** Code collaboration software. diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md index 7f980c2a017..37054c61a85 100644 --- a/doc/architecture/blueprints/work_items/index.md +++ b/doc/architecture/blueprints/work_items/index.md @@ -61,7 +61,7 @@ All Work Item types share the same pool of predefined widgets and are customized | description | | | hierarchy | | | [iteration](https://gitlab.com/gitlab-org/gitlab/-/issues/367456) | | -| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | work_items_mvc_2 | +| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | work_items_mvc | | labels | | | start and due date | | | status\* | | diff --git a/doc/integration/index.md b/doc/integration/index.md index b2a4201e88c..bdf6475b6d2 100644 --- a/doc/integration/index.md +++ b/doc/integration/index.md @@ -11,7 +11,7 @@ You can integrate GitLab with external services for enhanced functionality. ## Services -Services such as Campfire, Flowdock, Jira, Pivotal Tracker, and Slack +Services such as Campfire, Jira, Pivotal Tracker, and Slack are available as [integrations](../user/project/integrations/index.md). ## Issue trackers diff --git a/doc/update/removals.md b/doc/update/removals.md index c2a14a5cb2e..e4338bae1fc 100644 --- a/doc/update/removals.md +++ b/doc/update/removals.md @@ -57,6 +57,12 @@ If you want to preserve this functionality, you can follow one of these two path 1. Fork the [GitLab Auto Deploy Helm chart](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/tree/master/assets/auto-deploy-app) into the `chart/` path within your project 1. Set `AUTO_DEPLOY_IMAGE_VERSION` and `DAST_AUTO_DEPLOY_IMAGE_VERSION` to the most recent version of the image that included the CiliumNetworkPolicy +## Removed in 15.7 + +### Flowdock integration + +As of December 22, 2022, we are removing the Flowdock integration because the service was shut down on August 15, 2022. + ## Removed in 15.6 ### NFS as Git repository storage is no longer supported. Migrate to Gitaly Cluster as soon as possible diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index 6dbb6a3df37..769a45fc6ff 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -58,7 +58,6 @@ You can configure the following integrations. | [Emails on push](emails_on_push.md) | Send commits and diff of each push by email. | **{dotted-circle}** No | | [EWM](ewm.md) | Use IBM Engineering Workflow Management as the issue tracker. | **{dotted-circle}** No | | [External wiki](../wiki/index.md#link-an-external-wiki) | Link an external wiki. | **{dotted-circle}** No | -| [Flowdock](../../../api/integrations.md#flowdock) | Send notifications from GitLab to Flowdock flows. | **{dotted-circle}** No | | [GitHub](github.md) | Obtain statuses for commits and pull requests. | **{dotted-circle}** No | | [Google Chat](hangouts_chat.md) | Send notifications from your GitLab project to a room in Google Chat. | **{dotted-circle}** No | | [Harbor](harbor.md) | Use Harbor as the container registry. | **{dotted-circle}** No | diff --git a/doc/user/tasks.md b/doc/user/tasks.md index 71f427f11c1..9226f8d15c9 100644 --- a/doc/user/tasks.md +++ b/doc/user/tasks.md @@ -207,10 +207,12 @@ To set a start date: ## Add a task to a milestone -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) in GitLab 15.5 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) in GitLab 15.5 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default. +> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) to feature flag named `work_items_mvc` in GitLab 15.7. Disabled by default. FLAG: -On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc_2`. On GitLab.com, this feature is not available. The feature is not ready for production use. +On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc`. +On GitLab.com, this feature is not available. The feature is not ready for production use. You can add a task to a [milestone](project/milestones/index.md). You can see the milestone title when you view a task. diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index 99273e81730..543449c0349 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -415,14 +415,6 @@ module API desc: 'The URL of the external wiki' } ], - 'flowdock' => [ - { - required: true, - name: :token, - type: String, - desc: 'Flowdock token' - } - ], 'hangouts-chat' => [ { required: true, @@ -893,7 +885,6 @@ module API ::Integrations::EmailsOnPush, ::Integrations::Ewm, ::Integrations::ExternalWiki, - ::Integrations::Flowdock, ::Integrations::HangoutsChat, ::Integrations::Harbor, ::Integrations::Irker, diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb index 7622ec717cc..5dea41bfdb7 100644 --- a/lib/api/merge_request_approvals.rb +++ b/lib/api/merge_request_approvals.rb @@ -88,6 +88,23 @@ module API present_approval(merge_request) end + desc 'Remove all merge request approvals' do + detail 'Clear all approvals of merge request. This feature was added in GitLab 15.4' + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 404, message: 'Not found' } + ] + tags %w[merge_requests] + end + put 'reset_approvals', urgency: :low do + merge_request = find_project_merge_request(params[:merge_request_iid]) + + unauthorized! unless current_user.can?(:reset_merge_request_approvals, merge_request) + + merge_request.approvals.delete_all + + status :accepted + end end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index c826872af01..a9572cf7ce6 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -733,25 +733,6 @@ module API rescue ::MergeRequest::RebaseLockTimeout => e render_api_error!(e.message, 409) end - - desc 'Reset approvals of a merge request' do - detail 'Clear all approvals of merge request. This feature was added in GitLab 15.4' - failure [ - { code: 401, message: 'Unauthorized' }, - { code: 404, message: 'Not found' } - ] - tags %w[merge_requests] - end - put ':id/merge_requests/:merge_request_iid/reset_approvals', feature_category: :code_review, urgency: :low do - merge_request = find_project_merge_request(params[:merge_request_iid]) - - unauthorized! unless current_user.bot? && merge_request.eligible_for_approval_by?(current_user) - - merge_request.approvals.delete_all - - status :accepted - end - desc 'List issues that close on merge' do detail 'Get all the issues that would be closed by merging the provided merge request.' success Entities::MRNote diff --git a/lib/flowdock/git.rb b/lib/flowdock/git.rb deleted file mode 100644 index 897ee647d87..00000000000 --- a/lib/flowdock/git.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true -require 'flowdock' -require 'flowdock/git/builder' - -module Flowdock - class Git - TokenError = Class.new(StandardError) - - DEFAULT_PERMANENT_REFS = [ - Regexp.new('refs/heads/master') - ].freeze - - class << self - def post(ref, from, to, options = {}) - Git.new(ref, from, to, options).post - end - end - - def initialize(ref, from, to, options = {}) - raise TokenError, "Flowdock API token not found" unless options[:token] - - @ref = ref - @from = from - @to = to - @options = options - @token = options[:token] - @commit_url = options[:commit_url] - @diff_url = options[:diff_url] - @repo_url = options[:repo_url] - @repo_name = options[:repo_name] - @permanent_refs = options.fetch(:permanent_refs, DEFAULT_PERMANENT_REFS) - end - - # Send git push notification to Flowdock - def post - messages.each do |message| - ::Flowdock::Client.new(flow_token: @token).post_to_thread(message) - end - end - - def repo - @options[:repo] - end - - private - - def messages - Git::Builder.new(repo: repo, - ref: @ref, - before: @from, - after: @to, - commit_url: @commit_url, - branch_url: @branch_url, - diff_url: @diff_url, - repo_url: @repo_url, - repo_name: @repo_name, - permanent_refs: @permanent_refs, - tags: tags - ).to_hashes - end - - # Flowdock tags attached to the push notification - def tags - Array(@options[:tags]).map { |tag| CGI.escape(tag) } - end - end -end diff --git a/lib/flowdock/git/builder.rb b/lib/flowdock/git/builder.rb deleted file mode 100644 index 88d9814950a..00000000000 --- a/lib/flowdock/git/builder.rb +++ /dev/null @@ -1,145 +0,0 @@ -# frozen_string_literal: true -module Flowdock - class Git - class Commit - def initialize(external_thread_id, thread, tags, commit) - @commit = commit - @external_thread_id = external_thread_id - @thread = thread - @tags = tags - end - - def to_hash - hash = { - external_thread_id: @external_thread_id, - event: "activity", - author: { - name: @commit[:author][:name], - email: @commit[:author][:email] - }, - title: title, - thread: @thread, - body: body - } - hash[:tags] = @tags if @tags - encode(hash) - end - - private - - def encode(hash) - return hash unless "".respond_to?(:encode) - - encode_as_utf8(hash) - end - - # This only works on Ruby 1.9 - def encode_as_utf8(obj) - if obj.is_a? Hash - obj.each_pair do |key, val| - encode_as_utf8(val) - end - elsif obj.is_a?(Array) - obj.each do |val| - encode_as_utf8(val) - end - elsif obj.is_a?(String) && obj.encoding != Encoding::UTF_8 - unless obj.force_encoding("UTF-8").valid_encoding? - obj.force_encoding("ISO-8859-1").encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) - end - end - end - - def body - content = @commit[:message][first_line.size..] - content.strip! if content - "<pre>#{content}</pre>" unless content.empty? - end - - def first_line - @first_line ||= (@commit[:message].split("\n")[0] || @commit[:message]) - end - - def title - commit_id = @commit[:id][0, 7] - if @commit[:url] - "<a href=\"#{@commit[:url]}\">#{commit_id}</a> #{message_title}" - else - "#{commit_id} #{message_title}" - end - end - - def message_title - CGI.escape_html(first_line.strip) - end - end - - # Class used to build Git payload - class Builder - include ::Gitlab::Utils::StrongMemoize - - def initialize(opts) - @repo = opts[:repo] - @ref = opts[:ref] - @before = opts[:before] - @after = opts[:after] - @opts = opts - end - - def commits - @repo.commits_between(@before, @after).map do |commit| - { - url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil, - id: commit.sha, - message: commit.message, - author: { - name: commit.author_name, - email: commit.author_email - } - } - end - end - - def ref_name - @ref.to_s.sub(%r{\Arefs/(heads|tags)/}, '') - end - - def to_hashes - commits.map do |commit| - Commit.new(external_thread_id, thread, @opts[:tags], commit).to_hash - end - end - - private - - def thread - @thread ||= { - title: thread_title, - external_url: @opts[:repo_url] - } - end - - def permanent? - strong_memoize(:permanent) do - @opts[:permanent_refs].any? { |regex| regex.match(@ref) } - end - end - - def thread_title - action = "updated" if permanent? - type = @ref =~ %r(^refs/heads/) ? "branch" : "tag" - - [@opts[:repo_name], type, ref_name, action].compact.join(" ") - end - - def external_thread_id - @external_thread_id ||= - if permanent? - SecureRandom.hex - else - @ref - end - end - end - end -end diff --git a/lib/gitlab/ci/build/context/build.rb b/lib/gitlab/ci/build/context/build.rb index a2b330f19b1..a1a8e9288c7 100644 --- a/lib/gitlab/ci/build/context/build.rb +++ b/lib/gitlab/ci/build/context/build.rb @@ -50,5 +50,3 @@ module Gitlab end end end - -Gitlab::Ci::Build::Context::Build.prepend_mod_with('Gitlab::Ci::Build::Context::Build') diff --git a/lib/gitlab/process_management.rb b/lib/gitlab/process_management.rb index f8a1a3a97de..89ffd71c2d8 100644 --- a/lib/gitlab/process_management.rb +++ b/lib/gitlab/process_management.rb @@ -40,15 +40,6 @@ module Gitlab pids.each { |pid| signal(pid, signal) } end - # Waits for the given process to complete using a separate thread. - def self.wait_async(pid) - Thread.new do - Process.wait(pid) - rescue StandardError - nil # There is no reason to return `Errno::ECHILD` if it catches a `TypeError` - end - end - # Returns true if all the processes are alive. def self.all_alive?(pids) pids.each do |pid| diff --git a/lib/gitlab/process_supervisor.rb b/lib/gitlab/process_supervisor.rb index 714034f043d..09e923d1449 100644 --- a/lib/gitlab/process_supervisor.rb +++ b/lib/gitlab/process_supervisor.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative './daemon' + module Gitlab # Given a set of process IDs, the supervisor can monitor processes # for being alive and invoke a callback if some or all should go away. diff --git a/lib/gitlab/ssh/signature.rb b/lib/gitlab/ssh/signature.rb index b1cad8d76c9..3500d1346e2 100644 --- a/lib/gitlab/ssh/signature.rb +++ b/lib/gitlab/ssh/signature.rb @@ -18,11 +18,16 @@ module Gitlab def verification_status strong_memoize(:verification_status) do next :unverified unless all_attributes_present? - next :unverified unless valid_signature_blob? && committer + next :unverified unless valid_signature_blob? next :unknown_key unless signed_by_key + next :other_user unless committer next :other_user unless signed_by_key.user == committer - :verified + if signed_by_user_email_verified? + :verified + else + :unverified + end end end @@ -55,7 +60,11 @@ module Gitlab def committer # Lookup by email because users can push verified commits that were made # by someone else. For example: Doing a rebase. - strong_memoize(:committer) { User.find_by_any_email(@committer_email, confirmed: true) } + strong_memoize(:committer) { User.find_by_any_email(@committer_email) } + end + + def signed_by_user_email_verified? + signed_by_key.user.verified_emails.include?(@committer_email) end def signature diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bba56eeae78..975f8b50c87 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1279,12 +1279,18 @@ msgstr "" msgid "(Group Managed Account)" msgstr "" +msgid "(Limited to %{quota} pipeline minutes per month)" +msgstr "" + msgid "(No changes)" msgstr "" msgid "(UTC %{offset}) %{timezone}" msgstr "" +msgid "(Unlimited pipeline minutes)" +msgstr "" + msgid "(check progress)" msgstr "" @@ -9256,6 +9262,9 @@ msgstr "" msgid "ClusterAgents|Unknown user" msgstr "" +msgid "ClusterAgents|Use a Helm version compatible with your Kubernetes version (see %{linkStart}Helm version support policy%{linkEnd})." +msgstr "" + msgid "ClusterAgents|Valid access token" msgstr "" @@ -17295,15 +17304,6 @@ msgstr "" msgid "FloC|Participate in FLoC" msgstr "" -msgid "FlowdockService|Enter your Flowdock token." -msgstr "" - -msgid "FlowdockService|Send event notifications from GitLab to Flowdock flows." -msgstr "" - -msgid "FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}" -msgstr "" - msgid "Focus filter bar" msgstr "" @@ -27117,6 +27117,9 @@ msgstr "" msgid "New branch unavailable" msgstr "" +msgid "New code quality findings" +msgstr "" + msgid "New confidential epic title " msgstr "" @@ -31336,6 +31339,9 @@ msgstr "" msgid "Proceed" msgstr "" +msgid "Product Analytics|Onboarding view" +msgstr "" + msgid "Product analytics" msgstr "" @@ -35502,6 +35508,9 @@ msgstr "" msgid "Runners|IP Address" msgstr "" +msgid "Runners|Idle" +msgstr "" + msgid "Runners|Install a runner" msgstr "" @@ -35708,6 +35717,9 @@ msgstr "" msgid "Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator." msgstr "" +msgid "Runners|Running" +msgstr "" + msgid "Runners|Runs untagged jobs" msgstr "" @@ -38056,6 +38068,9 @@ msgstr "" msgid "Shared Runners" msgstr "" +msgid "Shared Runners:" +msgstr "" + msgid "Shared projects" msgstr "" @@ -46704,9 +46719,6 @@ msgstr "" msgid "WorkItem|Incident" msgstr "" -msgid "WorkItem|Introducing tasks" -msgstr "" - msgid "WorkItem|Issue" msgstr "" @@ -46716,9 +46728,6 @@ msgstr "" msgid "WorkItem|Key result" msgstr "" -msgid "WorkItem|Learn about tasks." -msgstr "" - msgid "WorkItem|Milestone" msgstr "" @@ -46836,9 +46845,6 @@ msgstr "" msgid "WorkItem|Undo" msgstr "" -msgid "WorkItem|Use tasks to break down your work in an issue into smaller pieces. %{learnMoreLink}" -msgstr "" - msgid "WorkItem|View current version" msgstr "" diff --git a/sidekiq_cluster/cli.rb b/sidekiq_cluster/cli.rb index 341ebd9019a..760a5f14c2d 100644 --- a/sidekiq_cluster/cli.rb +++ b/sidekiq_cluster/cli.rb @@ -112,7 +112,7 @@ module Gitlab end def start_and_supervise_workers(queue_groups) - worker_pids = SidekiqCluster.start( + wait_threads = SidekiqCluster.start( queue_groups, env: @environment, directory: @rails_path, @@ -135,6 +135,7 @@ module Gitlab ) metrics_server_pid = start_metrics_server + worker_pids = wait_threads.map(&:pid) supervisor.supervise(worker_pids + Array(metrics_server_pid)) do |dead_pids| # If we're not in the process of shutting down the cluster, # and the metrics server died, restart it. @@ -149,6 +150,13 @@ module Gitlab [] end end + + exit_statuses = wait_threads.map do |thread| + thread.join + thread.value + end + + exit 1 unless exit_statuses.compact.all?(&:success?) end def start_metrics_server diff --git a/sidekiq_cluster/sidekiq_cluster.rb b/sidekiq_cluster/sidekiq_cluster.rb index 66fb5603d2b..1ed08e7e839 100644 --- a/sidekiq_cluster/sidekiq_cluster.rb +++ b/sidekiq_cluster/sidekiq_cluster.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../lib/gitlab/process_management' +require_relative '../lib/gitlab/process_supervisor' module Gitlab module SidekiqCluster @@ -33,7 +34,8 @@ module Gitlab # # directory - The directory of the Rails application. # - # Returns an Array containing the PIDs of the started processes. + # Returns an Array containing the waiter threads (from Process.detach) of + # the started processes. def self.start(queues, env: :development, directory: Dir.pwd, max_concurrency: 20, min_concurrency: 0, timeout: DEFAULT_SOFT_TIMEOUT_SECONDS, dryrun: false) queues.map.with_index do |pair, index| start_sidekiq(pair, env: env, @@ -82,9 +84,7 @@ module Gitlab ) end - ProcessManagement.wait_async(pid) - - pid + Process.detach(pid) end def self.count_by_queue(queues) diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb index 4618c6681d3..c2ea9455de6 100644 --- a/spec/commands/sidekiq_cluster/cli_spec.rb +++ b/spec/commands/sidekiq_cluster/cli_spec.rb @@ -299,11 +299,11 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo end context 'starting the server' do - context 'without --dryrun' do - before do - allow(Gitlab::SidekiqCluster).to receive(:start).and_return([]) - end + before do + allow(Gitlab::SidekiqCluster).to receive(:start).and_return([]) + end + context 'without --dryrun' do it 'wipes the metrics directory before starting workers' do expect(metrics_cleanup_service).to receive(:execute).ordered expect(Gitlab::SidekiqCluster).to receive(:start).ordered.and_return([]) @@ -403,9 +403,42 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo let(:sidekiq_exporter_enabled) { true } let(:metrics_server_pid) { 99 } let(:sidekiq_worker_pids) { [2, 42] } + let(:waiter_threads) { [instance_double('Process::Waiter'), instance_double('Process::Waiter')] } + let(:process_status) { instance_double('Process::Status') } before do - allow(Gitlab::SidekiqCluster).to receive(:start).and_return(sidekiq_worker_pids) + allow(Gitlab::SidekiqCluster).to receive(:start).and_return(waiter_threads) + allow(process_status).to receive(:success?).and_return(true) + allow(cli).to receive(:exit) + + waiter_threads.each.with_index do |thread, i| + allow(thread).to receive(:join) + allow(thread).to receive(:pid).and_return(sidekiq_worker_pids[i]) + allow(thread).to receive(:value).and_return(process_status) + end + end + + context 'when one of the workers has been terminated gracefully' do + it 'stops the entire process cluster' do + expect(MetricsServer).to receive(:start_for_sidekiq).once.and_return(metrics_server_pid) + expect(supervisor).to receive(:supervise).and_yield([2, 99]) + expect(supervisor).to receive(:shutdown) + expect(cli).not_to receive(:exit).with(1) + + cli.run(%w(foo)) + end + end + + context 'when one of the workers has failed' do + it 'stops the entire process cluster and exits with a non-zero code' do + expect(MetricsServer).to receive(:start_for_sidekiq).once.and_return(metrics_server_pid) + expect(supervisor).to receive(:supervise).and_yield([2, 99]) + expect(supervisor).to receive(:shutdown) + expect(process_status).to receive(:success?).and_return(false) + expect(cli).to receive(:exit).with(1) + + cli.run(%w(foo)) + end end it 'stops the entire process cluster if one of the workers has been terminated' do diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 9e0a507626a..072cfacbe3b 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -2,16 +2,31 @@ require 'spec_helper' +# Flaky spec warning: the queries in this file routinely exceed the defined GraphQL query limit of 100. +# Until those queries are optimized, we need to disable query limit checking in order for these tests +# to pass consistently. Note that removing the disabling code can lead to flaky failures locally and in CI. +# +# In addition, it seems as though the use of `let_it_be` might be causing some of the +# flakiness, as discussed in https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#modifiers. +# `reload: true` has been added to all `let_it_be` statements. +# +# See: +# - https://gitlab.com/gitlab-org/gitlab/-/issues/323426 +# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56458#note_535900110 +# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102719 +# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105849 +# - https://gitlab.com/gitlab-org/gitlab/-/issues/383970 +# RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do include DragTo include MobileHelpers include BoardHelpers - let_it_be(:group) { create(:group, :nested) } - let_it_be(:project) { create(:project, :public, namespace: group) } - let_it_be(:board) { create(:board, project: project) } - let_it_be(:user) { create(:user) } - let_it_be(:user2) { create(:user) } + let_it_be(:group, reload: true) { create(:group, :nested) } + let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } + let_it_be(:board, reload: true) { create(:board, project: project) } + let_it_be(:user, reload: true) { create(:user) } + let_it_be(:user2, reload: true) { create(:user) } let(:filtered_search) { find('[data-testid="issue-board-filtered-search"]') } let(:filter_input) { find('.gl-filtered-search-term-input') } @@ -47,31 +62,31 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do end context 'with lists' do - let_it_be(:milestone) { create(:milestone, project: project) } - - let_it_be(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') } - let_it_be(:development) { create(:label, project: project, name: 'Development') } - let_it_be(:testing) { create(:label, project: project, name: 'Testing') } - let_it_be(:bug) { create(:label, project: project, name: 'Bug') } - let_it_be(:backlog) { create(:label, project: project, name: 'Backlog') } - let_it_be(:closed) { create(:label, project: project, name: 'Closed') } - let_it_be(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } - let_it_be(:a_plus) { create(:label, project: project, name: 'A+') } - let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } - let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } - let_it_be(:backlog_list) { create(:backlog_list, board: board) } - - let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } - let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) } - let_it_be(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) } - let_it_be(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) } - let_it_be(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) } - let_it_be(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) } - let_it_be(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) } - let_it_be(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } - let_it_be(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') } - let_it_be(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } - let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } + let_it_be(:milestone, reload: true) { create(:milestone, project: project) } + + let_it_be(:planning, reload: true) { create(:label, project: project, name: 'Planning', description: 'Test') } + let_it_be(:development, reload: true) { create(:label, project: project, name: 'Development') } + let_it_be(:testing, reload: true) { create(:label, project: project, name: 'Testing') } + let_it_be(:bug, reload: true) { create(:label, project: project, name: 'Bug') } + let_it_be(:backlog, reload: true) { create(:label, project: project, name: 'Backlog') } + let_it_be(:closed, reload: true) { create(:label, project: project, name: 'Closed') } + let_it_be(:accepting, reload: true) { create(:label, project: project, name: 'Accepting Merge Requests') } + let_it_be(:a_plus, reload: true) { create(:label, project: project, name: 'A+') } + let_it_be(:list1, reload: true) { create(:list, board: board, label: planning, position: 0) } + let_it_be(:list2, reload: true) { create(:list, board: board, label: development, position: 1) } + let_it_be(:backlog_list, reload: true) { create(:backlog_list, board: board) } + + let_it_be(:confidential_issue, reload: true) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } + let_it_be(:issue1, reload: true) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) } + let_it_be(:issue2, reload: true) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) } + let_it_be(:issue3, reload: true) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) } + let_it_be(:issue4, reload: true) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) } + let_it_be(:issue5, reload: true) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) } + let_it_be(:issue6, reload: true) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) } + let_it_be(:issue7, reload: true) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } + let_it_be(:issue8, reload: true) { create(:closed_issue, project: project, title: 'hhh', description: '888') } + let_it_be(:issue9, reload: true) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } + let_it_be(:issue10, reload: true) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } before do visit_project_board(project, board) @@ -125,7 +140,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do it 'infinite scrolls list' do create_list(:labeled_issue, 30, project: project, labels: [planning]) - visit_project_board(project, board) + visit_project_board_path_without_query_limit(project, board) page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('38') @@ -204,31 +219,26 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) # Make sure list positions are preserved after a reload - visit_project_board(project, board) + visit_project_board_path_without_query_limit(project, board) expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) end context 'without backlog and closed lists' do - let_it_be(:board) { create(:board, project: project, hide_backlog_list: true, hide_closed_list: true) } - let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } - let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } + let_it_be(:board, reload: true) { create(:board, project: project, hide_backlog_list: true, hide_closed_list: true) } + let_it_be(:list1, reload: true) { create(:list, board: board, label: planning, position: 0) } + let_it_be(:list2, reload: true) { create(:list, board: board, label: development, position: 1) } it 'changes position of list' do - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - visit_project_board(project, board) - end + visit_project_board_path_without_query_limit(project, board) drag(list_from_index: 0, list_to_index: 1, selector: '.board-header') expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - # Make sure list positions are preserved after a reload - visit_project_board(project, board) - end + visit_project_board_path_without_query_limit(project, board) expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) @@ -531,7 +541,7 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do end context 'as guest user' do - let_it_be(:user_guest) { create(:user) } + let_it_be(:user_guest, reload: true) { create(:user) } before do stub_feature_flags(apollo_boards: false) @@ -601,4 +611,10 @@ RSpec.describe 'Project issue boards', :js, feature_category: :team_planning do wait_for_requests end + + def visit_project_board_path_without_query_limit(project, board) + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + visit_project_board(project, board) + end + end end diff --git a/spec/features/clusters/create_agent_spec.rb b/spec/features/clusters/create_agent_spec.rb index d01fa520cb0..01902c36e99 100644 --- a/spec/features/clusters/create_agent_spec.rb +++ b/spec/features/clusters/create_agent_spec.rb @@ -30,8 +30,8 @@ RSpec.describe 'Cluster agent registration', :js, feature_category: :kubernetes_ click_button('Connect a cluster') expect(page).to have_content('Register') - click_button('Select an agent') - click_button('example-agent-2') + click_button('Select an agent or enter a name to create new') + page.find('li', text: 'example-agent-2').click click_button('Register') expect(page).to have_content('You cannot see this token again after you close this window.') diff --git a/spec/features/merge_request/user_assigns_themselves_reviewer_spec.rb b/spec/features/merge_request/user_assigns_themselves_reviewer_spec.rb new file mode 100644 index 00000000000..2b93f88e96b --- /dev/null +++ b/spec/features/merge_request/user_assigns_themselves_reviewer_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Merge request > User assigns themselves as a reviewer', feature_category: :code_review do + let_it_be(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "test mr") } + + context 'when logged in as a member of the project' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'updates updated_by', :js do + wait_for_requests + + expect do + page.within('.reviewer') do + click_button 'assign yourself' + end + + expect(find('.reviewer')).to have_content(user.name) + wait_for_all_requests + end.to change { merge_request.reload.updated_at } + end + + context 'when logged in as a non-member of the project' do + before do + sign_in(create(:user)) + visit project_merge_request_path(project, merge_request) + end + + it 'does not show link to assign self as Reviewer' do + page.within('.reviewer') do + expect(page).not_to have_content 'Assign yourself' + end + end + end + end +end diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb index 8244668b444..826904bd165 100644 --- a/spec/features/merge_request/user_assigns_themselves_spec.rb +++ b/spec/features/merge_request/user_assigns_themselves_spec.rb @@ -22,8 +22,12 @@ RSpec.describe 'Merge request > User assigns themselves', feature_category: :cod end it 'updates updated_by', :js do + wait_for_requests + expect do - click_button 'assign yourself' + page.within('[data-testid="assignee-block-container"]') do + click_button 'assign yourself' + end expect(find('.assignee')).to have_content(user.name) wait_for_all_requests @@ -36,7 +40,9 @@ RSpec.describe 'Merge request > User assigns themselves', feature_category: :cod end it 'does not display if related issues are already assigned' do - expect(page).not_to have_content 'Assign yourself' + page.within('[data-testid="assignee-block-container"]') do + expect(page).not_to have_content 'Assign yourself' + end end end end @@ -48,7 +54,9 @@ RSpec.describe 'Merge request > User assigns themselves', feature_category: :cod end it 'does not show assignment link' do - expect(page).not_to have_content 'Assign yourself' + page.within('[data-testid="assignee-block-container"]') do + expect(page).not_to have_content 'Assign yourself' + end end end end diff --git a/spec/features/projects/integrations/user_activates_flowdock_spec.rb b/spec/features/projects/integrations/user_activates_flowdock_spec.rb deleted file mode 100644 index 9786ea9a692..00000000000 --- a/spec/features/projects/integrations/user_activates_flowdock_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'User activates Flowdock', feature_category: :integrations do - include_context 'project integration activation' do - let(:project) { create(:project, :repository) } - end - - before do - stub_request(:post, /.*api.flowdock.com.*/) - end - - it 'activates integration', :js do - visit_project_integration('Flowdock') - fill_in('Token', with: 'verySecret') - - click_test_then_save_integration(expect_test_to_fail: false) - - expect(page).to have_content('Flowdock settings saved and active.') - end -end diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson index e5d39512255..2d092d4e916 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson +++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson @@ -7,7 +7,6 @@ {"id":95,"project_id":5,"created_at":"2016-06-14T15:01:51.255Z","updated_at":"2016-06-14T15:01:51.255Z","active":false,"properties":{"api_url":"","jira_issue_transition_id":"2"},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"JiraService","category":"issue_tracker","default":false,"wiki_page_events":true} {"id":94,"project_id":5,"created_at":"2016-06-14T15:01:51.232Z","updated_at":"2016-06-14T15:01:51.232Z","active":true,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"IrkerService","category":"common","default":false,"wiki_page_events":true} {"id":93,"project_id":5,"created_at":"2016-06-14T15:01:51.219Z","updated_at":"2016-06-14T15:01:51.219Z","active":false,"properties":{"notify_only_broken_pipelines":true},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"pipeline_events":true,"type":"HipchatService","category":"common","default":false,"wiki_page_events":true} -{"id":91,"project_id":5,"created_at":"2016-06-14T15:01:51.182Z","updated_at":"2016-06-14T15:01:51.182Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"FlowdockService","category":"common","default":false,"wiki_page_events":true} {"id":90,"project_id":5,"created_at":"2016-06-14T15:01:51.166Z","updated_at":"2016-06-14T15:01:51.166Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"ExternalWikiService","category":"common","default":false,"wiki_page_events":true} {"id":89,"project_id":5,"created_at":"2016-06-14T15:01:51.153Z","updated_at":"2016-06-14T15:01:51.153Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"EmailsOnPushService","category":"common","default":false,"wiki_page_events":true} {"id":88,"project_id":5,"created_at":"2016-06-14T15:01:51.139Z","updated_at":"2016-06-14T15:01:51.139Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"DroneCiService","category":"ci","default":false,"wiki_page_events":true} diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js index 03ecbc01a56..3d4eddedf34 100644 --- a/spec/frontend/batch_comments/components/draft_note_spec.js +++ b/spec/frontend/batch_comments/components/draft_note_spec.js @@ -1,6 +1,5 @@ import { nextTick } from 'vue'; import { GlButton, GlBadge } from '@gitlab/ui'; -import { getByRole } from '@testing-library/dom'; import { shallowMount } from '@vue/test-utils'; import { stubComponent } from 'helpers/stub_component'; import DraftNote from '~/batch_comments/components/draft_note.vue'; @@ -14,6 +13,7 @@ const NoteableNoteStub = stubComponent(NoteableNote, { template: ` <div> <slot name="note-header-info">Test</slot> + <slot name="after-note-body">Test</slot> </div> `, }); @@ -29,7 +29,6 @@ describe('Batch comments draft note component', () => { }, }; - const getList = () => getByRole(wrapper.element, 'list'); const findSubmitReviewButton = () => wrapper.findComponent(PublishButton); const findAddCommentButton = () => wrapper.findComponent(GlButton); @@ -189,7 +188,7 @@ describe('Batch comments draft note component', () => { }); it(`calls store ${expectedCalls.length} times on ${event}`, () => { - getList().dispatchEvent(new MouseEvent(event, { bubbles: true })); + wrapper.element.dispatchEvent(new MouseEvent(event, { bubbles: true })); expect(store.dispatch.mock.calls).toEqual(expectedCalls); }); }); diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js index 20463a63419..2bd7b20512d 100644 --- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js @@ -3,6 +3,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import RunnerTags from '~/ci/runner/components/runner_tags.vue'; +import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue'; import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -16,6 +17,7 @@ describe('RunnerTypeCell', () => { let wrapper; const findLockIcon = () => wrapper.findByTestId('lock-icon'); + const findRunnerJobStatusBadge = () => wrapper.findComponent(RunnerJobStatusBadge); const findRunnerTags = () => wrapper.findComponent(RunnerTags); const findRunnerSummaryField = (icon) => wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon) @@ -80,6 +82,10 @@ describe('RunnerTypeCell', () => { expect(wrapper.text()).toContain(mockRunner.description); }); + it('Displays job execution status', () => { + expect(findRunnerJobStatusBadge().props('jobStatus')).toBe(mockRunner.jobExecutionStatus); + }); + it('Displays last contact', () => { createComponent({ contactedAt: '2022-01-02', diff --git a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js new file mode 100644 index 00000000000..8cd3fb3cb4c --- /dev/null +++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js @@ -0,0 +1,44 @@ +import { GlBadge } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue'; +import { + I18N_JOB_STATUS_RUNNING, + I18N_JOB_STATUS_IDLE, + JOB_STATUS_RUNNING, + JOB_STATUS_IDLE, +} from '~/ci/runner/constants'; + +describe('RunnerTypeBadge', () => { + let wrapper; + + const findBadge = () => wrapper.findComponent(GlBadge); + + const createComponent = (props = {}) => { + wrapper = shallowMount(RunnerJobStatusBadge, { + propsData: { + ...props, + }, + }); + }; + + it.each` + jobStatus | classes | text + ${JOB_STATUS_RUNNING} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-blue-600!', 'gl-border', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING} + ${JOB_STATUS_IDLE} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-gray-700!', 'gl-border', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE} + `( + 'renders $jobStatus job status with "$text" text and styles', + ({ jobStatus, classes, text }) => { + createComponent({ jobStatus }); + + expect(findBadge().props()).toMatchObject({ size: 'sm', variant: 'muted' }); + expect(findBadge().classes().sort()).toEqual(classes.sort()); + expect(findBadge().text()).toBe(text); + }, + ); + + it('does not render an unknown status', () => { + createComponent({ jobStatus: 'UNKNOWN_STATUS' }); + + expect(wrapper.html()).toBe(''); + }); +}); diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js index e656a601699..a92a03fedb6 100644 --- a/spec/frontend/clusters_list/components/agent_token_spec.js +++ b/spec/frontend/clusters_list/components/agent_token_spec.js @@ -1,10 +1,12 @@ -import { GlAlert, GlFormInputGroup } from '@gitlab/ui'; +import { GlAlert, GlFormInputGroup, GlSprintf, GlLink, GlIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { sprintf } from '~/locale'; import AgentToken from '~/clusters_list/components/agent_token.vue'; import { I18N_AGENT_TOKEN, INSTALL_AGENT_MODAL_ID, NAME_MAX_LENGTH, + HELM_VERSION_POLICY_URL, } from '~/clusters_list/constants'; import { generateAgentRegistrationCommand } from '~/clusters_list/clusters_util'; import CodeBlock from '~/vue_shared/components/code_block.vue'; @@ -23,6 +25,8 @@ describe('InstallAgentModal', () => { const findCodeBlock = () => wrapper.findComponent(CodeBlock); const findCopyButton = () => wrapper.findComponent(ModalCopyButton); const findInput = () => wrapper.findComponent(GlFormInputGroup); + const findHelmVersionPolicyLink = () => wrapper.findComponent(GlLink); + const findHelmExternalLinkIcon = () => wrapper.findComponent(GlIcon); const createWrapper = (newAgentName = agentName) => { const provide = { @@ -39,6 +43,9 @@ describe('InstallAgentModal', () => { wrapper = shallowMountExtended(AgentToken, { provide, propsData, + stubs: { + GlSprintf, + }, }); }; @@ -56,6 +63,17 @@ describe('InstallAgentModal', () => { expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallBody); }); + it('shows Helm version policy text with an external link', () => { + expect(wrapper.text()).toContain( + sprintf(I18N_AGENT_TOKEN.helmVersionText, { linkStart: '', linkEnd: ' ' }), + ); + expect(findHelmVersionPolicyLink().attributes()).toMatchObject({ + href: HELM_VERSION_POLICY_URL, + target: '_blank', + }); + expect(findHelmExternalLinkIcon().props()).toMatchObject({ name: 'external-link', size: 12 }); + }); + it('shows advanced agent installation instructions', () => { expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.advancedInstallTitle); }); diff --git a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js index 197735d3c77..0a8447c5d80 100644 --- a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js +++ b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js @@ -1,34 +1,32 @@ -import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { GlSearchBoxByType, GlCollapsibleListbox, GlButton } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { ENTER_KEY } from '~/lib/utils/keys'; import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue'; import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants'; describe('AvailableAgentsDropdown', () => { let wrapper; + const configuredAgent = 'configured-agent'; + const searchAgentName = 'search-agent'; + const newAgentName = 'new-agent'; + const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN; - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findFirstAgentItem = () => findDropdownItems().at(0); - const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType); - const findCreateButton = () => wrapper.findByTestId('create-config-button'); + const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); + const findSearchInput = () => findDropdown().findComponent(GlSearchBoxByType); + const findCreateButton = () => wrapper.findComponent(GlButton); const createWrapper = ({ propsData }) => { wrapper = shallowMountExtended(AvailableAgentsDropdown, { propsData, - stubs: { GlDropdown }, + stubs: { GlCollapsibleListbox }, }); - wrapper.vm.$refs.dropdown.hide = jest.fn(); + wrapper.vm.$refs.dropdown.closeAndFocus = jest.fn(); }; - afterEach(() => { - wrapper.destroy(); - }); - describe('there are agents available', () => { const propsData = { - availableAgents: ['configured-agent', 'search-agent', 'test-agent'], + availableAgents: [configuredAgent, searchAgentName, 'test-agent'], isRegistering: false, }; @@ -37,91 +35,92 @@ describe('AvailableAgentsDropdown', () => { }); it('prompts to select an agent', () => { - expect(findDropdown().props('text')).toBe(i18n.selectAgent); + expect(findDropdown().props('toggleText')).toBe(i18n.selectAgent); }); describe('search agent', () => { it('renders search button', () => { - expect(findSearchInput().exists()).toBe(true); + expect(findDropdown().props('searchable')).toBe(true); }); it('renders all agents when search term is empty', () => { - expect(findDropdownItems()).toHaveLength(3); + expect(findDropdown().props('items')).toHaveLength(3); }); it('renders only the agent searched for when the search item exists', async () => { - await findSearchInput().vm.$emit('input', 'search-agent'); - - expect(findDropdownItems()).toHaveLength(1); - expect(findFirstAgentItem().text()).toBe('search-agent'); - }); + findSearchInput().vm.$emit('input', searchAgentName); + await nextTick(); - it('renders create button when search started', async () => { - await findSearchInput().vm.$emit('input', 'new-agent'); - - expect(findCreateButton().exists()).toBe(true); + expect(findDropdown().props('items')).toMatchObject([ + { text: searchAgentName, value: searchAgentName }, + ]); }); - it("doesn't render create button when search item is found", async () => { - await findSearchInput().vm.$emit('input', 'search-agent'); - - expect(findCreateButton().exists()).toBe(false); + describe('create button', () => { + it.each` + condition | search | createButtonRendered + ${'is rendered'} | ${newAgentName} | ${true} + ${'is not rendered'} | ${''} | ${false} + ${'is not rendered'} | ${searchAgentName} | ${false} + `('$condition when search is "$search"', async ({ search, createButtonRendered }) => { + findSearchInput().vm.$emit('input', search); + await nextTick(); + + expect(findCreateButton().exists()).toBe(createButtonRendered); + }); }); }); describe('select existing agent configuration', () => { beforeEach(() => { - findFirstAgentItem().vm.$emit('click'); + findDropdown().vm.$emit('select', configuredAgent); }); - it('emits agentSelected with the name of the clicked agent', () => { - expect(wrapper.emitted('agentSelected')).toEqual([['configured-agent']]); + it('emits `agentSelected` with the name of the clicked agent', () => { + expect(wrapper.emitted('agentSelected')).toEqual([[configuredAgent]]); }); it('marks the clicked item as selected', () => { - expect(findDropdown().props('text')).toBe('configured-agent'); - expect(findFirstAgentItem().props('isChecked')).toBe(true); + expect(findDropdown().props('toggleText')).toBe(configuredAgent); }); }); describe('create new agent configuration', () => { beforeEach(async () => { - await findSearchInput().vm.$emit('input', 'new-agent'); + findSearchInput().vm.$emit('input', newAgentName); + await nextTick(); findCreateButton().vm.$emit('click'); }); it('emits agentSelected with the name of the clicked agent', () => { - expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]); + expect(wrapper.emitted('agentSelected')).toEqual([[newAgentName]]); }); it('marks the clicked item as selected', () => { - expect(findDropdown().props('text')).toBe('new-agent'); + expect(findDropdown().props('toggleText')).toBe(newAgentName); }); }); describe('click enter to register new agent without configuration', () => { beforeEach(async () => { - await findSearchInput().vm.$emit('input', 'new-agent'); - await findSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY })); + findSearchInput().vm.$emit('input', newAgentName); + await nextTick(); + await findDropdown().trigger('keydown.enter'); }); it('emits agentSelected with the name of the clicked agent', () => { - expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]); + expect(wrapper.emitted('agentSelected')).toEqual([[newAgentName]]); }); it('marks the clicked item as selected', () => { - expect(findDropdown().props('text')).toBe('new-agent'); - }); - - it('closes the dropdown', () => { - expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1); + expect(findDropdown().props('toggleText')).toBe(newAgentName); }); }); }); describe('registration in progress', () => { const propsData = { - availableAgents: ['configured-agent'], + availableAgents: [configuredAgent], isRegistering: true, }; @@ -130,7 +129,7 @@ describe('AvailableAgentsDropdown', () => { }); it('updates the text in the dropdown', () => { - expect(findDropdown().props('text')).toBe(i18n.registeringAgent); + expect(findDropdown().props('toggleText')).toBe(i18n.registeringAgent); }); it('displays a loading icon', () => { diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js index 5fc5a57ee5b..7bd9afab648 100644 --- a/spec/frontend/diffs/components/diff_code_quality_spec.js +++ b/spec/frontend/diffs/components/diff_code_quality_spec.js @@ -2,11 +2,13 @@ import { GlIcon } from '@gitlab/ui'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue'; import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants'; +import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n'; import { multipleFindingsArr } from '../mock_data/diff_code_quality'; let wrapper; const findIcon = () => wrapper.findComponent(GlIcon); +const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`); describe('DiffCodeQuality', () => { afterEach(() => { @@ -30,14 +32,17 @@ describe('DiffCodeQuality', () => { expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1); }); - it('renders correct amount of list items for codequality array and their description', async () => { + it('renders heading and correct amount of list items for codequality array and their description', async () => { wrapper = createWrapper(multipleFindingsArr); - const listItems = wrapper.findAll('li'); + expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS); + const listItems = wrapper.findAll('li'); expect(wrapper.findAll('li').length).toBe(5); listItems.wrappers.map((e, i) => { - return expect(e.text()).toEqual(multipleFindingsArr[i].description); + return expect(e.text()).toContain( + `${multipleFindingsArr[i].severity} - ${multipleFindingsArr[i].description}`, + ); }); }); diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js index 5e68725c03e..26af7af701b 100644 --- a/spec/frontend/filtered_search/filtered_search_manager_spec.js +++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js @@ -8,7 +8,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes'; import { visitUrl, getParameterByName } from '~/lib/utils/url_utility'; @@ -130,14 +130,14 @@ describe('Filtered Search Manager', () => { manager = new FilteredSearchManager({ page }); }); - it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => { + it('should not show an alert if an RecentSearchesServiceError is caught', () => { jest .spyOn(RecentSearchesService.prototype, 'fetch') .mockImplementation(() => Promise.reject(new RecentSearchesServiceError())); manager.setup(); - expect(createFlash).not.toHaveBeenCalled(); + expect(createAlert).not.toHaveBeenCalled(); }); }); diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js index e52ffa7bd9f..43c10090739 100644 --- a/spec/frontend/filtered_search/visual_token_value_spec.js +++ b/spec/frontend/filtered_search/visual_token_value_spec.js @@ -5,7 +5,7 @@ import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper'; import { TEST_HOST } from 'helpers/test_constants'; import DropdownUtils from '~/filtered_search/dropdown_utils'; import VisualTokenValue from '~/filtered_search/visual_token_value'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; @@ -61,7 +61,7 @@ describe('Filtered Search Visual Tokens', () => { }; await subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue); - expect(createFlash.mock.calls.length).toBe(0); + expect(createAlert).toHaveBeenCalledTimes(0); }); it('does nothing if user cannot be found', async () => { diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js new file mode 100644 index 00000000000..57ae146a27a --- /dev/null +++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -0,0 +1,77 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import axios from 'axios'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.vue'; +import SidebarService from '~/sidebar/services/sidebar_service'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; +import SidebarStore from '~/sidebar/stores/sidebar_store'; +import Mock from '../../mock_data'; + +Vue.use(VueApollo); + +describe('sidebar reviewers', () => { + const apolloMock = createMockApollo(); + let wrapper; + let mediator; + let axiosMock; + + const createComponent = (props) => { + wrapper = shallowMount(SidebarReviewers, { + apolloProvider: apolloMock, + propsData: { + issuableIid: '1', + issuableId: 1, + mediator, + field: '', + projectPath: 'projectPath', + changing: false, + ...props, + }, + // Attaching to document is required because this component emits something from the parent element :/ + attachTo: document.body, + }); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + mediator = new SidebarMediator(Mock.mediator); + + jest.spyOn(mediator, 'saveReviewers'); + jest.spyOn(mediator, 'addSelfReview'); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + SidebarService.singleton = null; + SidebarStore.singleton = null; + SidebarMediator.singleton = null; + axiosMock.restore(); + }); + + it('calls the mediator when it saves the reviewers', () => { + createComponent(); + + expect(mediator.saveReviewers).not.toHaveBeenCalled(); + + wrapper.vm.saveReviewers(); + + expect(mediator.saveReviewers).toHaveBeenCalled(); + }); + + it('calls the mediator when "reviewBySelf" method is called', () => { + createComponent(); + + expect(mediator.addSelfReview).not.toHaveBeenCalled(); + expect(mediator.store.reviewers.length).toBe(0); + + wrapper.vm.reviewBySelf(); + + expect(mediator.addSelfReview).toHaveBeenCalled(); + expect(mediator.store.reviewers.length).toBe(1); + }); +}); diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index 254489e49a5..cdb9ced70b8 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -43,6 +43,13 @@ describe('Sidebar mediator', () => { }); }); + it('assigns yourself as a reviewer', () => { + mediator.addSelfReview(); + + expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser); + expect(mediator.store.reviewers[0]).toEqual(mediatorMockData.currentUser); + }); + describe('saves reviewers', () => { const mockUpdateResponseData = { reviewers: [1, 2], diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index a016960a85a..2a347892973 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -12,7 +12,6 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import setWindowLocation from 'helpers/set_window_location_helper'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import WorkItemDetail from '~/work_items/components/work_item_detail.vue'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; @@ -22,7 +21,6 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue'; -import WorkItemInformation from '~/work_items/components/work_item_information.vue'; import { i18n } from '~/work_items/constants'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; @@ -32,7 +30,6 @@ import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assign import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; -import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { mockParent, workItemDatesSubscriptionResponse, @@ -45,7 +42,6 @@ import { describe('WorkItemDetail component', () => { let wrapper; - useLocalStorageSpy(); Vue.use(VueApollo); @@ -82,8 +78,6 @@ describe('WorkItemDetail component', () => { const findParentButton = () => findParent().findComponent(GlButton); const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]'); const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]'); - const findWorkItemInformationAlert = () => wrapper.findComponent(WorkItemInformation); - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const createComponent = ({ isModal = false, @@ -93,6 +87,7 @@ describe('WorkItemDetail component', () => { subscriptionHandler = titleSubscriptionHandler, confidentialityMock = [updateWorkItemMutation, jest.fn()], error = undefined, + workItemsMvcEnabled = false, workItemsMvc2Enabled = false, fetchByIid = false, } = {}) => { @@ -117,6 +112,7 @@ describe('WorkItemDetail component', () => { }, provide: { glFeatures: { + workItemsMvc: workItemsMvcEnabled, workItemsMvc2: workItemsMvc2Enabled, useIidInWorkItemsPath: fetchByIid, }, @@ -579,7 +575,7 @@ describe('WorkItemDetail component', () => { `('$description', async ({ milestoneWidgetPresent, exists }) => { const response = workItemResponseFactory({ milestoneWidgetPresent }); const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler, workItemsMvc2Enabled: true }); + createComponent({ handler, workItemsMvcEnabled: true }); await waitForPromises(); expect(findWorkItemMilestone().exists()).toBe(exists); @@ -610,24 +606,6 @@ describe('WorkItemDetail component', () => { }); }); - describe('work item information', () => { - beforeEach(() => { - createComponent(); - return waitForPromises(); - }); - - it('is visible when viewed for the first time and sets localStorage value', async () => { - localStorage.clear(); - expect(findWorkItemInformationAlert().exists()).toBe(true); - expect(findLocalStorageSync().props('value')).toBe(true); - }); - - it('is not visible after reading local storage input', async () => { - await findLocalStorageSync().vm.$emit('input', false); - expect(findWorkItemInformationAlert().exists()).toBe(false); - }); - }); - it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => { createComponent(); await waitForPromises(); diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js deleted file mode 100644 index 887c5f615e9..00000000000 --- a/spec/frontend/work_items/components/work_item_information_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlAlert, GlLink } from '@gitlab/ui'; -import WorkItemInformation from '~/work_items/components/work_item_information.vue'; -import { helpPagePath } from '~/helpers/help_page_helper'; - -const createComponent = () => mount(WorkItemInformation); - -describe('Work item information alert', () => { - let wrapper; - const tasksHelpPath = helpPagePath('user/tasks'); - - const findAlert = () => wrapper.findComponent(GlAlert); - const findHelpLink = () => wrapper.findComponent(GlLink); - beforeEach(() => { - wrapper = createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('should be visible', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('should emit `work-item-banner-dismissed` event when cross icon is clicked', () => { - findAlert().vm.$emit('dismiss'); - expect(wrapper.emitted('work-item-banner-dismissed').length).toBe(1); - }); - - it('the alert variant should be tip', () => { - expect(findAlert().props('variant')).toBe('tip'); - }); - - it('should have the correct text for title', () => { - expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle); - }); - - it('should have the correct link to work item link', () => { - expect(findHelpLink().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe(tasksHelpPath); - }); -}); diff --git a/spec/graphql/types/projects/service_type_enum_spec.rb b/spec/graphql/types/projects/service_type_enum_spec.rb index f7256910bb0..8b444a08c3b 100644 --- a/spec/graphql/types/projects/service_type_enum_spec.rb +++ b/spec/graphql/types/projects/service_type_enum_spec.rb @@ -23,7 +23,6 @@ RSpec.describe GitlabSchema.types['ServiceType'] do EMAILS_ON_PUSH_SERVICE EWM_SERVICE EXTERNAL_WIKI_SERVICE - FLOWDOCK_SERVICE HANGOUTS_CHAT_SERVICE IRKER_SERVICE JENKINS_SERVICE diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 74a59aa37ce..0450ecc0f21 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -886,17 +886,18 @@ RSpec.describe SearchHelper do end context 'code' do - where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :project_search_tabs, :condition) do - false | false | false | false - true | true | true | true - true | false | false | false - false | true | false | false - false | false | true | true - true | false | true | true + where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do + false | false | nil | false | false + true | true | nil | true | true + true | false | nil | false | false + false | true | nil | false | false + false | false | ref(:project) | true | true + true | false | ref(:project) | false | false end with_them do it 'data item condition is set correctly' do + @project = global_project allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs) allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_code_tab).and_return(feature_flag_tab_enabled) allow(self).to receive(:project_search_tabs?).with(:blobs).and_return(project_search_tabs) @@ -907,16 +908,16 @@ RSpec.describe SearchHelper do end context 'issues' do - where(:feature_flag_tab_enabled, :project_search_tabs, :condition) do - false | false | false - true | true | true - true | false | true - false | true | true + where(:project_search_tabs, :global_search_issues_tab, :condition) do + false | false | false + false | true | true + true | false | true + true | true | true end with_them do it 'data item condition is set correctly' do - allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_issues_tab).and_return(feature_flag_tab_enabled) + allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_issues_tab).and_return(global_search_issues_tab) allow(self).to receive(:project_search_tabs?).with(:issues).and_return(project_search_tabs) expect(search_navigation[:issues][:condition]).to eq(condition) @@ -925,11 +926,11 @@ RSpec.describe SearchHelper do end context 'merge requests' do - where(:feature_flag_tab_enabled, :project_search_tabs, :condition) do - false | false | false - true | true | true - true | false | true - false | true | true + where(:project_search_tabs, :feature_flag_tab_enabled, :condition) do + false | false | false + true | false | true + false | true | true + true | true | true end with_them do @@ -943,16 +944,19 @@ RSpec.describe SearchHelper do end context 'wiki' do - where(:project_search_tabs, :show_elasticsearch_tabs, :condition) do - false | false | false - true | true | true - true | false | true - false | true | true + where(:global_search_wiki_tab, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do + false | false | nil | true | true + false | false | nil | false | false + false | true | nil | false | false + true | false | nil | false | false + true | true | ref(:project) | false | false end with_them do it 'data item condition is set correctly' do + @project = global_project allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs) + allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_wiki_tab).and_return(global_search_wiki_tab) allow(self).to receive(:project_search_tabs?).with(:wiki).and_return(project_search_tabs) expect(search_navigation[:wiki_blobs][:condition]).to eq(condition) @@ -961,17 +965,20 @@ RSpec.describe SearchHelper do end context 'commits' do - where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :project_search_tabs, :condition) do - false | false | false | false - true | true | true | true - true | false | false | false - false | true | true | true + where(:global_search_commits_tab, :show_elasticsearch_tabs, :global_project, :project_search_tabs, :condition) do + false | false | nil | true | true + false | false | nil | false | false + false | true | nil | false | false + true | false | nil | false | false + true | true | ref(:project) | false | false + true | true | nil | false | true end with_them do it 'data item condition is set correctly' do + @project = global_project allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(show_elasticsearch_tabs) - allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_commits_tab).and_return(feature_flag_tab_enabled) + allow(self).to receive(:feature_flag_tab_enabled?).with(:global_search_commits_tab).and_return(global_search_commits_tab) allow(self).to receive(:project_search_tabs?).with(:commits).and_return(project_search_tabs) expect(search_navigation[:commits][:condition]).to eq(condition) @@ -980,11 +987,11 @@ RSpec.describe SearchHelper do end context 'comments' do - where(:show_elasticsearch_tabs, :project_search_tabs, :condition) do - true | true | true - false | false | false - true | false | true - false | true | true + where(:project_search_tabs, :show_elasticsearch_tabs, :condition) do + true | true | true + false | false | false + false | true | true + true | false | true end with_them do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a9ec285f13c..b9867f794dc 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -424,7 +424,6 @@ project: - packagist_integration - pivotaltracker_integration - prometheus_integration -- flowdock_integration - assembla_integration - asana_integration - slack_integration diff --git a/spec/lib/gitlab/process_management_spec.rb b/spec/lib/gitlab/process_management_spec.rb index a71a476b540..fbd39702efb 100644 --- a/spec/lib/gitlab/process_management_spec.rb +++ b/spec/lib/gitlab/process_management_spec.rb @@ -41,15 +41,6 @@ RSpec.describe Gitlab::ProcessManagement do end end - describe '.wait_async' do - it 'waits for a process in a separate thread' do - thread = described_class.wait_async(Process.spawn('true')) - - # Upon success Process.wait just returns the PID. - expect(thread.value).to be_a_kind_of(Numeric) - end - end - # In the X_alive? checks, we check negative PIDs sometimes as a simple way # to be sure the pids are definitely for non-existent processes. # Note that -1 is special, and sends the signal to every process we have permission diff --git a/spec/lib/gitlab/ssh/signature_spec.rb b/spec/lib/gitlab/ssh/signature_spec.rb index f3f1ba84f9e..4868ed68db6 100644 --- a/spec/lib/gitlab/ssh/signature_spec.rb +++ b/spec/lib/gitlab/ssh/signature_spec.rb @@ -151,16 +151,32 @@ RSpec.describe Gitlab::Ssh::Signature do context 'when user email is not verified' do before do + email = user.emails.find_by(email: committer_email) + email.update!(confirmed_at: nil) user.update!(confirmed_at: nil) end - it_behaves_like 'unverified signature' + it 'reports unverified status' do + expect(signature.verification_status).to eq(:unverified) + end + end + + context 'when no user exist with the committer email' do + before do + user.delete + end + + it 'reports other_user status' do + expect(signature.verification_status).to eq(:other_user) + end end context 'when no user exists with the committer email' do let(:committer_email) { 'different-email+ssh-commit-test@example.com' } - it_behaves_like 'unverified signature' + it 'reports other_user status' do + expect(signature.verification_status).to eq(:other_user) + end end context 'when signature is invalid' do diff --git a/spec/models/integrations/flowdock_spec.rb b/spec/models/integrations/flowdock_spec.rb deleted file mode 100644 index daafb1b3958..00000000000 --- a/spec/models/integrations/flowdock_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Integrations::Flowdock do - describe 'Validations' do - context 'when integration is active' do - before do - subject.active = true - end - - it { is_expected.to validate_presence_of(:token) } - end - - context 'when integration is inactive' do - before do - subject.active = false - end - - it { is_expected.not_to validate_presence_of(:token) } - end - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - let(:api_url) { 'https://api.flowdock.com/v1/messages' } - - subject(:flowdock_integration) { described_class.new } - - before do - allow(flowdock_integration).to receive_messages( - project_id: project.id, - project: project, - token: 'verySecret' - ) - WebMock.stub_request(:post, api_url) - end - - it "calls FlowDock API" do - flowdock_integration.execute(sample_data) - - sample_data[:commits].each do |commit| - # One request to Flowdock per new commit - next if commit[:id] == sample_data[:before] - - expect(WebMock).to have_requested(:post, api_url).with( - body: /#{commit[:id]}.*#{project.path}/ - ).once - end - end - end -end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a6dbaef3a66..e800a468f8c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -58,7 +58,6 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_one(:pipelines_email_integration) } it { is_expected.to have_one(:irker_integration) } it { is_expected.to have_one(:pivotaltracker_integration) } - it { is_expected.to have_one(:flowdock_integration) } it { is_expected.to have_one(:assembla_integration) } it { is_expected.to have_one(:slack_slash_commands_integration) } it { is_expected.to have_one(:mattermost_slash_commands_integration) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 18d3fdb2d50..0affc522d4e 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -3627,12 +3627,6 @@ RSpec.describe API::MergeRequests do expect(merge_request.approvals).to be_empty end - it 'for users with bot role' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) - - expect(response).to have_gitlab_http_status(:accepted) - end - context 'for users with non-bot roles' do let(:human_user) { create(:user) } @@ -3642,7 +3636,9 @@ RSpec.describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user) + merge_request.reload expect(response).to have_gitlab_http_status(:unauthorized) + expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id]) end end end @@ -3658,7 +3654,9 @@ RSpec.describe API::MergeRequests do it 'returns 401' do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + merge_request.reload expect(response).to have_gitlab_http_status(:unauthorized) + expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id]) end end @@ -3670,10 +3668,26 @@ RSpec.describe API::MergeRequests do it 'returns 401' do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + merge_request.reload expect(response).to have_gitlab_http_status(:unauthorized) + expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id]) end end end + + context 'for a bot user who approved the merge request' do + before do + merge_request.approvals.create!(user: bot) + end + + it "returns 200" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) + + merge_request.reload + expect(response).to have_gitlab_http_status(:accepted) + expect(merge_request.approvals).to be_empty + end + end end end diff --git a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb index 4a532fa9ecb..b06a5709bd5 100644 --- a/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb +++ b/spec/services/projects/container_repository/gitlab/cleanup_tags_service_spec.rb @@ -49,6 +49,9 @@ RSpec.describe Projects::ContainerRepository::Gitlab::CleanupTagsService do it_behaves_like 'when regex matching everything is specified', delete_expectations: [%w[A], %w[Ba Bb], %w[C D], %w[E]] + it_behaves_like 'when regex matching everything is specified and latest is not kept', + delete_expectations: [%w[latest A], %w[Ba Bb], %w[C D], %w[E]] + it_behaves_like 'when delete regex matching specific tags is used' it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex' diff --git a/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb index 2d034d577ac..7227834b131 100644 --- a/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb +++ b/spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb @@ -51,6 +51,16 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::CleanupTagsService, :c }, supports_caching: true + it_behaves_like 'when regex matching everything is specified and latest is not kept', + delete_expectations: [%w[A Ba Bb C D E latest]], + service_response_extra: { + before_truncate_size: 7, + after_truncate_size: 7, + before_delete_size: 7, + cached_tags_count: 0 + }, + supports_caching: true + it_behaves_like 'when delete regex matching specific tags is used', service_response_extra: { before_truncate_size: 2, diff --git a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb index 822acc3fe0f..25a600405fe 100644 --- a/spec/sidekiq_cluster/sidekiq_cluster_spec.rb +++ b/spec/sidekiq_cluster/sidekiq_cluster_spec.rb @@ -20,13 +20,16 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath "SIDEKIQ_WORKER_ID" => "0" }, "bundle", "exec", "sidekiq", "-c10", "-eproduction", "-t25", "-gqueues:foo", "-rfoo/bar", "-qfoo,1", process_options - ) + ).and_return(1) + expect(Process).to receive(:detach).ordered.with(1) + expect(Process).to receive(:spawn).ordered.with({ "ENABLE_SIDEKIQ_CLUSTER" => "1", "SIDEKIQ_WORKER_ID" => "1" }, "bundle", "exec", "sidekiq", "-c10", "-eproduction", "-t25", "-gqueues:bar,baz", "-rfoo/bar", "-qbar,1", "-qbaz,1", process_options - ) + ).and_return(2) + expect(Process).to receive(:detach).ordered.with(2) described_class.start([%w(foo), %w(bar baz)], env: :production, directory: 'foo/bar', max_concurrency: 20, min_concurrency: 10) end @@ -58,11 +61,13 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath let(:env) { { "ENABLE_SIDEKIQ_CLUSTER" => "1", "SIDEKIQ_WORKER_ID" => first_worker_id.to_s } } let(:args) { ['bundle', 'exec', 'sidekiq', anything, '-eproduction', '-t10', *([anything] * 5)] } + let(:waiter_thread) { instance_double('Process::Waiter') } + it 'starts a Sidekiq process' do allow(Process).to receive(:spawn).and_return(1) + allow(Process).to receive(:detach).with(1).and_return(waiter_thread) - expect(Gitlab::ProcessManagement).to receive(:wait_async).with(1) - expect(described_class.start_sidekiq(%w(foo), **options)).to eq(1) + expect(described_class.start_sidekiq(%w(foo), **options)).to eq(waiter_thread) end it 'handles duplicate queue names' do @@ -70,9 +75,9 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath .to receive(:spawn) .with(env, *args, anything) .and_return(1) + allow(Process).to receive(:detach).with(1).and_return(waiter_thread) - expect(Gitlab::ProcessManagement).to receive(:wait_async).with(1) - expect(described_class.start_sidekiq(%w(foo foo bar baz), **options)).to eq(1) + expect(described_class.start_sidekiq(%w(foo foo bar baz), **options)).to eq(waiter_thread) end it 'runs the sidekiq process in a new process group' do @@ -80,9 +85,9 @@ RSpec.describe Gitlab::SidekiqCluster do # rubocop:disable RSpec/FilePath .to receive(:spawn) .with(anything, *args, a_hash_including(pgroup: true)) .and_return(1) + allow(Process).to receive(:detach).with(1).and_return(waiter_thread) - allow(Gitlab::ProcessManagement).to receive(:wait_async) - expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(1) + expect(described_class.start_sidekiq(%w(foo bar baz), **options)).to eq(waiter_thread) end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 8080a7a3edb..26e9cd6cbd9 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -4261,7 +4261,6 @@ - './spec/features/projects/integrations/user_activates_assembla_spec.rb' - './spec/features/projects/integrations/user_activates_atlassian_bamboo_ci_spec.rb' - './spec/features/projects/integrations/user_activates_emails_on_push_spec.rb' -- './spec/features/projects/integrations/user_activates_flowdock_spec.rb' - './spec/features/projects/integrations/user_activates_irker_spec.rb' - './spec/features/projects/integrations/user_activates_issue_tracker_spec.rb' - './spec/features/projects/integrations/user_activates_jetbrains_teamcity_ci_spec.rb' @@ -8291,7 +8290,6 @@ - './spec/models/integrations/ewm_spec.rb' - './spec/models/integrations/external_wiki_spec.rb' - './spec/models/integrations/field_spec.rb' -- './spec/models/integrations/flowdock_spec.rb' - './spec/models/integrations/hangouts_chat_spec.rb' - './spec/models/integrations/harbor_spec.rb' - './spec/models/integrations/irker_spec.rb' diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 68c0d06e7d0..91e465871a4 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -301,7 +301,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re if resource_name == 'merge request' let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] } - let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] } + let(:reply_id) { all("#{comments_selector} [data-note-id]")[1]['data-note-id'] } it 'can be replied to after resolving' do find('button[data-testid="resolve-discussion-button"]').click diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb index 7acef9efd3a..f70621673d5 100644 --- a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb +++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb @@ -23,6 +23,19 @@ RSpec.shared_examples 'when regex matching everything is specified' do end end +RSpec.shared_examples 'when regex matching everything is specified and latest is not kept' do + |delete_expectations:, service_response_extra: {}, supports_caching: false| + + let(:params) do + { 'name_regex_delete' => '.*', 'keep_latest' => false } + end + + it_behaves_like 'removing the expected tags', + service_response_extra: service_response_extra, + supports_caching: supports_caching, + delete_expectations: delete_expectations +end + RSpec.shared_examples 'when delete regex matching specific tags is used' do |service_response_extra: {}, supports_caching: false| let(:params) do diff --git a/spec/workers/container_registry/delete_container_repository_worker_spec.rb b/spec/workers/container_registry/delete_container_repository_worker_spec.rb index 381e0cc164c..c98bf867c99 100644 --- a/spec/workers/container_registry/delete_container_repository_worker_spec.rb +++ b/spec/workers/container_registry/delete_container_repository_worker_spec.rb @@ -103,6 +103,18 @@ RSpec.describe ContainerRegistry::DeleteContainerRepositoryWorker, :aggregate_fa end end + context 'with container_registry_delete_repository_with_cron_worker disabled' do + before do + stub_feature_flags(container_registry_delete_repository_with_cron_worker: false) + end + + it 'will not delete any container repository' do + expect(::Projects::ContainerRepository::CleanupTagsService).not_to receive(:new) + + expect { perform_work }.to not_change { ContainerRepository.count } + end + end + def expect_next_pending_destruction_container_repository original_method = ContainerRepository.method(:next_pending_destruction) expect(ContainerRepository).to receive(:next_pending_destruction).with(order_by: nil) do @@ -142,5 +154,13 @@ RSpec.describe ContainerRegistry::DeleteContainerRepositoryWorker, :aggregate_fa subject { worker.remaining_work_count } it { is_expected.to eq(described_class::MAX_CAPACITY + 1) } + + context 'with container_registry_delete_repository_with_cron_worker disabled' do + before do + stub_feature_flags(container_registry_delete_repository_with_cron_worker: false) + end + + it { is_expected.to eq(0) } + end end end |