diff options
73 files changed, 769 insertions, 247 deletions
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index 40d9c55c5bd..33efc3c4788 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -96,6 +96,13 @@ retire-js-dependency_scanning: gemnasium-python-dependency_scanning: rules: !reference [".reports:rules:gemnasium-python-dependency_scanning", rules] +yarn-audit-dependency_scanning: + extends: .ds-analyzer + image: "registry.gitlab.com/gitlab-org/security-products/analyzers/npm-audit:1.4.0" + variables: + TOOL: yarn + rules: !reference [".reports:rules:yarn-audit-dependency_scanning", rules] + # Analyze dependencies for malicious behavior # See https://gitlab.com/gitlab-com/gl-security/security-research/package-hunter .package_hunter-base: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 17e17712ff8..c5e936c4382 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -167,6 +167,7 @@ .nodejs-patterns: &nodejs-patterns - '{package.json,*/package.json,*/*/package.json}' + - '{yarn.lock,*/yarn.lock,*/*/yarn.lock}' .python-patterns: &python-patterns - '{requirements.txt,*/requirements.txt,*/*/requirements.txt}' @@ -373,10 +374,6 @@ - ".dockerignore" - "qa/**/*" -.code-shell-patterns: &code-shell-patterns - - "bin/**/*" - - "tooling/**/*" - # .code-backstage-qa-patterns + .workhorse-patterns .setup-test-env-patterns: &setup-test-env-patterns - "{package.json,yarn.lock}" @@ -1487,6 +1484,12 @@ when: never - changes: *python-patterns +.reports:rules:yarn-audit-dependency_scanning: + rules: + - if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/' + when: never + - changes: *nodejs-patterns + .reports:rules:schedule-dast: rules: - if: '$DAST_DISABLED || $GITLAB_FEATURES !~ /\bdast\b/' @@ -1779,13 +1782,6 @@ - changes: *code-backstage-qa-patterns - changes: *startup-css-patterns -############### -# Shell rules # -############### -.shell:rules: - rules: - - changes: *code-shell-patterns - ####################### # Test metadata rules # ####################### diff --git a/.gitlab/ci/static-analysis.gitlab-ci.yml b/.gitlab/ci/static-analysis.gitlab-ci.yml index 82c11cb8009..8824d3d753f 100644 --- a/.gitlab/ci/static-analysis.gitlab-ci.yml +++ b/.gitlab/ci/static-analysis.gitlab-ci.yml @@ -107,15 +107,3 @@ feature-flags-usage: when: always paths: - tmp/feature_flags/ - -shellcheck: - extends: - - .default-retry - - .shell:rules - stage: lint - needs: [] - image: - name: koalaman/shellcheck-alpine - entrypoint: [""] - script: - - tooling/bin/shellcheck diff --git a/app/assets/javascripts/init_confirm_danger.js b/app/assets/javascripts/init_confirm_danger.js index 3e7f60bc237..d3d32c8be54 100644 --- a/app/assets/javascripts/init_confirm_danger.js +++ b/app/assets/javascripts/init_confirm_danger.js @@ -1,22 +1,37 @@ import Vue from 'vue'; +import { parseBoolean } from './lib/utils/common_utils'; import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue'; export default () => { const el = document.querySelector('.js-confirm-danger'); if (!el) return null; - const { phrase, buttonText, confirmDangerMessage } = el.dataset; + const { + removeFormId = null, + phrase, + buttonText, + buttonTestid = null, + confirmDangerMessage, + disabled = false, + } = el.dataset; return new Vue({ el, + provide: { + confirmDangerMessage, + }, render: (createElement) => createElement(ConfirmDanger, { props: { phrase, buttonText, + buttonTestid, + disabled: parseBoolean(disabled), }, - provide: { - confirmDangerMessage, + on: { + confirm: () => { + if (removeFormId) document.getElementById(removeFormId)?.submit(); + }, }, }), }); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 49b9822795c..604da77f60c 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -10,10 +10,12 @@ import projectSelect from '~/project_select'; import initSearchSettings from '~/search_settings'; import initSettingsPanels from '~/settings_panels'; import setupTransferEdit from '~/transfer_edit'; +import initConfirmDanger from '~/init_confirm_danger'; document.addEventListener('DOMContentLoaded', () => { initFilePickers(); initConfirmDangerModal(); + initConfirmDanger(); initSettingsPanels(); dirtySubmitFactory( document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'), diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue index 97e21d67fa5..0163b8c572b 100644 --- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue +++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue @@ -10,7 +10,6 @@ import { toggleQueryPollingByVisibility, } from '~/pipelines/components/graph/utils'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue'; const POLL_INTERVAL = 10000; @@ -37,7 +36,6 @@ export default { GlSprintf, PipelineEditorMiniGraph, }, - mixins: [glFeatureFlagMixin()], inject: ['projectFullPath'], props: { commitSha: { @@ -172,11 +170,7 @@ export default { </span> </div> <div class="gl-display-flex gl-flex-wrap"> - <pipeline-editor-mini-graph - v-if="glFeatures.pipelineEditorMiniGraph" - :pipeline="pipeline" - v-on="$listeners" - /> + <pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" /> <gl-button class="gl-mt-2 gl-md-mt-0" target="_blank" diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index e41bb41dc05..968efab5c82 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -39,6 +39,9 @@ export default { assignSelf() { this.$emit('assign-self'); }, + toggleAttentionRequired(data) { + this.$emit('toggle-attention-required', data); + }, }, }; </script> @@ -58,7 +61,12 @@ export default { </template> </span> - <uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" /> + <uncollapsed-assignee-list + v-else + :users="sortedAssigness" + :issuable-type="issuableType" + @toggle-attention-required="toggleAttentionRequired" + /> </div> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue index 80caebad39d..c4856a801cf 100644 --- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue @@ -32,6 +32,11 @@ export default { return this.users.length === 0; }, }, + methods: { + toggleAttentionRequired(data) { + this.$emit('toggle-attention-required', data); + }, + }, }; </script> @@ -61,6 +66,7 @@ export default { :users="users" :issuable-type="issuableType" class="gl-text-gray-800 gl-mt-2 hide-collapsed" + @toggle-attention-required="toggleAttentionRequired" /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index c6877226b7d..6af33f0eec2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -125,6 +125,9 @@ export default { availability: this.assigneeAvailabilityStatus[username] || '', })); }, + toggleAttentionRequired(data) { + this.mediator.toggleAttentionRequired('assignee', data); + }, }, }; </script> @@ -152,6 +155,7 @@ export default { :editable="store.editable" :issuable-type="issuableType" @assign-self="assignSelf" + @toggle-attention-required="toggleAttentionRequired" /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue index 9259b9f5a64..3bf0acf7216 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -2,6 +2,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { IssuableType } from '~/issue_show/constants'; import { __, sprintf } from '~/locale'; +import AttentionRequiredToggle from '../attention_required_toggle.vue'; import AssigneeAvatarLink from './assignee_avatar_link.vue'; import UserNameWithStatus from './user_name_with_status.vue'; @@ -9,6 +10,7 @@ const DEFAULT_RENDER_COUNT = 5; export default { components: { + AttentionRequiredToggle, AssigneeAvatarLink, UserNameWithStatus, }, @@ -80,6 +82,9 @@ export default { } return u?.status?.availability || ''; }, + toggleAttentionRequired(data) { + this.$emit('toggle-attention-required', data); + }, }, }; </script> @@ -108,6 +113,12 @@ export default { }" class="gl-display-inline-block" > + <attention-required-toggle + v-if="showVerticalList && user.can_update_merge_request" + :user="user" + type="assignee" + @toggle-attention-required="toggleAttentionRequired" + /> <assignee-avatar-link :user="user" :issuable-type="issuableType" diff --git a/app/assets/javascripts/sidebar/components/attention_required_toggle.vue b/app/assets/javascripts/sidebar/components/attention_required_toggle.vue new file mode 100644 index 00000000000..90cb0bfcc3f --- /dev/null +++ b/app/assets/javascripts/sidebar/components/attention_required_toggle.vue @@ -0,0 +1,74 @@ +<script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; + +export default { + i18n: { + attentionRequiredReviewer: __('Request attention to review'), + attentionRequiredAssignee: __('Request attention'), + removeAttentionRequired: __('Remove attention request'), + }, + components: { + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + type: { + type: String, + required: true, + }, + user: { + type: Object, + required: true, + }, + }, + data() { + return { + loading: false, + }; + }, + computed: { + tooltipTitle() { + if (this.user.attention_required) { + return this.$options.i18n.removeAttentionRequired; + } + + return this.type === 'reviewer' + ? this.$options.i18n.attentionRequiredReviewer + : this.$options.i18n.attentionRequiredAssignee; + }, + }, + methods: { + toggleAttentionRequired() { + if (this.loading) return; + + this.$root.$emit(BV_HIDE_TOOLTIP); + this.loading = true; + this.$emit('toggle-attention-required', { + user: this.user, + callback: this.toggleAttentionRequiredComplete, + }); + }, + toggleAttentionRequiredComplete() { + this.loading = false; + }, + }, +}; +</script> + +<template> + <span v-gl-tooltip.left.viewport="tooltipTitle"> + <gl-button + :loading="loading" + :variant="user.attention_required ? 'warning' : 'default'" + :icon="user.attention_required ? 'star' : 'star-o'" + :aria-label="tooltipTitle" + size="small" + category="tertiary" + @click="toggleAttentionRequired" + /> + </span> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue index 5729b958b5d..78f095dc77d 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue @@ -49,6 +49,9 @@ export default { requestReview(data) { this.$emit('request-review', data); }, + toggleAttentionRequired(data) { + this.$emit('toggle-attention-required', data); + }, }, }; </script> @@ -70,6 +73,7 @@ export default { :root-path="rootPath" :issuable-type="issuableType" @request-review="requestReview" + @toggle-attention-required="toggleAttentionRequired" /> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index e414aaf719b..8f33d4cd056 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -88,6 +88,9 @@ export default { requestReview(data) { this.mediator.requestReview(data); }, + toggleAttentionRequired(data) { + this.mediator.toggleAttentionRequired('reviewer', data); + }, }, }; </script> @@ -106,6 +109,7 @@ export default { :editable="store.editable" :issuable-type="issuableType" @request-review="requestReview" + @toggle-attention-required="toggleAttentionRequired" /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue index 2922008cfb2..adfb2491720 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue @@ -1,6 +1,8 @@ <script> import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __, sprintf, s__ } from '~/locale'; +import AttentionRequiredToggle from '../attention_required_toggle.vue'; import ReviewerAvatarLink from './reviewer_avatar_link.vue'; const LOADING_STATE = 'loading'; @@ -14,10 +16,12 @@ export default { GlButton, GlIcon, ReviewerAvatarLink, + AttentionRequiredToggle, }, directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagsMixin()], props: { users: { type: Array, @@ -76,6 +80,9 @@ export default { this.loadingStates[userId] = null; } }, + toggleAttentionRequired(data) { + this.$emit('toggle-attention-required', data); + }, }, LOADING_STATE, SUCCESS_STATE, @@ -90,6 +97,12 @@ export default { :class="{ 'gl-mb-3': index !== users.length - 1 }" data-testid="reviewer" > + <attention-required-toggle + v-if="glFeatures.mrAttentionRequests && user.can_update_merge_request" + :user="user" + type="reviewer" + @toggle-attention-required="toggleAttentionRequired" + /> <reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType"> <div class="gl-ml-3 gl-line-height-normal gl-display-grid"> <span>{{ user.name }}</span> @@ -113,7 +126,9 @@ export default { data-testid="re-request-success" /> <gl-button - v-else-if="user.can_update_merge_request && user.reviewed" + v-else-if=" + user.can_update_merge_request && user.reviewed && !glFeatures.mrAttentionRequests + " v-gl-tooltip.left :title="$options.i18n.reRequestReview" :aria-label="$options.i18n.reRequestReview" diff --git a/app/assets/javascripts/sidebar/queries/attention_required.mutation.graphql b/app/assets/javascripts/sidebar/queries/attention_required.mutation.graphql new file mode 100644 index 00000000000..2843291de97 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/attention_required.mutation.graphql @@ -0,0 +1,5 @@ +mutation mergeRequestAttentionRequired($projectPath: ID!, $iid: String!, $userId: ID!) { + mergeRequestAttentionRequired(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) { + errors + } +} diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index cea26acd101..455825c09f9 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -5,6 +5,7 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql'; import axios from '~/lib/utils/axios_utils'; import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql'; import sidebarDetailsMRQuery from '../queries/sidebarDetailsMR.query.graphql'; +import attentionRequiredMutation from '../queries/attention_required.mutation.graphql'; const queries = { merge_request: sidebarDetailsMRQuery, @@ -90,4 +91,15 @@ export default class SidebarService { }, }); } + + attentionRequired(userId) { + return gqClient.mutate({ + mutation: attentionRequiredMutation, + variables: { + userId: convertToGraphQLId(TYPE_USER, `${userId}`), + projectPath: this.fullPath, + iid: this.iid.toString(), + }, + }); + } } diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 9144e3b08db..0de4ae4569f 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -1,6 +1,6 @@ import Store from 'ee_else_ce/sidebar/stores/sidebar_store'; import createFlash from '~/flash'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import toast from '~/vue_shared/plugins/global_toast'; import { visitUrl } from '../lib/utils/url_utility'; import Service from './services/sidebar_service'; @@ -56,13 +56,55 @@ export default class SidebarMediator { return this.service .requestReview(userId) .then(() => { - this.store.updateReviewer(userId); + this.store.updateReviewer(userId, 'reviewed'); toast(__('Requested review')); callback(userId, true); }) .catch(() => callback(userId, false)); } + async toggleAttentionRequired(type, { user, callback }) { + try { + const isReviewer = type === 'reviewer'; + const reviewerOrAssignee = isReviewer + ? this.store.findReviewer(user) + : this.store.findAssignee(user); + + if (reviewerOrAssignee.attention_required) { + toast( + sprintf(__('Removed attention request from @%{username}'), { + username: user.username, + }), + ); + } else { + await this.service.attentionRequired(user.id); + + toast(sprintf(__('Requested attention from @%{username}'), { username: user.username })); + } + + if (isReviewer) { + this.store.updateReviewer(user.id, 'attention_required'); + } else { + this.store.updateAssignee(user.id, 'attention_required'); + } + + callback(); + } catch (error) { + callback(); + createFlash({ + message: sprintf(__('Updating the attention request for %{username} failed.'), { + username: user.username, + }), + error, + captureError: true, + actionConfig: { + title: __('Try again'), + clickHandler: () => this.toggleAttentionRequired(type, { user, callback }), + }, + }); + } + } + setMoveToProjectId(projectId) { this.store.setMoveToProjectId(projectId); } diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index 94c54fc0980..5376791469e 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -82,11 +82,19 @@ export default class SidebarStore { } } - updateReviewer(id) { + updateAssignee(id, stateKey) { + const assignee = this.findAssignee({ id }); + + if (assignee) { + assignee[stateKey] = !assignee[stateKey]; + } + } + + updateReviewer(id, stateKey) { const reviewer = this.findReviewer({ id }); if (reviewer) { - reviewer.reviewed = false; + reviewer[stateKey] = !reviewer[stateKey]; } } diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue index 294450851c5..4c07cf44fed 100644 --- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue +++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue @@ -26,6 +26,11 @@ export default { type: String, required: true, }, + buttonTestid: { + type: String, + required: false, + default: 'confirm-danger-button', + }, }, modalId: CONFIRM_DANGER_MODAL_ID, }; @@ -37,7 +42,7 @@ export default { class="gl-button" variant="danger" :disabled="disabled" - data-testid="confirm-danger-button" + :data-testid="buttonTestid" >{{ buttonText }}</gl-button > <confirm-danger-modal diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue index 445fb5404a2..30c96daf7e3 100644 --- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue +++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue @@ -93,6 +93,7 @@ export default { </p> <gl-form-group :state="isValid" :invalid-feedback="$options.i18n.CONFIRM_DANGER_MODAL_ERROR"> <gl-form-input + id="confirm_name_input" v-model="confirmationPhrase" class="form-control" data-testid="confirm-danger-input" diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb index 5e6195296f7..8707381bb68 100644 --- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb +++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb @@ -73,13 +73,23 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy end def upload_manifest - @group.dependency_proxy_manifests.create!( + attrs = { file_name: manifest_file_name, content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER], - digest: request.headers['Docker-Content-Digest'], + digest: request.headers[DependencyProxy::Manifest::DIGEST_HEADER], file: params[:file], size: params[:file].size - ) + } + + manifest = @group.dependency_proxy_manifests + .active + .find_by_file_name(manifest_file_name) + + if manifest + manifest.update!(attrs) + else + @group.dependency_proxy_manifests.create!(attrs) + end event_name = tracking_event_name(object_type: :manifest, from_cache: false) track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user) @@ -105,7 +115,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy def send_manifest(manifest, from_cache:) # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536 manifest.touch - response.headers['Docker-Content-Digest'] = manifest.digest + response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest response.headers['Content-Length'] = manifest.size response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION response.headers['Etag'] = "\"#{manifest.digest}\"" diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb index 22cd247644d..c8394d91677 100644 --- a/app/controllers/projects/ci/pipeline_editor_controller.rb +++ b/app/controllers/projects/ci/pipeline_editor_controller.rb @@ -3,7 +3,6 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController before_action :check_can_collaborate! before_action do - push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml) push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml) end diff --git a/app/graphql/types/dependency_proxy/manifest_type.rb b/app/graphql/types/dependency_proxy/manifest_type.rb index 9aa62266ef7..ef9f730df43 100644 --- a/app/graphql/types/dependency_proxy/manifest_type.rb +++ b/app/graphql/types/dependency_proxy/manifest_type.rb @@ -8,6 +8,7 @@ module Types authorize :read_dependency_proxy + field :id, ::Types::GlobalIDType[::DependencyProxy::Manifest], null: false, description: 'ID of the manifest.' field :created_at, Types::TimeType, null: false, description: 'Date of creation.' field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.' field :file_name, GraphQL::Types::String, null: false, description: 'Name of the manifest.' diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index b1bbabcdaed..b8f382681f3 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -220,6 +220,10 @@ module Types group.container_repositories.size end + def dependency_proxy_manifests + group.dependency_proxy_manifests.order_id_desc + end + def dependency_proxy_image_count group.dependency_proxy_manifests.count end diff --git a/app/helpers/groups/settings_helper.rb b/app/helpers/groups/settings_helper.rb new file mode 100644 index 00000000000..1b391680996 --- /dev/null +++ b/app/helpers/groups/settings_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Groups + module SettingsHelper + include GroupsHelper + + def group_settings_confirm_modal_data(group, remove_form_id = nil) + { + remove_form_id: remove_form_id, + button_text: _('Remove group'), + button_testid: 'remove-group-button', + disabled: group.paid?.to_s, + confirm_danger_message: remove_group_message(group), + phrase: group.full_path + } + end + end +end + +Groups::SettingsHelper.prepend_mod_with('Groups::SettingsHelper') diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 73ee4aca43c..c5a4f8523ef 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -139,8 +139,6 @@ module Clusters scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) } scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) } scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct } - scope :preload_elasticstack, -> { preload(:integration_elastic_stack) } - scope :preload_environments, -> { preload(:environments) } scope :managed, -> { where(managed: true) } scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) } diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb index 6a5ccd12cac..64f484942ef 100644 --- a/app/models/dependency_proxy/manifest.rb +++ b/app/models/dependency_proxy/manifest.rb @@ -8,12 +8,15 @@ class DependencyProxy::Manifest < ApplicationRecord belongs_to :group MAX_FILE_SIZE = 10.megabytes.freeze + DIGEST_HEADER = 'Docker-Content-Digest' validates :group, presence: true validates :file, presence: true validates :file_name, presence: true validates :digest, presence: true + scope :order_id_desc, -> { reorder(id: :desc) } + mount_file_store_uploader DependencyProxy::FileUploader def self.find_by_file_name_or_digest(file_name:, digest:) diff --git a/app/models/issue.rb b/app/models/issue.rb index 405dae85488..47dc084d69c 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -204,6 +204,8 @@ class Issue < ApplicationRecord before_transition closed: :opened do |issue| issue.closed_at = nil issue.closed_by = nil + + issue.clear_closure_reason_references end end @@ -379,6 +381,11 @@ class Issue < ApplicationRecord !duplicated_to_id.nil? end + def clear_closure_reason_references + self.moved_to_id = nil + self.duplicated_to_id = nil + end + def can_move?(user, to_project = nil) if to_project return false unless user.can?(:admin_issue, to_project) diff --git a/app/services/dependency_proxy/head_manifest_service.rb b/app/services/dependency_proxy/head_manifest_service.rb index ecc3eb77399..cd575b83a98 100644 --- a/app/services/dependency_proxy/head_manifest_service.rb +++ b/app/services/dependency_proxy/head_manifest_service.rb @@ -14,7 +14,10 @@ module DependencyProxy response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS)) if response.success? - success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']) + success( + digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER], + content_type: response.headers['content-type'] + ) else error(response.body, response.code) end diff --git a/app/services/dependency_proxy/pull_manifest_service.rb b/app/services/dependency_proxy/pull_manifest_service.rb index 31494773cc0..e8f0ad6374a 100644 --- a/app/services/dependency_proxy/pull_manifest_service.rb +++ b/app/services/dependency_proxy/pull_manifest_service.rb @@ -20,7 +20,13 @@ module DependencyProxy file.write(response.body) file.flush - yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])) + yield( + success( + file: file, + digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER], + content_type: response.headers['content-type'] + ) + ) ensure file.close file.unlink diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml index cdff533e3c7..a82f7803b44 100644 --- a/app/views/groups/settings/_advanced.html.haml +++ b/app/views/groups/settings/_advanced.html.haml @@ -1,3 +1,4 @@ +- remove_form_id = 'js-remove-group-form' = render 'groups/settings/export', group: @group .sub-section @@ -26,6 +27,6 @@ = f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-warning' = render 'groups/settings/transfer', group: @group -= render 'groups/settings/remove', group: @group += render 'groups/settings/remove', group: @group, remove_form_id: remove_form_id = render_if_exists 'groups/settings/restore', group: @group -= render_if_exists 'groups/settings/immediately_remove', group: @group += render_if_exists 'groups/settings/immediately_remove', group: @group, remove_form_id: remove_form_id diff --git a/app/views/groups/settings/_permanent_deletion.html.haml b/app/views/groups/settings/_permanent_deletion.html.haml index 125a20060ed..152cdfc1411 100644 --- a/app/views/groups/settings/_permanent_deletion.html.haml +++ b/app/views/groups/settings/_permanent_deletion.html.haml @@ -1,9 +1,11 @@ +- remove_form_id = local_assigns.fetch(:remove_form_id, nil) + .sub-section %h4.danger-title= _('Remove group') - = form_tag(group, method: :delete) do + = form_tag(group, method: :delete, id: remove_form_id) do %p = _('Removing this group also removes all child projects, including archived projects, and their resources.') %br %strong= _('Removed group can not be restored!') - = render 'groups/settings/remove_button', group: group + = render 'groups/settings/remove_button', group: group, remove_form_id: remove_form_id diff --git a/app/views/groups/settings/_remove.html.haml b/app/views/groups/settings/_remove.html.haml index a617467019a..8571b93364b 100644 --- a/app/views/groups/settings/_remove.html.haml +++ b/app/views/groups/settings/_remove.html.haml @@ -1,5 +1,6 @@ +- remove_form_id = local_assigns.fetch(:remove_form_id, nil) + - if group.adjourned_deletion? - = render_if_exists 'groups/settings/adjourned_deletion', group: group + = render_if_exists 'groups/settings/adjourned_deletion', group: group, remove_form_id: remove_form_id - else - = render 'groups/settings/permanent_deletion', group: group - + = render 'groups/settings/permanent_deletion', group: group, remove_form_id: remove_form_id diff --git a/app/views/groups/settings/_remove_button.html.haml b/app/views/groups/settings/_remove_button.html.haml index 75413a2975e..1d5b7160049 100644 --- a/app/views/groups/settings/_remove_button.html.haml +++ b/app/views/groups/settings/_remove_button.html.haml @@ -1,7 +1,9 @@ +- remove_form_id = local_assigns.fetch(:remove_form_id, nil) + - if group.paid? .gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-has-linked-subscription-alert' } } = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') .gl-alert-body = html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe } -= button_to _('Remove group'), '#', class: ['btn gl-button btn-danger js-legacy-confirm-danger', ('disabled' if group.paid?)], data: { 'confirm-danger-message' => remove_group_message(group), 'testid' => 'remove-group-button' } +.js-confirm-danger{ data: group_settings_confirm_modal_data(group, remove_form_id) } diff --git a/app/views/projects/confluences/show.html.haml b/app/views/projects/confluences/show.html.haml index cf4e39f9659..413de90b67b 100644 --- a/app/views/projects/confluences/show.html.haml +++ b/app/views/projects/confluences/show.html.haml @@ -6,9 +6,8 @@ = s_('WikiEmpty|Confluence is enabled') %p - wiki_confluence_epic_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/3629' - - wiki_confluence_epic_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, url: wiki_confluence_epic_link_url) - = format(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.").html_safe, wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe) + - wiki_confluence_epic_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wiki_confluence_epic_link_url } + = html_escape(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.")) % { wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe } = link_to @project.confluence_integration.confluence_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn btn-success external-url', title: s_('WikiEmpty|Go to Confluence') do - = sprite_icon('external-link') = s_('WikiEmpty|Go to Confluence') - + = sprite_icon('external-link') diff --git a/bin/background_jobs b/bin/background_jobs index f301bb46ca9..d8929881f12 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -1,12 +1,12 @@ #!/usr/bin/env bash -cd "$(dirname "$0")/.." || exit - +cd $(dirname $0)/.. app_root=$(pwd) sidekiq_workers=${SIDEKIQ_WORKERS:-1} sidekiq_queues=${SIDEKIQ_QUEUES:-*} # Queues to listen to; default to `*` (all) sidekiq_pidfile="$app_root/tmp/pids/sidekiq-cluster.pid" sidekiq_logfile="$app_root/log/sidekiq.log" +gitlab_user=$(ls -l config.ru | awk '{print $3}') trap cleanup EXIT @@ -17,26 +17,26 @@ warn() get_sidekiq_pid() { - if [ ! -f "$sidekiq_pidfile" ]; then + if [ ! -f $sidekiq_pidfile ]; then warn "No pidfile found at $sidekiq_pidfile; is Sidekiq running?" return fi - cat "$sidekiq_pidfile" + cat $sidekiq_pidfile } stop() { sidekiq_pid=$(get_sidekiq_pid) - if [ "$sidekiq_pid" ]; then - kill -TERM "$sidekiq_pid" + if [ $sidekiq_pid ]; then + kill -TERM $sidekiq_pid fi } restart() { - if [ -f "$sidekiq_pidfile" ]; then + if [ -f $sidekiq_pidfile ]; then stop fi @@ -53,12 +53,12 @@ start_sidekiq() fi # sidekiq-cluster expects an argument per process. - for (( i=1; i<=sidekiq_workers; i++ )) + for (( i=1; i<=$sidekiq_workers; i++ )) do processes_args+=("${sidekiq_queues}") done - ${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P "$sidekiq_pidfile" -e "$RAILS_ENV" "$@" 2>&1 | tee -a "$sidekiq_logfile" + ${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P $sidekiq_pidfile -e $RAILS_ENV "$@" 2>&1 | tee -a $sidekiq_logfile } cleanup() diff --git a/bin/mail_room b/bin/mail_room index 3717e49e37f..cf9d422909e 100755 --- a/bin/mail_room +++ b/bin/mail_room @@ -1,6 +1,6 @@ #!/bin/sh -cd "$(dirname "$0")/.." || exit 1 +cd $(dirname $0)/.. || exit 1 app_root=$(pwd) mail_room_pidfile="$app_root/tmp/pids/mail_room.pid" @@ -9,7 +9,8 @@ mail_room_config="$app_root/config/mail_room.yml" get_mail_room_pid() { - pid=$(cat "$mail_room_pidfile") + local pid + pid=$(cat $mail_room_pidfile) if [ -z "$pid" ] ; then echo "Could not find a PID in $mail_room_pidfile" exit 1 @@ -19,13 +20,13 @@ get_mail_room_pid() start() { - bin/daemon_with_pidfile "$mail_room_pidfile" bundle exec mail_room --log-exit-as json -q -c "$mail_room_config" >> "$mail_room_logfile" 2>&1 + bin/daemon_with_pidfile $mail_room_pidfile bundle exec mail_room --log-exit-as json -q -c $mail_room_config >> $mail_room_logfile 2>&1 } stop() { get_mail_room_pid - kill -TERM "$mail_room_pid" + kill -TERM $mail_room_pid } restart() diff --git a/bin/parallel-rsync-repos b/bin/parallel-rsync-repos index bd849371766..21921148fa0 100755 --- a/bin/parallel-rsync-repos +++ b/bin/parallel-rsync-repos @@ -32,20 +32,20 @@ if [ -z "$RSYNC" ] ; then RSYNC=rsync fi -if ! cd "$SRC" ; then +if ! cd $SRC ; then echo "cd $SRC failed" exit 1 fi rsyncjob() { - relative_dir="./${1#"$SRC"}" + relative_dir="./${1#$SRC}" if ! $RSYNC --delete --relative -a "$relative_dir" "$DEST" ; then echo "rsync $1 failed" return 1 fi - echo "$1" >> "$LOGFILE" + echo "$1" >> $LOGFILE } export LOGFILE SRC DEST RSYNC @@ -2,7 +2,7 @@ set -e -cd "$(dirname "$0")/.." +cd $(dirname $0)/.. app_root=$(pwd) puma_pidfile="$app_root/tmp/pids/puma.pid" @@ -25,12 +25,12 @@ get_puma_pid() start() { - spawn_puma "$@" & + spawn_puma & } start_foreground() { - spawn_puma "$@" + spawn_puma } stop() @@ -46,10 +46,10 @@ reload() case "$1" in start) - start "$@" + start ;; start_foreground) - start_foreground "$@" + start_foreground ;; stop) stop diff --git a/bin/with_env b/bin/with_env index b0647a50e27..e678fa2f0cc 100755 --- a/bin/with_env +++ b/bin/with_env @@ -10,7 +10,6 @@ shift # Use set -a to export all variables defined in env_file. set -a -# shellcheck disable=SC1090 . "${env_file}" set +a diff --git a/config/feature_flags/development/pipeline_editor_mini_graph.yml b/config/feature_flags/development/pipeline_editor_mini_graph.yml deleted file mode 100644 index 6f31cb18d82..00000000000 --- a/config/feature_flags/development/pipeline_editor_mini_graph.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: pipeline_editor_mini_graph -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71622 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342217 -milestone: '14.4' -type: development -group: group::pipeline authoring -default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9694fd84fd4..66be4ffff97 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9237,6 +9237,7 @@ Dependency proxy manifest. | <a id="dependencyproxymanifestcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. | | <a id="dependencyproxymanifestdigest"></a>`digest` | [`String!`](#string) | Digest of the manifest. | | <a id="dependencyproxymanifestfilename"></a>`fileName` | [`String!`](#string) | Name of the manifest. | +| <a id="dependencyproxymanifestid"></a>`id` | [`DependencyProxyManifestID!`](#dependencyproxymanifestid) | ID of the manifest. | | <a id="dependencyproxymanifestimagename"></a>`imageName` | [`String!`](#string) | Name of the image. | | <a id="dependencyproxymanifestsize"></a>`size` | [`String!`](#string) | Size of the manifest file. | | <a id="dependencyproxymanifestupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. | @@ -17280,6 +17281,12 @@ An example `DastSiteValidationID` is: `"gid://gitlab/DastSiteValidation/1"`. Date represented in ISO 8601. +### `DependencyProxyManifestID` + +A `DependencyProxyManifestID` is a global ID. It is encoded as a string. + +An example `DependencyProxyManifestID` is: `"gid://gitlab/DependencyProxy::Manifest/1"`. + ### `DesignManagementDesignAtVersionID` A `DesignManagementDesignAtVersionID` is a global ID. It is encoded as a string. diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index d3cb9768d14..1382ec263f2 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1055,15 +1055,19 @@ Guidance for each individual UI element is in [the word list](word_list.md). To be consistent, use this format when you write navigation steps in a task topic. +```markdown 1. On the top bar, select **Menu > Projects** and find your project. 1. On the left sidebar, select **Settings > CI/CD**. 1. Expand **General pipelines**. +``` Another example: +```markdown 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Settings > CI/CD**. 1. Expand **General pipelines**. +``` An Admin Area example: @@ -1092,7 +1096,7 @@ For example: If the UI text sufficiently explains the fields in a section, do not include a task step for every field. Instead, summarize multiple fields in a single task step. -Use the phrase **Complete the fields**. +Use the phrase **Complete the fields**. For example: diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index 482a3a7045f..68c1f1410ea 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -19,7 +19,7 @@ are accessible. - **Jira Server**: Your network must allow access to your instance. - **Jira Cloud**: Your instance must be accessible through the internet. -## Smart commits +## Smart Commits When connecting GitLab with Jira with DVCS, you can process your Jira issues using special commands, called @@ -48,17 +48,24 @@ Smart Commits should follow the pattern of: Some examples: -- Adding a comment to a Jira issue: `KEY-123 fixes a bug #comment Bug is fixed.` -- Recording time tracking: `KEY-123 #time 2w 4d 10h 52m Tracking work time.` -- Closing an issue: `KEY-123 #close Closing issue` +- Add a comment to a Jira issue: `KEY-123 fixes a bug #comment Bug is fixed.` +- Record time tracking: `KEY-123 #time 2w 4d 10h 52m Tracking work time.` +- Close an issue: `KEY-123 #close Closing issue` A Smart Commit message must not span more than one line (no carriage returns) but -you can still perform multiple actions in a single commit: +you can still perform multiple actions in a single commit. For example: -- Time tracking, commenting, and transitioning to **Closed**: - `KEY-123 #time 2d 5h #comment Task completed ahead of schedule #close`. -- Commenting, transitioning to **In-progress**, and time tracking: - `KEY-123 #comment started working on the issue #in-progress #time 12d 5h`. +- Add time tracking, add a comment, and transition to **Closed**: + + ```plaintext + KEY-123 #time 2d 5h #comment Task completed ahead of schedule #close + ``` + +- Add a comment, transition to **In-progress**, and add time tracking: + + ```plaintext + KEY-123 #comment started working on the issue #in-progress #time 12d 5h + ``` ## Configure a GitLab application for DVCS @@ -69,9 +76,9 @@ you can set up this integration with your own account instead. 1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to use to connect to GitLab. This user must be added to each project you want Jira to have access to, - or have an [Administrator](../../user/permissions.md) role to access all projects. + or be an administrator to access all projects. 1. Sign in as the `jira` user. -1. In the top right corner, click the account's avatar, and select **Edit profile**. +1. On the top bar, in the top right corner, select the user's avatar, and select **Edit profile**. 1. On the left sidebar, select **Applications**. 1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`. 1. In the **Redirect URI** field, enter the URI appropriate for your version of GitLab, @@ -86,10 +93,10 @@ you can set up this integration with your own account instead. `https://<gitlab.example.com>/-/jira/login/oauth/callback`. 1. For **Scopes**, select `api` and clear any other checkboxes. - - The connector requires a _write-enabled_ `api` scope to automatically create and manage required webhooks. + - The DVCS connector requires a _write-enabled_ `api` scope to automatically create and manage required webhooks. 1. Select **Submit**. -1. GitLab displays the generated **Application ID** - and **Secret** values. Copy these values, as you need them to configure Jira. +1. Copy the **Application ID** and **Secret** values. + You need them to configure Jira. ## Configure Jira for DVCS @@ -97,19 +104,21 @@ Configure this connection when you want to import all GitLab commits and branche for the groups you specify, into Jira. This import takes a few minutes and, after it completes, refreshes every 60 minutes: -1. Ensure you have completed the [GitLab configuration](#configure-a-gitlab-application-for-dvcs). +1. Complete the [GitLab configuration](#configure-a-gitlab-application-for-dvcs). 1. Go to your DVCS accounts: - - *For Jira Server,* go to **Settings (gear) > Applications > DVCS accounts**. - - *For Jira Cloud,* go to **Settings (gear) > Products > DVCS accounts**. + - *For Jira Server,* select **Settings (gear) > Applications > DVCS accounts**. + - *For Jira Cloud,* select **Settings (gear) > Products > DVCS accounts**. 1. To create a new integration, select the appropriate value for **Host**: - *For Jira versions 8.14 and later:* Select **GitLab** or **GitLab Self-Managed**. - *For Jira versions 8.13 and earlier:* Select **GitHub Enterprise**. 1. For **Team or User Account**, enter either: - *For Jira versions 8.14 and later:* - - The relative path of a top-level GitLab group that [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to. + - The relative path of a top-level GitLab group that + [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to. - *For Jira versions 8.13 and earlier:* - - The relative path of a top-level GitLab group that [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to. + - The relative path of a top-level GitLab group that + [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to. - The relative path of your personal namespace. 1. In the **Host URL** field, enter the URI appropriate for your version of GitLab, @@ -120,13 +129,13 @@ it completes, refreshes every 60 minutes: 1. For **Client ID**, use the **Application ID** value from the previous section. 1. For **Client Secret**, use the **Secret** value from the previous section. -1. Ensure that the rest of the checkboxes are checked. -1. Select **Add** and then **Continue** to create the DVCS account. -1. Jira redirects to GitLab where you have to confirm the authorization, - and then GitLab redirects back to Jira where you should see the synced - projects show up inside the new account. +1. Ensure that the rest of the checkboxes are selected. +1. To create the DVCS account, select **Add** and then **Continue**. +1. Jira redirects to GitLab where you have to confirm the authorization. + GitLab then redirects back to Jira where the synced + projects should display in the new account. -To connect additional GitLab projects from other GitLab top-level groups, or +To connect additional GitLab projects from other GitLab top-level groups or personal namespaces, repeat the previous steps with additional Jira DVCS accounts. After you configure the integration, read more about [how to test and use it](development_panel.md). @@ -172,9 +181,8 @@ Error obtaining access token. Cannot access https://gitlab.example.com from Jira as GitLab is the TLS client. - The Jira Development panel integration requires Jira to connect to GitLab, which causes Jira to be the TLS client. If your GitLab server's certificate is not - issued by a public certificate authority, the Java Truststore on Jira's server - must have the appropriate certificate (such as your organization's - root certificate) added to it . + issued by a public certificate authority, add the appropriate certificate + (such as your organization's root certificate) to the Java Truststore on Jira's server. Refer to Atlassian's documentation and Atlassian Support for assistance setting up Jira correctly: @@ -187,8 +195,8 @@ up Jira correctly: - If the integration stops working after upgrading Jira's Java runtime, the `cacerts` Truststore may have been replaced during the upgrade. -- Troubleshooting connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html), - using the a java class called `SSLPoke`. +- Troubleshoot connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html), + using the `SSLPoke` Java class. - Download the class from Atlassian's knowledge base to a directory on Jira's server, such as `/tmp`. - Use the same Java runtime as Jira. - Pass all networking-related parameters that Jira is called with, such as proxy @@ -203,7 +211,7 @@ The message `Successfully connected` indicates a successful TLS handshake. If there are problems, the Java TLS library generates errors that you can look up for more detail. -### Scope error when connecting Jira via DVCS +### Scope error when connecting to Jira using DVCS ```plaintext The requested scope is invalid, unknown, or malformed. @@ -224,12 +232,12 @@ After you complete the **Add New Account** form in Jira and authorize access, yo encounter these issues: - An `Error! Failed adding the account: [Error retrieving list of repositories]` error. -- An `Account is already integrated with JIRA` error when you click **Try Again**. +- An `Account is already integrated with JIRA` error when you select **Try Again**. - An account is visible in the DVCS accounts view, but no repositories are listed. To resolve this issue: -- If you're using GitLab Free, be sure you're using GitLab 13.4 or later. +- If you're using GitLab Free, ensure you're using GitLab 13.4 or later. - If you're using GitLab versions 11.10-12.7, upgrade to GitLab 12.8.10 or later to resolve [an identified issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012). @@ -243,17 +251,17 @@ This issue occurs when you use the Jira DVCS connector and your integration is c For more information and possible fixes, see [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340160). -### Fix synchronization issues +### Synchronization issues If Jira displays incorrect information, such as deleted branches, you may have to -resynchronize the information. To do so: - -1. In Jira, go to **Jira Administration > Applications > DVCS accounts**. -1. At the account (group or subgroup) level, Jira displays an option to - **Refresh repositories** in the **{ellipsis_h}** (ellipsis) menu. -1. For each project, there's a sync button displayed next to the **last activity** date. - - To perform a *soft resync*, click the button. - - To complete a *full sync*, shift-click the button. +resynchronize the information: + +1. In Jira, select **Jira Administration > Applications > DVCS accounts**. +1. For the account (group or subgroup), select + **Refresh repositories** from the **{ellipsis_h}** (ellipsis) menu. +1. For each project, next to the **Last activity** date: + - To perform a *soft resync*, select the sync icon. + - To complete a *full sync*, press `Shift` and select the sync icon. For more information, read [Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/). diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 46399224a5d..c2ad9e6ae89 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -90,7 +90,7 @@ module ContainerRegistry def repository_tag_digest(name, reference) response = faraday.head("/v2/#{name}/manifests/#{reference}") - response.headers['docker-content-digest'] if response.success? + response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success? end def delete_repository_tag_by_digest(name, reference) @@ -171,7 +171,7 @@ module ContainerRegistry req.body = Gitlab::Json.pretty_generate(manifest) end - response.headers['docker-content-digest'] if response.success? + response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success? end private diff --git a/lib/gitlab/database/load_balancing/primary_host.rb b/lib/gitlab/database/load_balancing/primary_host.rb index 7070cc54d4b..fb52b384ddb 100644 --- a/lib/gitlab/database/load_balancing/primary_host.rb +++ b/lib/gitlab/database/load_balancing/primary_host.rb @@ -49,6 +49,11 @@ module Gitlab end def offline! + ::Gitlab::Database::LoadBalancing::Logger.warn( + event: :host_offline, + message: 'Marking primary host as offline' + ) + nil end diff --git a/lib/gitlab/prometheus/queries/validate_query.rb b/lib/gitlab/prometheus/queries/validate_query.rb index 1f55f3e9768..160db7d44bc 100644 --- a/lib/gitlab/prometheus/queries/validate_query.rb +++ b/lib/gitlab/prometheus/queries/validate_query.rb @@ -7,7 +7,7 @@ module Gitlab def query(query) client_query(query) { valid: true } - rescue Gitlab::PrometheusClient::QueryError, Gitlab::HTTP::BlockedUrlError => ex + rescue Gitlab::PrometheusClient::QueryError, Gitlab::PrometheusClient::ConnectionError => ex { valid: false, error: ex.message } end diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb index 8182dbad4f8..dda28ffdf90 100644 --- a/lib/gitlab/prometheus_client.rb +++ b/lib/gitlab/prometheus_client.rb @@ -151,12 +151,8 @@ module Gitlab def get(path, args) Gitlab::HTTP.get(path, { query: args }.merge(http_options) ) - rescue SocketError - raise PrometheusClient::ConnectionError, "Can't connect to #{api_url}" - rescue OpenSSL::SSL::SSLError - raise PrometheusClient::ConnectionError, "#{api_url} contains invalid SSL data" - rescue Errno::ECONNREFUSED - raise PrometheusClient::ConnectionError, 'Connection refused' + rescue *Gitlab::HTTP::HTTP_ERRORS => e + raise PrometheusClient::ConnectionError, e.message end def handle_management_api_response(response) diff --git a/lib/sidebars/projects/menus/confluence_menu.rb b/lib/sidebars/projects/menus/confluence_menu.rb index 0d83238fa82..0fd42a57da3 100644 --- a/lib/sidebars/projects/menus/confluence_menu.rb +++ b/lib/sidebars/projects/menus/confluence_menu.rb @@ -37,6 +37,11 @@ module Sidebars def render? context.project.has_confluence? end + + override :active_routes + def active_routes + { controller: :confluences } + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 01ebba2c07e..2f38f16edda 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -28636,6 +28636,9 @@ msgstr "" msgid "Remove assignee" msgstr "" +msgid "Remove attention request" +msgstr "" + msgid "Remove avatar" msgstr "" @@ -28771,6 +28774,9 @@ msgstr "" msgid "Removed an issue from an epic." msgstr "" +msgid "Removed attention request from @%{username}" +msgstr "" + msgid "Removed group can not be restored!" msgstr "" @@ -29199,6 +29205,12 @@ msgstr "" msgid "Request a new one" msgstr "" +msgid "Request attention" +msgstr "" + +msgid "Request attention to review" +msgstr "" + msgid "Request details" msgstr "" @@ -29220,6 +29232,9 @@ msgstr "" msgid "Requested %{time_ago}" msgstr "" +msgid "Requested attention from @%{username}" +msgstr "" + msgid "Requested design version does not exist." msgstr "" @@ -30909,7 +30924,7 @@ msgstr "" msgid "SecurityReports|Take survey" msgstr "" -msgid "SecurityReports|The Vulnerability Report shows the results of the lastest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}" +msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}" msgstr "" msgid "SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}." @@ -36990,6 +37005,9 @@ msgstr "" msgid "Updating" msgstr "" +msgid "Updating the attention request for %{username} failed." +msgstr "" + msgid "Updating…" msgstr "" diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 661b7d210bb..b22307578ab 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -425,28 +425,28 @@ RSpec.describe Groups::DependencyProxyForContainersController do end end - describe 'GET #authorize_upload_blob' do + describe 'POST #authorize_upload_blob' do let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' } let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE } subject do request.headers.merge!(workhorse_internal_api_request_header) - get :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha } + post :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha } end it_behaves_like 'without permission' it_behaves_like 'authorize action with permission' end - describe 'GET #upload_blob' do + describe 'POST #upload_blob' do let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' } let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') } subject do request.headers.merge!(workhorse_internal_api_request_header) - get :upload_blob, params: { + post :upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha, @@ -469,31 +469,45 @@ RSpec.describe Groups::DependencyProxyForContainersController do end end - describe 'GET #authorize_upload_manifest' do + describe 'POST #authorize_upload_manifest' do let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE } subject do request.headers.merge!(workhorse_internal_api_request_header) - get :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' } + post :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' } end it_behaves_like 'without permission' it_behaves_like 'authorize action with permission' end - describe 'GET #upload_manifest' do - let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') } + describe 'POST #upload_manifest' do + let_it_be(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') } + let_it_be(:image) { 'alpine' } + let_it_be(:tag) { 'latest' } + let_it_be(:content_type) { 'v2/manifest' } + let_it_be(:digest) { 'foo' } + let_it_be(:file_name) { "#{image}:#{tag}.json" } subject do - request.headers.merge!(workhorse_internal_api_request_header) - - get :upload_manifest, params: { + request.headers.merge!( + workhorse_internal_api_request_header.merge!( + { + Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER => content_type, + DependencyProxy::Manifest::DIGEST_HEADER => digest + } + ) + ) + params = { group_id: group.to_param, - image: 'alpine', - tag: 'latest', - file: file + image: image, + tag: tag, + file: file, + file_name: file_name } + + post :upload_manifest, params: params end it_behaves_like 'without permission' @@ -501,13 +515,30 @@ RSpec.describe Groups::DependencyProxyForContainersController do context 'with a valid user' do before do group.add_guest(user) + end - expect_next_found_instance_of(Group) do |instance| - expect(instance).to receive_message_chain(:dependency_proxy_manifests, :create!) + it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest' + + context 'with no existing manifest' do + it 'creates a manifest' do + expect { subject }.to change { group.dependency_proxy_manifests.count }.by(1) + + manifest = group.dependency_proxy_manifests.first.reload + expect(manifest.content_type).to eq(content_type) + expect(manifest.digest).to eq(digest) + expect(manifest.file_name).to eq(file_name) end end - it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest' + context 'with existing stale manifest' do + let_it_be(:old_digest) { 'asdf' } + let_it_be_with_reload(:manifest) { create(:dependency_proxy_manifest, file_name: file_name, digest: old_digest, group: group) } + + it 'updates the existing manifest' do + expect { subject }.to change { group.dependency_proxy_manifests.count }.by(0) + .and change { manifest.reload.digest }.from(old_digest).to(digest) + end + end end end diff --git a/spec/features/projects/confluence/user_views_confluence_page_spec.rb b/spec/features/projects/confluence/user_views_confluence_page_spec.rb index ece2f82f5c6..49e7839f16c 100644 --- a/spec/features/projects/confluence/user_views_confluence_page_spec.rb +++ b/spec/features/projects/confluence/user_views_confluence_page_spec.rb @@ -16,9 +16,12 @@ RSpec.describe 'User views the Confluence page' do visit project_wikis_confluence_path(project) + expect(page).to have_css('.nav-sidebar li.active', text: 'Confluence', match: :first) + element = page.find('.row.empty-state') expect(element).to have_link('Go to Confluence', href: service.confluence_url) + expect(element).to have_link('Confluence epic', href: 'https://gitlab.com/groups/gitlab-org/-/epics/3629') end it 'does not show the page when the Confluence integration disabled' do diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js index 939a2939243..3878f1e162f 100644 --- a/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js +++ b/spec/frontend/pipeline_editor/components/header/pipeline_status_spec.js @@ -16,7 +16,7 @@ describe('Pipeline Status', () => { let mockApollo; let mockPipelineQuery; - const createComponentWithApollo = (glFeatures = {}) => { + const createComponentWithApollo = () => { const handlers = [[getPipelineQuery, mockPipelineQuery]]; mockApollo = createMockApollo(handlers); @@ -27,7 +27,6 @@ describe('Pipeline Status', () => { commitSha: mockCommitSha, }, provide: { - glFeatures, projectFullPath: mockProjectFullPath, }, stubs: { GlLink, GlSprintf }, @@ -106,8 +105,8 @@ describe('Pipeline Status', () => { expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath); }); - it('does not render the pipeline mini graph', () => { - expect(findPipelineEditorMiniGraph().exists()).toBe(false); + it('renders the pipeline mini graph', () => { + expect(findPipelineEditorMiniGraph().exists()).toBe(true); }); }); @@ -150,19 +149,4 @@ describe('Pipeline Status', () => { }); }); }); - - describe('when feature flag for pipeline mini graph is enabled', () => { - beforeEach(() => { - mockPipelineQuery.mockResolvedValue({ - data: { project: mockProjectPipeline() }, - }); - - createComponentWithApollo({ pipelineEditorMiniGraph: true }); - waitForPromises(); - }); - - it('renders the pipeline mini graph', () => { - expect(findPipelineEditorMiniGraph().exists()).toBe(true); - }); - }); }); diff --git a/spec/frontend/sidebar/components/attention_required_toggle_spec.js b/spec/frontend/sidebar/components/attention_required_toggle_spec.js new file mode 100644 index 00000000000..e3da456684b --- /dev/null +++ b/spec/frontend/sidebar/components/attention_required_toggle_spec.js @@ -0,0 +1,84 @@ +import { GlButton } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import AttentionRequiredToggle from '~/sidebar/components/attention_required_toggle.vue'; + +let wrapper; + +function factory(propsData = {}) { + wrapper = mount(AttentionRequiredToggle, { propsData }); +} + +const findToggle = () => wrapper.findComponent(GlButton); + +describe('Attention require toggle', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('renders button', () => { + factory({ type: 'reviewer', user: { attention_required: false } }); + + expect(findToggle().exists()).toBe(true); + }); + + it.each` + attentionRequired | icon + ${true} | ${'star'} + ${false} | ${'star-o'} + `( + 'renders $icon icon when attention_required is $attentionRequired', + ({ attentionRequired, icon }) => { + factory({ type: 'reviewer', user: { attention_required: attentionRequired } }); + + expect(findToggle().props('icon')).toBe(icon); + }, + ); + + it.each` + attentionRequired | variant + ${true} | ${'warning'} + ${false} | ${'default'} + `( + 'renders button with variant $variant when attention_required is $attentionRequired', + ({ attentionRequired, variant }) => { + factory({ type: 'reviewer', user: { attention_required: attentionRequired } }); + + expect(findToggle().props('variant')).toBe(variant); + }, + ); + + it('emits toggle-attention-required on click', async () => { + factory({ type: 'reviewer', user: { attention_required: true } }); + + await findToggle().trigger('click'); + + expect(wrapper.emitted('toggle-attention-required')[0]).toEqual([ + { + user: { attention_required: true }, + callback: expect.anything(), + }, + ]); + }); + + it('sets loading on click', async () => { + factory({ type: 'reviewer', user: { attention_required: true } }); + + await findToggle().trigger('click'); + + expect(findToggle().props('loading')).toBe(true); + }); + + it.each` + type | attentionRequired | tooltip + ${'reviewer'} | ${true} | ${AttentionRequiredToggle.i18n.removeAttentionRequired} + ${'reviewer'} | ${false} | ${AttentionRequiredToggle.i18n.attentionRequiredReviewer} + ${'assignee'} | ${false} | ${AttentionRequiredToggle.i18n.attentionRequiredAssignee} + `( + 'sets tooltip as $tooltip when attention_required is $attentionRequired and type is $type', + ({ type, attentionRequired, tooltip }) => { + factory({ type, user: { attention_required: attentionRequired } }); + + expect(findToggle().attributes('aria-label')).toBe(tooltip); + }, + ); +}); diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js index 6b80224083a..668d08f0a55 100644 --- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js +++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; +import AttentionRequiredToggle from '~/sidebar/components/attention_required_toggle.vue'; import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue'; import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue'; import userDataMock from '../../user_data_mock'; @@ -9,7 +10,7 @@ describe('UncollapsedReviewerList component', () => { const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]'); - function createComponent(props = {}) { + function createComponent(props = {}, glFeatures = {}) { const propsData = { users: [], rootPath: TEST_HOST, @@ -18,6 +19,9 @@ describe('UncollapsedReviewerList component', () => { wrapper = shallowMount(UncollapsedReviewerList, { propsData, + provide: { + glFeatures, + }, }); } @@ -110,4 +114,18 @@ describe('UncollapsedReviewerList component', () => { expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true); }); }); + + it('hides re-request review button when attentionRequired feature flag is enabled', () => { + createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true }); + + expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0); + }); + + it('emits toggle-attention-required', () => { + createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true }); + + wrapper.find(AttentionRequiredToggle).vm.$emit('toggle-attention-required', 'data'); + + expect(wrapper.emitted('toggle-attention-required')[0]).toEqual(['data']); + }); }); diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index cb84c142d55..b87de89802c 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -4,8 +4,11 @@ import * as urlUtility from '~/lib/utils/url_utility'; import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; +import toast from '~/vue_shared/plugins/global_toast'; import Mock from './mock_data'; +jest.mock('~/vue_shared/plugins/global_toast'); + describe('Sidebar mediator', () => { const { mediator: mediatorMockData } = Mock; let mock; @@ -115,4 +118,56 @@ describe('Sidebar mediator', () => { urlSpy.mockRestore(); }); }); + + describe('toggleAttentionRequired', () => { + let attentionRequiredService; + + beforeEach(() => { + attentionRequiredService = jest + .spyOn(mediator.service, 'attentionRequired') + .mockResolvedValue(); + }); + + it('calls attentionRequired service method', async () => { + mediator.store.reviewers = [{ id: 1, attention_required: false, username: 'root' }]; + + await mediator.toggleAttentionRequired('reviewer', { + user: { id: 1, username: 'root' }, + callback: jest.fn(), + }); + + expect(attentionRequiredService).toHaveBeenCalledWith(1); + }); + + it.each` + type | method + ${'reviewer'} | ${'findReviewer'} + `('finds $type', ({ type, method }) => { + const methodSpy = jest.spyOn(mediator.store, method); + + mediator.toggleAttentionRequired(type, { user: { id: 1 }, callback: jest.fn() }); + + expect(methodSpy).toHaveBeenCalledWith({ id: 1 }); + }); + + it.each` + attentionRequired | toastMessage + ${true} | ${'Removed attention request from @root'} + ${false} | ${'Requested attention from @root'} + `( + 'it creates toast $toastMessage when attention_required is $attentionRequired', + async ({ attentionRequired, toastMessage }) => { + mediator.store.reviewers = [ + { id: 1, attention_required: attentionRequired, username: 'root' }, + ]; + + await mediator.toggleAttentionRequired('reviewer', { + user: { id: 1, username: 'root' }, + callback: jest.fn(), + }); + + expect(toast).toHaveBeenCalledWith(toastMessage); + }, + ); + }); }); diff --git a/spec/graphql/types/dependency_proxy/manifest_type_spec.rb b/spec/graphql/types/dependency_proxy/manifest_type_spec.rb index 18cc89adfcb..b251ca63c4f 100644 --- a/spec/graphql/types/dependency_proxy/manifest_type_spec.rb +++ b/spec/graphql/types/dependency_proxy/manifest_type_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['DependencyProxyManifest'] do it 'includes dependency proxy manifest fields' do expected_fields = %w[ - file_name image_name size created_at updated_at digest + id file_name image_name size created_at updated_at digest ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/helpers/groups/settings_helper_spec.rb b/spec/helpers/groups/settings_helper_spec.rb new file mode 100644 index 00000000000..f8c0bfc19a1 --- /dev/null +++ b/spec/helpers/groups/settings_helper_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::SettingsHelper do + include GroupsHelper + + let_it_be(:group) { create(:group, path: "foo") } + + describe('#group_settings_confirm_modal_data') do + using RSpec::Parameterized::TableSyntax + + fake_form_id = "fake_form_id" + + where(:is_paid, :is_button_disabled, :form_value_id) do + true | "true" | nil + true | "true" | fake_form_id + false | "false" | nil + false | "false" | fake_form_id + end + + with_them do + it "returns expected parameters" do + allow(group).to receive(:paid?).and_return(is_paid) + + expected = helper.group_settings_confirm_modal_data(group, form_value_id) + expect(expected).to eq({ + button_text: "Remove group", + confirm_danger_message: remove_group_message(group), + remove_form_id: form_value_id, + phrase: group.full_path, + button_testid: "remove-group-button", + disabled: is_button_disabled + }) + end + end + end +end diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb index 47a8fcf5dd0..259d7d5ad13 100644 --- a/spec/lib/container_registry/client_spec.rb +++ b/spec/lib/container_registry/client_spec.rb @@ -279,7 +279,7 @@ RSpec.describe ContainerRegistry::Client do it 'uploads the manifest and returns the digest' do stub_request(:put, "http://container-registry/v2/path/manifests/tagA") .with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers) - .to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' }) + .to_return(status: 200, body: "", headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:123' }) expect_new_faraday(timeout: false) diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index d6e6b254dd9..9b931ab6dbc 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -213,7 +213,7 @@ RSpec.describe ContainerRegistry::Tag do before do stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag') .with(headers: headers) - .to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) + .to_return(status: 200, headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:digest' }) end describe '#digest' do diff --git a/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb b/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb index 45d81808971..02c9499bedb 100644 --- a/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/primary_host_spec.rb @@ -51,7 +51,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do end describe '#offline!' do - it 'does nothing' do + it 'logs the event but does nothing else' do + expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn) + .with(hash_including(event: :host_offline)) + .and_call_original + expect(host.offline!).to be_nil end end diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 82ef4675553..89ddde4a01d 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -107,36 +107,14 @@ RSpec.describe Gitlab::PrometheusClient do let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"} shared_examples 'exceptions are raised' do - it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SocketError is rescued' do - req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) + Gitlab::HTTP::HTTP_ERRORS.each do |error| + it "raises a Gitlab::PrometheusClient::ConnectionError when a #{error} is rescued" do + req_stub = stub_prometheus_request_with_exception(prometheus_url, error.new) - expect { subject } - .to raise_error(Gitlab::PrometheusClient::ConnectionError, "Can't connect to #{prometheus_url}") - expect(req_stub).to have_been_requested - end - - it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SSLError is rescued' do - req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError) - - expect { subject } - .to raise_error(Gitlab::PrometheusClient::ConnectionError, "#{prometheus_url} contains invalid SSL data") - expect(req_stub).to have_been_requested - end - - it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError is rescued' do - req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError) - - expect { subject } - .to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error") - expect(req_stub).to have_been_requested - end - - it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError with a code is rescued' do - req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError.new(code: 400)) - - expect { subject } - .to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error") - expect(req_stub).to have_been_requested + expect { subject } + .to raise_error(Gitlab::PrometheusClient::ConnectionError, kind_of(String)) + expect(req_stub).to have_been_requested + end end end diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb index 01c987a1d92..4158e8a0a4c 100644 --- a/spec/models/concerns/prometheus_adapter_spec.rb +++ b/spec/models/concerns/prometheus_adapter_spec.rb @@ -165,6 +165,14 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) } end end + + context "when client raises Gitlab::PrometheusClient::ConnectionError" do + before do + stub_any_prometheus_request.to_raise(Gitlab::PrometheusClient::ConnectionError) + end + + it { is_expected.to include(success: false, result: kind_of(String)) } + end end describe '#build_query_args' do diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb index 4629ff4b5b3..59415096989 100644 --- a/spec/models/dependency_proxy/manifest_spec.rb +++ b/spec/models/dependency_proxy/manifest_spec.rb @@ -15,6 +15,17 @@ RSpec.describe DependencyProxy::Manifest, type: :model do it { is_expected.to validate_presence_of(:digest) } end + describe 'scopes' do + let_it_be(:manifest_one) { create(:dependency_proxy_manifest) } + let_it_be(:manifest_two) { create(:dependency_proxy_manifest) } + let_it_be(:manifests) { [manifest_one, manifest_two] } + let_it_be(:ids) { manifests.map(&:id) } + + it 'order_id_desc' do + expect(described_class.where(id: ids).order_id_desc.to_a).to eq [manifest_two, manifest_one] + end + end + describe 'file is being stored' do subject { create(:dependency_proxy_manifest) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index d7a23cfc796..ba4429451d1 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -306,7 +306,7 @@ RSpec.describe Issue do end describe '#reopen' do - let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) } + let_it_be_with_reload(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) } it 'sets closed_at to nil when an issue is reopened' do expect { issue.reopen }.to change { issue.closed_at }.to(nil) @@ -316,6 +316,22 @@ RSpec.describe Issue do expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil) end + it 'clears moved_to_id for moved issues' do + moved_issue = create(:issue) + + issue.update!(moved_to_id: moved_issue.id) + + expect { issue.reopen }.to change { issue.moved_to_id }.from(moved_issue.id).to(nil) + end + + it 'clears duplicated_to_id for duplicated issues' do + duplicate_issue = create(:issue) + + issue.update!(duplicated_to_id: duplicate_issue.id) + + expect { issue.reopen }.to change { issue.duplicated_to_id }.from(duplicate_issue.id).to(nil) + end + it 'changes the state to opened' do expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened]) end diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb index 30e704adb92..3527c8183f6 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb @@ -116,4 +116,26 @@ RSpec.describe 'getting dependency proxy manifests in a group' do expect(dependency_proxy_image_count_response).to eq(manifests.size) end + + describe 'sorting and pagination' do + let(:data_path) { ['group', :dependencyProxyManifests] } + let(:current_user) { owner } + + context 'with default sorting' do + let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest)} } + + it_behaves_like 'sorted paginated query' do + let(:sort_param) { '' } + let(:first_param) { 2 } + let(:all_records) { descending_manifests } + end + end + + def pagination_query(params) + # remove sort since the type does not accept sorting, but be future proof + graphql_query_for('group', { 'fullPath' => group.full_path }, + query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil)) + ) + end + end end diff --git a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb index f2a605756fb..407ee42d345 100644 --- a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb +++ b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb @@ -13,7 +13,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do let(:token) { Digest::SHA256.hexdigest('123') } let(:headers) do { - 'docker-content-digest' => dependency_proxy_manifest.digest, + DependencyProxy::Manifest::DIGEST_HEADER => dependency_proxy_manifest.digest, 'content-type' => dependency_proxy_manifest.content_type } end @@ -100,8 +100,8 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do let(:content_type) { 'new-content-type' } before do - stub_manifest_head(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type }) - stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type }) + stub_manifest_head(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type }) + stub_manifest_download(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type }) end it_behaves_like 'returning no manifest' diff --git a/spec/services/dependency_proxy/head_manifest_service_spec.rb b/spec/services/dependency_proxy/head_manifest_service_spec.rb index 9c1e4d650f8..949a8eb3bee 100644 --- a/spec/services/dependency_proxy/head_manifest_service_spec.rb +++ b/spec/services/dependency_proxy/head_manifest_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::HeadManifestService do let(:content_type) { 'foo' } let(:headers) do { - 'docker-content-digest' => digest, + DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type } end diff --git a/spec/services/dependency_proxy/pull_manifest_service_spec.rb b/spec/services/dependency_proxy/pull_manifest_service_spec.rb index b3053174cc0..6018a3229fb 100644 --- a/spec/services/dependency_proxy/pull_manifest_service_spec.rb +++ b/spec/services/dependency_proxy/pull_manifest_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::PullManifestService do let(:digest) { '12345' } let(:content_type) { 'foo' } let(:headers) do - { 'docker-content-digest' => digest, 'content-type' => content_type } + { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type } end subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) } diff --git a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb index 80f011f622b..21be989d697 100644 --- a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb +++ b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb @@ -31,14 +31,14 @@ RSpec.shared_context 'container repository delete tags service shared context' d end end - def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' }) + def stub_put_manifest_request(tag, status = 200, headers = { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:dummy' }) stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") .to_return(status: status, body: '', headers: headers) end def stub_tag_digest(tag, digest) stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") - .to_return(status: 200, body: '', headers: { 'docker-content-digest' => digest }) + .to_return(status: 200, body: '', headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest }) end def stub_digest_config(digest, created_at) diff --git a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb index d29c677a962..5d1488502d2 100644 --- a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb +++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb @@ -26,7 +26,7 @@ RSpec.shared_examples 'a successful manifest pull' do subject expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest) + expect(response.headers[DependencyProxy::Manifest::DIGEST_HEADER]).to eq(manifest.digest) expect(response.headers['Content-Length']).to eq(manifest.size) expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION) expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"") diff --git a/spec/views/groups/settings/_remove.html.haml_spec.rb b/spec/views/groups/settings/_remove.html.haml_spec.rb index 07fe900bc2d..e40fda58a72 100644 --- a/spec/views/groups/settings/_remove.html.haml_spec.rb +++ b/spec/views/groups/settings/_remove.html.haml_spec.rb @@ -9,8 +9,8 @@ RSpec.describe 'groups/settings/_remove.html.haml' do render 'groups/settings/remove', group: group - expect(rendered).to have_selector '[data-testid="remove-group-button"]' - expect(rendered).not_to have_selector '[data-testid="remove-group-button"].disabled' + expect(rendered).to have_selector '[data-button-testid="remove-group-button"]' + expect(rendered).not_to have_selector '[data-button-testid="remove-group-button"].disabled' expect(rendered).not_to have_selector '[data-testid="group-has-linked-subscription-alert"]' end end diff --git a/tooling/bin/shellcheck b/tooling/bin/shellcheck deleted file mode 100755 index b499bfa3e5e..00000000000 --- a/tooling/bin/shellcheck +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -root="$(cd "$(dirname "$0")/../.." || exit ; pwd -P)" - -if [ $# -ne 0 ]; then - shellcheck --exclude=SC1071 --external-sources "$@" -else - find \ - "${root}/bin" \ - "${root}/tooling" \ - -type f \ - -not -path "*.swp" \ - -not -path "*.rb" \ - -not -path "*.js" \ - -not -path "*.md" \ - -not -path "*.haml" \ - -not -path "*/Gemfile*" \ - -not -path '*/.bundle*' \ - -not -path '*/Makefile*' \ - -print0 \ - | xargs -0 shellcheck --exclude=SC1071 --external-sources -- -fi |