Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-19 15:10:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-19 15:10:37 +0300
commita4db97517ad095914c0652a07486ac607d99dab4 (patch)
tree58f57b42c52b1b4231cab44ef3934cbe55991d25
parent17295c75a1a28df78f719e0098dd31fe45ce0446 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/qa-common/main.gitlab-ci.yml25
-rw-r--r--.gitlab/ci/qa-common/variables.gitlab-ci.yml3
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue1
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue5
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/organization_settings.vue27
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue21
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js1
-rw-r--r--app/assets/javascripts/organizations/show/components/organization_avatar.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue112
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/breadcrumbs.scss21
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss22
-rw-r--r--app/assets/stylesheets/framework/header.scss124
-rw-r--r--app/assets/stylesheets/framework/top_bar.scss18
-rw-r--r--app/controllers/uploads_controller.rb5
-rw-r--r--app/helpers/organizations/organization_helper.rb4
-rw-r--r--app/models/container_registry/protection/rule.rb17
-rw-r--r--app/models/organizations/organization.rb2
-rw-r--r--app/services/organizations/update_service.rb4
-rw-r--r--app/views/dashboard/todos/index.html.haml5
-rw-r--r--config/feature_flags/development/ci_job_token_scope.yml8
-rw-r--r--config/routes/uploads.rb5
-rw-r--r--doc/administration/settings/jira_cloud_app_troubleshooting.md14
-rw-r--r--doc/api/container_registry.md26
-rw-r--r--doc/ci/jobs/ci_job_token.md2
-rw-r--r--doc/development/code_owners/index.md2
-rw-r--r--doc/development/documentation/styleguide/index.md3
-rw-r--r--doc/integration/jira/dvcs/troubleshooting.md20
-rw-r--r--doc/integration/jira/troubleshooting.md6
-rw-r--r--doc/user/project/integrations/gitlab_slack_app_troubleshooting.md8
-rw-r--r--doc/user/project/members/share_project_with_groups.md9
-rw-r--r--doc/user/public_access.md24
-rw-r--r--lib/api/helpers.rb3
-rw-r--r--lib/integrations/google_cloud_platform/base_client.rb2
-rw-r--r--lib/sidebars/organizations/menus/scope_menu.rb2
-rw-r--r--locale/gitlab.pot15
-rw-r--r--qa/qa/specs/helpers/fast_quarantine.rb70
-rw-r--r--qa/qa/specs/spec_helper.rb7
-rw-r--r--qa/spec/README.md8
-rw-r--r--qa/spec/specs/helpers/fast_quarantine_spec.rb34
-rw-r--r--spec/controllers/uploads_controller_spec.rb39
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb4
-rw-r--r--spec/frontend/organizations/settings/general/components/organization_settings_spec.js61
-rw-r--r--spec/frontend/organizations/shared/components/new_edit_form_spec.js39
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/avatar_upload_dropzone_spec.js116
-rw-r--r--spec/helpers/organizations/organization_helper_spec.rb11
-rw-r--r--spec/lib/api/helpers_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb6
-rw-r--r--spec/lib/sidebars/organizations/menus/scope_menu_spec.rb5
-rw-r--r--spec/models/container_registry/protection/rule_spec.rb190
-rw-r--r--spec/models/organizations/organization_spec.rb1
-rw-r--r--spec/requests/api/graphql/boards/board_list_issues_query_spec.rb4
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb15
-rw-r--r--spec/routing/uploads_routing_spec.rb13
-rw-r--r--spec/services/organizations/update_service_spec.rb8
57 files changed, 953 insertions, 318 deletions
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml
index 836f1559264..ee27f9a2fbd 100644
--- a/.gitlab/ci/qa-common/main.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml
@@ -69,11 +69,6 @@ stages:
QA_INTERCEPT_REQUESTS: "true"
GITLAB_LICENSE_MODE: test
GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
- before_script:
- - !reference [.qa-base, before_script]
- # Prepend the file paths with the absolute path from inside the container since the files will be read from there
- - export RSPEC_FAST_QUARANTINE_PATH="/home/gitlab/qa/${RSPEC_FAST_QUARANTINE_PATH}"
- - export RSPEC_SKIPPED_TESTS_REPORT_PATH="/home/gitlab/qa/rspec/skipped_tests-${CI_JOB_ID}.txt"
# Allow QA jobs to fail as they are flaky. The top level `package-and-e2e:ee`
# pipeline is not allowed to fail, so without allowing QA to fail, we will be
# blocking merges due to flaky tests.
@@ -98,26 +93,6 @@ stages:
- qa/knapsack/*.json
expire_in: 1 day
-.download-fast-quarantine-report:
- image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}alpine:edge
- stage: .pre
- variables:
- GIT_STRATEGY: none
- before_script:
- - apk add --no-cache --update curl bash
- script:
- - mkdir -p "${QA_RSPEC_REPORT_PATH}"
- - |
- if [[ ! -f "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}" ]]; then
- curl --location -o "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}" "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/${RSPEC_FAST_QUARANTINE_PATH}" ||
- echo "" > "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}"
- fi
- allow_failure: true
- artifacts:
- paths:
- - "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}"
- expire_in: 1 day
-
.upload-knapsack-report:
extends:
- .generate-knapsack-report-base
diff --git a/.gitlab/ci/qa-common/variables.gitlab-ci.yml b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
index bc756b6808a..2ac1121791d 100644
--- a/.gitlab/ci/qa-common/variables.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
@@ -14,7 +14,4 @@ variables:
QA_RUN_ALL_TESTS: "true"
# Used by gitlab-qa to set up a volume for `${CI_PROJECT_DIR}/qa/rspec:/home/gitlab/qa/rspec/`
QA_RSPEC_REPORT_PATH: "${CI_PROJECT_DIR}/qa/rspec"
- RSPEC_FAST_QUARANTINE_FILE: "fast_quarantine-gitlab.txt"
- # This path is relative to /home/gitlab/qa/ in the QA container
- RSPEC_FAST_QUARANTINE_PATH: "rspec/${RSPEC_FAST_QUARANTINE_FILE}"
QA_OMNIBUS_MR_TESTS: "only-smoke-reliable"
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index 7f76b7ca1ac..5d9663abaf2 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -94,7 +94,6 @@ export default {
<gl-dropdown-item
v-else-if="isDropdownWithEmojiTrigger"
v-bind="componentAttributes"
- button-class="top-nav-menu-item"
@click="openModal"
>
{{ displayText }}
diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue
index f7f7b79d52b..ee50afd3613 100644
--- a/app/assets/javascripts/organizations/new/components/app.vue
+++ b/app/assets/javascripts/organizations/new/components/app.vue
@@ -42,7 +42,10 @@ export default {
} = await this.$apollo.mutate({
mutation: organizationCreateMutation,
variables: {
- input: { name: formValues.name, path: formValues.path },
+ input: { name: formValues.name, path: formValues.path, avatar: formValues.avatar },
+ },
+ context: {
+ hasUpload: formValues.avatar instanceof File,
},
});
diff --git a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
index 1acc4c54f75..283a652f90e 100644
--- a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
+++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
@@ -3,7 +3,11 @@ import { s__, __ } from '~/locale';
import { createAlert } from '~/alert';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
-import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
+import {
+ FORM_FIELD_NAME,
+ FORM_FIELD_ID,
+ FORM_FIELD_AVATAR,
+} from '~/organizations/shared/constants';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
@@ -25,7 +29,7 @@ export default {
),
successMessage: s__('Organization|Organization was successfully updated.'),
},
- fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR],
data() {
return {
loading: false,
@@ -33,9 +37,24 @@ export default {
};
},
methods: {
+ avatarInput(formValues) {
+ // Organization has an avatar and it is been explicitly removed.
+ if (this.organization.avatar && formValues.avatar === null) {
+ return { avatar: null };
+ }
+
+ // Avatar has been set or changed.
+ if (formValues.avatar instanceof File) {
+ return { avatar: formValues.avatar };
+ }
+
+ // Avatar has not been changed at all, do not include the `avatar` key in input.
+ return {};
+ },
async onSubmit(formValues) {
this.errors = [];
this.loading = true;
+
try {
const {
data: {
@@ -47,8 +66,12 @@ export default {
input: {
id: convertToGraphQLId(TYPE_ORGANIZATION, this.organization.id),
name: formValues.name,
+ ...this.avatarInput(formValues),
},
},
+ context: {
+ hasUpload: formValues.avatar instanceof File,
+ },
});
if (errors.length) {
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
index c5bb16b944a..3567fa490ea 100644
--- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -3,10 +3,12 @@ import { GlForm, GlFormFields, GlButton } from '@gitlab/ui';
import { formValidators } from '@gitlab/ui/dist/utils';
import { s__, __ } from '~/locale';
import { slugify } from '~/lib/utils/text_utility';
+import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue';
import {
FORM_FIELD_NAME,
FORM_FIELD_ID,
FORM_FIELD_PATH,
+ FORM_FIELD_AVATAR,
FORM_FIELD_PATH_VALIDATORS,
} from '../constants';
import OrganizationUrlField from './organization_url_field.vue';
@@ -18,6 +20,7 @@ export default {
GlFormFields,
GlButton,
OrganizationUrlField,
+ AvatarUploadDropzone,
},
i18n: {
cancel: __('Cancel'),
@@ -36,6 +39,7 @@ export default {
return {
[FORM_FIELD_NAME]: '',
[FORM_FIELD_PATH]: '',
+ [FORM_FIELD_AVATAR]: null,
};
},
},
@@ -43,7 +47,7 @@ export default {
type: Array,
required: false,
default() {
- return [FORM_FIELD_NAME, FORM_FIELD_PATH];
+ return [FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_AVATAR];
},
},
submitButtonText: {
@@ -98,6 +102,13 @@ export default {
class: 'gl-w-full',
},
},
+ [FORM_FIELD_AVATAR]: {
+ label: s__('Organization|Organization avatar'),
+ groupAttrs: {
+ class: 'gl-w-full',
+ labelSrOnly: true,
+ },
+ },
};
return Object.entries(fields).reduce((accumulator, [fieldKey, fieldDefinition]) => {
@@ -148,6 +159,14 @@ export default {
@blur="blur"
/>
</template>
+ <template #input(avatar)="{ input, value }">
+ <avatar-upload-dropzone
+ :value="value"
+ :entity="formValues"
+ :label="fields.avatar.label"
+ @input="input"
+ />
+ </template>
</gl-form-fields>
<div class="gl-display-flex gl-gap-3">
<gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
index 7287d84f99f..9f3d066f984 100644
--- a/app/assets/javascripts/organizations/shared/constants.js
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -4,6 +4,7 @@ import { s__ } from '~/locale';
export const FORM_FIELD_NAME = 'name';
export const FORM_FIELD_ID = 'id';
export const FORM_FIELD_PATH = 'path';
+export const FORM_FIELD_AVATAR = 'avatar';
export const FORM_FIELD_PATH_VALIDATORS = [
formValidators.required(s__('Organization|Organization URL is required.')),
diff --git a/app/assets/javascripts/organizations/show/components/organization_avatar.vue b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
index c57ee0ea5b5..d569af3e9b4 100644
--- a/app/assets/javascripts/organizations/show/components/organization_avatar.vue
+++ b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
@@ -44,6 +44,7 @@ export default {
:entity-name="organization.name"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
:size="64"
+ :src="organization.avatar_url"
/>
<div class="gl-ml-3">
<div class="gl-display-flex gl-align-items-center">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 1516b63f96d..2607d8e4bb7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -119,7 +119,11 @@ export default {
},
) {
if (mergeRequestMergeStatusUpdated) {
- this.state = mergeRequestMergeStatusUpdated;
+ this.state = {
+ ...mergeRequestMergeStatusUpdated,
+ mergeRequestsFfOnlyEnabled: this.state.mergeRequestsFfOnlyEnabled,
+ onlyAllowMergeIfPipelineSucceeds: this.state.onlyAllowMergeIfPipelineSucceeds,
+ };
if (!this.commitMessageIsTouched) {
this.commitMessage = mergeRequestMergeStatusUpdated.defaultMergeCommitMessage;
diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue
new file mode 100644
index 00000000000..944a48df279
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlButton, GlAvatar, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { __ } from '~/locale';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+
+export default {
+ i18n: {
+ uploadText: __('Drop or %{linkStart}upload%{linkEnd} an avatar.'),
+ maxFileSize: __('Max file size is 200 KiB.'),
+ removeAvatar: __('Remove avatar'),
+ },
+ AVATAR_SHAPE_OPTION_RECT,
+ components: { GlButton, GlAvatar, GlSprintf, GlTruncate, UploadDropzone },
+ props: {
+ entity: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ value: {
+ type: [String, File],
+ required: false,
+ default: '',
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ avatarObjectUrl: null,
+ };
+ },
+ computed: {
+ avatarSrc() {
+ if (this.avatarObjectUrl) {
+ return this.avatarObjectUrl;
+ }
+
+ if (this.isValueAFile) {
+ return null;
+ }
+
+ return this.value;
+ },
+ isValueAFile() {
+ return this.value instanceof File;
+ },
+ },
+ watch: {
+ value(newValue) {
+ this.revokeAvatarObjectUrl();
+
+ if (newValue instanceof File) {
+ this.avatarObjectUrl = URL.createObjectURL(newValue);
+ } else {
+ this.avatarObjectUrl = null;
+ }
+ },
+ },
+ beforeDestroy() {
+ this.revokeAvatarObjectUrl();
+ },
+ methods: {
+ revokeAvatarObjectUrl() {
+ if (this.avatarObjectUrl === null) {
+ return;
+ }
+
+ URL.revokeObjectURL(this.avatarObjectUrl);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-column-gap-5">
+ <gl-avatar
+ :entity-id="entity.id || null"
+ :entity-name="entity.name || 'organization'"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="96"
+ :src="avatarSrc"
+ />
+ <div class="gl-min-w-0">
+ <p class="gl-font-weight-bold gl-line-height-1 gl-mb-3">
+ {{ label }}
+ </p>
+ <div v-if="value" class="gl-display-flex gl-align-items-center gl-column-gap-3">
+ <gl-button @click="$emit('input', null)">{{ $options.i18n.removeAvatar }}</gl-button>
+ <gl-truncate
+ v-if="isValueAFile"
+ class="gl-text-secondary gl-max-w-48 gl-min-w-0"
+ position="middle"
+ :text="value.name"
+ />
+ </div>
+ <upload-dropzone v-else single-file-selection @change="$emit('input', $event)">
+ <template #upload-text>
+ <gl-sprintf :message="$options.i18n.uploadText">
+ <template #link="{ content }">
+ <span class="gl-link gl-hover-text-decoration-underline">{{ content }}</span>
+ </template>
+ </gl-sprintf>
+ </template>
+ </upload-dropzone>
+ <p class="gl-mb-0 gl-mt-3 gl-text-secondary">{{ $options.i18n.maxFileSize }}</p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 6f4f7a29334..19eaac9e908 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -10,6 +10,7 @@
@import 'framework/animations';
@import 'framework/vue_transitions';
@import 'framework/blocks';
+@import 'framework/breadcrumbs';
@import 'framework/buttons';
@import 'framework/badges';
@import 'framework/calendar';
@@ -23,6 +24,7 @@
@import 'framework/gfm';
@import 'framework/kbd';
@import 'framework/header';
+@import 'framework/top_bar';
@import 'framework/highlight';
@import 'framework/lists';
@import 'framework/logo';
diff --git a/app/assets/stylesheets/framework/breadcrumbs.scss b/app/assets/stylesheets/framework/breadcrumbs.scss
new file mode 100644
index 00000000000..2ed611f7ba9
--- /dev/null
+++ b/app/assets/stylesheets/framework/breadcrumbs.scss
@@ -0,0 +1,21 @@
+.breadcrumbs {
+ flex: 1;
+ min-width: 0;
+ align-self: center;
+ color: $gl-text-color-secondary;
+
+ .avatar-tile {
+ margin-right: 4px;
+ border: 1px solid $border-color;
+ border-radius: 50%;
+ vertical-align: sub;
+ }
+}
+
+.breadcrumb-item-text {
+ text-decoration: inherit;
+
+ @include media-breakpoint-down(xs) {
+ @include str-truncated(128px);
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index e791a0dbbbd..8282f6143c2 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -806,28 +806,6 @@
}
}
-@include media-breakpoint-down(xs) {
- .navbar-gitlab {
- li.dropdown {
- position: static;
- }
- }
-
- header.navbar-gitlab .dropdown {
- .dropdown-menu {
- width: 100%;
- min-width: 100%;
- }
- }
-
- header.navbar-gitlab-new .header-content .dropdown {
- .dropdown-menu {
- left: 0;
- min-width: 100%;
- }
- }
-}
-
.dropdown-content-faded-mask {
position: relative;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 23f40dfe4bf..84e69e40bc2 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -9,119 +9,6 @@
left: 0;
right: 0;
border-radius: 0;
-
- .close-icon {
- display: none;
- }
-
- .header-content {
- width: 100%;
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: var(--header-height);
- padding-left: 0;
-
- .title {
- padding-right: 0;
- color: currentColor;
- display: flex;
- position: relative;
- margin: 0;
- font-size: 18px;
- vertical-align: top;
- white-space: nowrap;
-
- img {
- height: 24px;
-
- + .logo-text {
- margin-left: 8px;
- }
- }
-
- &.wrap {
- white-space: normal;
- }
-
- &.initializing {
- opacity: 0;
- }
-
- a:not(.canary-badge) {
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 4px 2px 4px -8px;
- border-radius: $border-radius-default;
-
- &:active,
- &:focus {
- @include gl-focus($focus-ring: $focus-ring-dark);
- }
- }
- }
-
- .dropdown.open {
- > a {
- border-bottom-color: $white;
- }
- }
- }
-
- .container-fluid {
- padding: 0;
-
- .nav > li {
- > a {
- will-change: color;
- margin: 4px 0;
- padding: 6px 8px;
- height: 32px;
- }
- }
- }
-}
-
-.top-bar-container {
- min-height: $top-bar-height;
-}
-
-.top-bar-fixed {
- @include gl-inset-border-b-1-gray-100;
- background-color: $body-bg;
- left: var(--application-bar-left);
- position: fixed;
- right: var(--application-bar-right);
- top: $calc-application-bars-height;
- width: auto;
- z-index: $top-bar-z-index;
-
- @media (prefers-reduced-motion: no-preference) {
- transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium;
- }
-}
-
-.breadcrumbs {
- flex: 1;
- min-width: 0;
- align-self: center;
- color: $gl-text-color-secondary;
-
- .avatar-tile {
- margin-right: 4px;
- border: 1px solid $border-color;
- border-radius: 50%;
- vertical-align: sub;
- }
-}
-
-.breadcrumb-item-text {
- text-decoration: inherit;
-
- @include media-breakpoint-down(xs) {
- @include str-truncated(128px);
- }
}
.navbar-empty {
@@ -173,17 +60,6 @@
@include media-breakpoint-down(xs) { margin-right: 3px; }
}
-.top-nav-menu-item {
- &.active,
- &:hover {
- background-color: $nav-active-bg !important;
- }
-
- .gl-icon {
- color: inherit !important;
- }
-}
-
.header-logged-out {
z-index: $header-zindex;
min-height: var(--header-height);
diff --git a/app/assets/stylesheets/framework/top_bar.scss b/app/assets/stylesheets/framework/top_bar.scss
new file mode 100644
index 00000000000..424d4269c52
--- /dev/null
+++ b/app/assets/stylesheets/framework/top_bar.scss
@@ -0,0 +1,18 @@
+.top-bar-container {
+ min-height: $top-bar-height;
+}
+
+.top-bar-fixed {
+ @include gl-inset-border-b-1-gray-100;
+ background-color: $body-bg;
+ left: var(--application-bar-left);
+ position: fixed;
+ right: var(--application-bar-right);
+ top: $calc-application-bars-height;
+ width: auto;
+ z-index: $top-bar-z-index;
+
+ @media (prefers-reduced-motion: no-preference) {
+ transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium;
+ }
+}
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 6d3811514d9..94e114e7da8 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -16,6 +16,7 @@ class UploadsController < ApplicationController
"projects/topic" => Projects::Topic,
'alert_management_metric_image' => ::AlertManagement::MetricImage,
"achievements/achievement" => Achievements::Achievement,
+ "organizations/organization_detail" => Organizations::OrganizationDetail,
"abuse_report" => AbuseReport,
nil => PersonalSnippet
}.freeze
@@ -65,6 +66,8 @@ class UploadsController < ApplicationController
can?(current_user, :read_alert_management_metric_image, model.alert)
when ::Achievements::Achievement
true
+ when Organizations::OrganizationDetail
+ can?(current_user, :read_organization, model.organization)
else
can?(current_user, "read_#{model.class.underscore}".to_sym, model)
end
@@ -96,7 +99,7 @@ class UploadsController < ApplicationController
def cache_settings
case model
- when User, Appearance, Projects::Topic, Achievements::Achievement
+ when User, Appearance, Projects::Topic, Achievements::Achievement, Organizations::OrganizationDetail
[5.minutes, { public: true, must_revalidate: false }]
when Project, Group
[5.minutes, { private: true, must_revalidate: true }]
diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb
index d0dd9dc5aea..b6d39276a03 100644
--- a/app/helpers/organizations/organization_helper.rb
+++ b/app/helpers/organizations/organization_helper.rb
@@ -4,7 +4,7 @@ module Organizations
module OrganizationHelper
def organization_show_app_data(organization)
{
- organization: organization.slice(:id, :name),
+ organization: organization.slice(:id, :name).merge({ avatar_url: organization.avatar_url(size: 128) }),
groups_and_projects_organization_path: groups_and_projects_organization_path(organization),
# TODO: Update counts to use real data
# https://gitlab.com/gitlab-org/gitlab/-/issues/424531
@@ -25,7 +25,7 @@ module Organizations
def organization_settings_general_app_data(organization)
{
- organization: organization.slice(:id, :name, :path),
+ organization: organization.slice(:id, :name, :path).merge({ avatar: organization.avatar_url(size: 192) }),
organizations_path: organizations_path,
root_url: root_url
}.to_json
diff --git a/app/models/container_registry/protection/rule.rb b/app/models/container_registry/protection/rule.rb
index a7324b3b3b8..34d00bdef2f 100644
--- a/app/models/container_registry/protection/rule.rb
+++ b/app/models/container_registry/protection/rule.rb
@@ -19,6 +19,23 @@ module ContainerRegistry
validates :repository_path_pattern, presence: true, uniqueness: { scope: :project_id }, length: { maximum: 255 }
validates :delete_protected_up_to_access_level, presence: true
validates :push_protected_up_to_access_level, presence: true
+
+ scope :for_repository_path, ->(repository_path) do
+ return none if repository_path.blank?
+
+ where(
+ ":repository_path ILIKE #{::Gitlab::SQL::Glob.to_like('repository_path_pattern')}",
+ repository_path: repository_path
+ )
+ end
+
+ def self.for_push_exists?(access_level:, repository_path:)
+ return false if access_level.blank? || repository_path.blank?
+
+ where(push_protected_up_to_access_level: access_level..)
+ .for_repository_path(repository_path)
+ .exists?
+ end
end
end
end
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 764378a5d19..dad57dca9e6 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -28,7 +28,7 @@ module Organizations
'organizations/path': true,
length: { minimum: 2, maximum: 255 }
- delegate :description, :avatar, :avatar_url, to: :organization_detail
+ delegate :description, :avatar, :avatar_url, :remove_avatar!, to: :organization_detail
accepts_nested_attributes_for :organization_detail
diff --git a/app/services/organizations/update_service.rb b/app/services/organizations/update_service.rb
index bc3a2d29abf..6e3a2cddddb 100644
--- a/app/services/organizations/update_service.rb
+++ b/app/services/organizations/update_service.rb
@@ -17,6 +17,10 @@ module Organizations
def execute
return error_no_permissions unless allowed?
+ if params[:organization_detail_attributes].key?(:avatar) && params[:organization_detail_attributes][:avatar].nil?
+ organization.remove_avatar!
+ end
+
if organization.update(params)
ServiceResponse.success(payload: { organization: organization })
else
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 4f3ca9fd71b..1b0bd10db77 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -104,7 +104,10 @@
- if todos_filter_empty?
%p
- = (s_("Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item.") % { strongStart: '<strong>', strongEnd: '</strong>', openIssuesLinkStart: "<a href=\"#{issues_dashboard_path}\">", openIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path}\">", mergeRequestLinkEnd: '</a>' }).html_safe
+ = (s_("Todos|Not sure where to go next? Take a look at your %{strongStart}%{assignedIssuesLinkStart}assigned issues%{assignedIssuesLinkEnd}%{strongEnd} or %{strongStart}%{mergeRequestLinkStart}merge requests%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}.") % { strongStart: '<strong>', strongEnd: '</strong>', assignedIssuesLinkStart: "<a href=\"#{issues_dashboard_path(assignee_username: current_user.username)}\">", assignedIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path(assignee_username: current_user.username)}\">", mergeRequestLinkEnd: '</a>' }).html_safe
+ %p
+ = link_to s_("Todos| What actions create to-do items?"), help_page_path('user/todos', anchor: 'actions-that-create-to-do-items'), target: '_blank', rel: 'noopener noreferrer'
+
- elsif todos_has_filtered_results?
%p
= link_to s_("Todos|Do you want to remove the filters?"), todos_filter_path(without: [:project_id, :author_id, :type, :action_id])
diff --git a/config/feature_flags/development/ci_job_token_scope.yml b/config/feature_flags/development/ci_job_token_scope.yml
deleted file mode 100644
index aa64f5ba872..00000000000
--- a/config/feature_flags/development/ci_job_token_scope.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_job_token_scope
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300821
-milestone: '13.12'
-type: development
-group: group::container registry
-default_enabled: false
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index f45139b96b8..1ad973c76d0 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -4,7 +4,10 @@ scope path: :uploads do
# Note attachments and User/Group/Project/Topic avatars
get "-/system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
- constraints: { model: %r{note|user|group|project|projects\/topic|achievements\/achievement}, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
+ constraints: {
+ model: %r{note|user|group|project|projects\/topic|achievements\/achievement|organizations\/organization_detail},
+ mounted_as: /avatar|attachment/, filename: %r{[^/]+}
+ }
# show uploads for models, snippets (notes) available for now
get '-/system/:model/:id/:secret/:filename',
diff --git a/doc/administration/settings/jira_cloud_app_troubleshooting.md b/doc/administration/settings/jira_cloud_app_troubleshooting.md
index 7bbf232d412..a3f0289bbf7 100644
--- a/doc/administration/settings/jira_cloud_app_troubleshooting.md
+++ b/doc/administration/settings/jira_cloud_app_troubleshooting.md
@@ -10,7 +10,7 @@ When administering the GitLab for Jira Cloud app for self-managed instances, you
For GitLab.com, see [GitLab for Jira Cloud app](../../integration/jira/connect-app.md#troubleshooting).
-## Browser displays a sign-in message when already signed in
+## Sign-in message displayed when already signed in
You might get the following message prompting you to sign in to GitLab.com
when you're already signed in:
@@ -26,7 +26,8 @@ To resolve this issue, set up [OAuth authentication](jira_cloud_app.md#set-up-oa
## Manual installation fails
-You might see one of the following errors if you have installed the GitLab for Jira Cloud app from the official marketplace listing and replaced it with [manual installation](jira_cloud_app.md#install-the-gitlab-for-jira-cloud-app-manually):
+You might get one of the following errors if you've installed the GitLab for Jira Cloud app
+from the official marketplace listing and replaced it with [manual installation](jira_cloud_app.md#install-the-gitlab-for-jira-cloud-app-manually):
```plaintext
The app "gitlab-jira-connect-gitlab.com" could not be installed as a local app as it has previously been installed from Atlassian Marketplace
@@ -51,7 +52,7 @@ To resolve this issue, disable the **Jira Connect Proxy URL** setting.
1. Clear the **Jira Connect Proxy URL** text box.
1. Select **Save changes**.
-## Data sync fails with `Invalid JWT` error
+## Data sync fails with `Invalid JWT`
If the GitLab for Jira Cloud app continuously fails to sync data, it may be due to an outdated secret token. Atlassian can send new secret tokens that must be processed and stored by GitLab.
If GitLab fails to store the token or misses the new token request, an `Invalid JWT` error occurs.
@@ -111,7 +112,8 @@ tools while reproducing the `Failed to update the GitLab instance` error to see
You should see a `GET` request to `https://gitlab.com/-/jira_connect/installations`.
-This request should return a `200` status code, but it can return a `422` status code if there was a problem. The response body can be checked for the error.
+This request should return a `200 OK`, but it might return a `422 Unprocessable Entity` if there was a problem.
+You can check the response body for the error.
If you cannot resolve the problem and you are a GitLab customer, contact [GitLab Support](https://about.gitlab.com/support/) for assistance. Provide
GitLab Support with:
@@ -123,7 +125,7 @@ GitLab Support with:
The GitLab Support team can then look up why this is failing in the GitLab.com server logs.
-#### Process for GitLab Support
+#### GitLab Support
NOTE:
These steps can only be completed by GitLab Support.
@@ -163,6 +165,6 @@ When you check the browser console, you might see the following message:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://gitlab.example.com/-/jira_connect/oauth_application_id. (Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 403.
```
-`403` status code is returned if the user information cannot be fetched from Jira because of insufficient permissions.
+A `403 Forbidden` is returned if the user information cannot be fetched from Jira because of insufficient permissions.
To resolve this issue, ensure that the Jira user that installs and configures the GitLab for Jira Cloud app meets certain [requirements](jira_cloud_app.md#jira-user-requirements).
diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md
index ab8be6abab7..eef90fc512e 100644
--- a/doc/api/container_registry.md
+++ b/doc/api/container_registry.md
@@ -6,28 +6,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container registry API **(FREE ALL)**
-> The use of `CI_JOB_TOKEN` scoped to the current project was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750) in GitLab 13.12.
+> - The ability to authenticate with a CI/CD job token [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750) in GitLab 13.12 [with a flag](../administration/feature_flags.md) named `ci_job_token_scope`. Disabled by default.
+> - CI/CD job token authentication [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/300821) in GitLab 16.8. Feature flag `ci_job_token_scope` removed.
-This API documentation is about the [GitLab container registry](../user/packages/container_registry/index.md).
+Use these API endpoints to work with the [GitLab container registry](../user/packages/container_registry/index.md).
-When the `ci_job_token_scope` feature flag is enabled (it is **disabled by default**), you can use the below endpoints
-from a CI/CD job, by passing the `$CI_JOB_TOKEN` variable as the `JOB-TOKEN` header.
-The job token only has access to its own project.
-
-[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
-can opt to enable it.
-
-To enable it:
-
-```ruby
-Feature.enable(:ci_job_token_scope)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:ci_job_token_scope)
-```
+You can authenticate with these endpoints from a CI/CD job by passing the [`$CI_JOB_TOKEN`](../ci/jobs/ci_job_token.md)
+variable as the `JOB-TOKEN` header. The job token only has access to the container registry
+of the project that created the pipeline.
## Change the visibility of the container registry
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index ba2d63c12e4..3a787189ac0 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -17,7 +17,7 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints
- [Container registry](../../user/packages/container_registry/build_and_push_images.md#use-gitlab-cicd)
(the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`).
- [Container registry API](../../api/container_registry.md)
- (scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled).
+ (scoped to the job's project).
- [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts).
- [Get job token's job](../../api/jobs.md#get-job-tokens-job).
- [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter
diff --git a/doc/development/code_owners/index.md b/doc/development/code_owners/index.md
index 45c632d5adc..d15e5cf0ea2 100644
--- a/doc/development/code_owners/index.md
+++ b/doc/development/code_owners/index.md
@@ -58,7 +58,7 @@ namespace. Code Owners is an EE-only feature, so the files only exist in the `./
### `ProtectedBranch`
The `ProtectedBranch` model is defined in `app/models/protected_branch.rb` and
-extended in `ee/app/ee/models/protected_branch.rb`. The EE version includes a column
+extended in `ee/app/models/concerns/ee/protected_branch.rb`. The EE version includes a column
named `require_code_owner_approval` which prevents changes from being pushed directly
to the branch being protected if the file is listed in `CODEOWNERS`.
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 26660d2eba1..098538e580d 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -1237,8 +1237,7 @@ and annoying for users.
If you're describing a complicated interaction in the user interface and want to
include a visual representation to help readers understand it, you can:
-- Use a static image (screenshot) and if necessary, add callouts to emphasize an
- an area of the screen.
+- Use a static image (screenshot) and if necessary, add callouts to emphasize an area of the screen.
- Create a short video of the interaction and link to it.
### Automatic screenshot generator
diff --git a/doc/integration/jira/dvcs/troubleshooting.md b/doc/integration/jira/dvcs/troubleshooting.md
index ee697f1bffd..6de54136f3e 100644
--- a/doc/integration/jira/dvcs/troubleshooting.md
+++ b/doc/integration/jira/dvcs/troubleshooting.md
@@ -6,9 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Troubleshooting Jira DVCS connector **(FREE ALL)**
-Refer to the items in this section if you're having problems with your Jira DVCS connector.
+When working with the [Jira DVCS connector](index.md), you might encounter the following issues.
-## Jira cannot access GitLab server
+## Jira cannot access the GitLab server
If you complete the **Add New Account** form, authorize access, and you receive
this error, Jira and GitLab cannot connect. No other error messages
@@ -68,7 +68,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 to Jira using DVCS
+## Scope error when connecting to Jira with DVCS
```plaintext
The requested scope is invalid, unknown, or malformed.
@@ -83,7 +83,7 @@ Potential resolutions:
[GitLab account configuration](index.md#create-a-gitlab-application-for-dvcs). Review
the **Scopes** field and ensure the `api` checkbox is selected.
-## Jira error adding account and no repositories listed
+## Error when adding an account in Jira
After you complete the **Add New Account** form in Jira and authorize access, you might
encounter these issues:
@@ -100,13 +100,13 @@ To resolve this issue:
[Contact GitLab Support](https://about.gitlab.com/support/) if none of these reasons apply.
-## `410 : Gone` error when connecting to Jira
+## `410 Gone` when connecting to Jira
-When you connect to Jira and synchronize repositories, you may receive a `410 : Gone` error.
+When you connect to Jira and synchronize repositories, you might get a `410 Gone` error.
This issue occurs when you use the Jira DVCS connector and your integration is configured to use **GitHub Enterprise**.
-For more information and possible fixes, see [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340160).
+For more information, see [issue 340160](https://gitlab.com/gitlab-org/gitlab/-/issues/340160).
## Synchronization issues
@@ -123,7 +123,7 @@ resynchronize the information:
For more information, see the
[Atlassian documentation](https://support.atlassian.com/jira-cloud-administration/docs/integrate-with-development-tools/).
-## `Sync Failed` error when refreshing repository data
+## `Sync Failed` when refreshing repository data
If you get a `Sync Failed` error in Jira when [refreshing repository data](index.md#refresh-data-imported-to-jira) for specific projects, check your Jira DVCS connector logs. Look for errors that occur when executing requests to API resources in GitLab. For example:
@@ -132,8 +132,8 @@ Failed to execute request [https://gitlab.com/api/v4/projects/:id/merge_requests
{"message":"403 Forbidden"}
```
-If you find a `{"message":"403 Forbidden"}` error, it is possible that this specific project has some [GitLab features disabled](../../../user/project/settings/project_features_permissions.md#configure-project-features-and-permissions).
-In the example above, the merge requests feature is disabled.
+If you get a `403 Forbidden` error, this project might have some [GitLab features disabled](../../../user/project/settings/project_features_permissions.md#configure-project-features-and-permissions).
+In the previous example, the merge requests feature is disabled.
To resolve the issue, enable the relevant feature:
diff --git a/doc/integration/jira/troubleshooting.md b/doc/integration/jira/troubleshooting.md
index 6c8b49b4159..14d8c65bcda 100644
--- a/doc/integration/jira/troubleshooting.md
+++ b/doc/integration/jira/troubleshooting.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Troubleshooting Jira issue integration **(FREE ALL)**
-This page contains a list of common issues you might encounter when working with the [Jira issue integration](configure.md).
+When working with the [Jira issue integration](configure.md), you might encounter the following issues.
## GitLab cannot link to a Jira issue
@@ -122,7 +122,7 @@ To resolve this issue, see
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
-### Change all projects on the instance
+### Change all projects on an instance
To change all Jira projects to use instance-level integration settings:
@@ -189,7 +189,7 @@ To change all Jira projects in a group (and its subgroups) to use group-level in
end
```
-## Update the Jira issue integration password for all projects
+## Update the integration password for all projects
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
diff --git a/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md b/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md
index 363e7c2c364..a44c5a029df 100644
--- a/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md
+++ b/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md
@@ -10,13 +10,13 @@ When configuring the GitLab for Slack app on GitLab.com, you might encounter the
For self-managed GitLab, see [GitLab for Slack app administration](../../../administration/settings/slack_app.md#troubleshooting).
-## The app does not appear in the list of integrations
+## App does not appear in the list of integrations
The GitLab for Slack app might not appear in the list of integrations. To have the GitLab for Slack app on your self-managed instance, an administrator must [enable the integration](../../../administration/settings/slack_app.md). On GitLab.com, the GitLab for Slack app is available by default.
The GitLab for Slack app is enabled at the project level only. Support for the app at the group and instance levels is proposed in [issue 391526](https://gitlab.com/gitlab-org/gitlab/-/issues/391526).
-## Project or alias not found
+## `Project or alias not found`
Some Slack commands must have a project full path or alias and fail with the following error
if the project cannot be found:
@@ -36,13 +36,13 @@ To resolve this issue, ensure:
Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack.
To resolve this issue, ensure an administrator has properly configured the [GitLab for Slack app settings](../../../administration/settings/slack_app.md) on your self-managed instance.
-## Notifications are not received to a channel
+## Notifications not received to a channel
If you're not receiving notifications to a Slack channel, ensure:
- The channel name you configured is correct.
- If the channel is private, you've [added the GitLab for Slack app to the channel](gitlab_slack_application.md#receive-notifications-to-a-private-channel).
-## The App Home does not display properly
+## App Home does not display properly
If the [App Home](https://api.slack.com/start/overview#app_home) does not display properly, ensure your [app is up to date](gitlab_slack_application.md#update-the-gitlab-for-slack-app).
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index 3881220ec7a..22881a14704 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -101,6 +101,15 @@ NOTE:
The Max role does not elevate the privileges of users.
For example, if a group member has the role of Developer, and the group is invited to a project with a Max role of Maintainer, the member's role is not elevated to Maintainer.
+### Which roles you can assign
+
+In GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/233408) and later, the maximum role you can assign depends on whether you have the Owner or Maintainer role for the project. The maximum role you can set is:
+
+- Owner (`50`), if you have the Owner role for the project.
+- Maintainer (`40`), if you have the Maintainer role for the project.
+
+In GitLab 16.6 and earlier, the maximum role you can assign to an invited group is Maintainer (`40`).
+
### View the member's Max role
To view the maximum role assigned to a member:
diff --git a/doc/user/public_access.md b/doc/user/public_access.md
index b7ee354ed9a..826f9548982 100644
--- a/doc/user/public_access.md
+++ b/doc/user/public_access.md
@@ -8,11 +8,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Projects and groups in GitLab can be private, internal, or public.
-The visibility level of the group or project has no influence on whether members within the group or project can see each other.
-A group or project is an object to allow collaborative work. This is only possible if all members know about each other.
+The visibility level of the project or group does not affect whether members of the project or group can see each other.
+Projects and groups are intended for collaborative work. This work is only possible if all members know about each other.
-Group or project members can see all members of the group or project they belong to.
-Group or project owners can see the origin of membership (the original group or project) of all members.
+Project or group members can see all members of the project or group they belong to.
+Project or group members can see the origin of membership (the original project or group) of all members for the projects and groups they have access to.
## Private projects and groups
@@ -38,15 +38,9 @@ Only internal members can view internal content.
Internal groups can have internal or private subgroups.
-NOTE:
-From July 2019, the `Internal` visibility setting is disabled for new projects, groups,
-and snippets on GitLab.com. Existing projects, groups, and snippets using the `Internal`
-visibility setting keep this setting. For more information, see
-[issue 12388](https://gitlab.com/gitlab-org/gitlab/-/issues/12388).
-
## Public projects and groups
-For public projects, **users who are not authenticated**, including users with the Guest role, can:
+For public projects, **unauthenticated users**, including users with the Guest role, can:
- Clone the project.
- View the public access directory (`/public`).
@@ -56,7 +50,7 @@ Public groups can have public, internal, or private subgroups.
NOTE:
If an administrator restricts the
[**Public** visibility level](../administration/settings/visibility_and_access_controls.md#restrict-visibility-levels),
-then `/public` is visible only to authenticated users.
+then the public access directory (`/public`) is visible only to authenticated users.
## Change project visibility
@@ -85,7 +79,7 @@ Prerequisites:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Settings > General**.
1. Expand **Visibility, project features, permissions**.
-1. To enable or disable a feature, turn on or off the feature toggle.
+1. To enable or disable a feature, turn on or turn off the feature toggle.
1. Select **Save changes**.
## Change group visibility
@@ -95,9 +89,9 @@ You can change the visibility of all projects in a group.
Prerequisites:
- You must have the Owner role for a group.
-- Subgroups and projects must already have visibility settings that are at least as
+- Projects and subgroups must already have visibility settings that are at least as
restrictive as the new setting of the parent group. For example, you cannot set a group
- to private if a subgroup or project in that group is public.
+ to private if a project or subgroup in that group is public.
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > General**.
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index f5dcbc07704..6cb9d19a2ad 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -184,8 +184,7 @@ module API
return true unless job_token_authentication?
return true unless route_authentication_setting[:job_token_scope] == :project
- ::Feature.enabled?(:ci_job_token_scope, project) &&
- current_authenticated_job.project == project
+ current_authenticated_job.project == project
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/integrations/google_cloud_platform/base_client.rb b/lib/integrations/google_cloud_platform/base_client.rb
index 56c05e7987b..937454cda43 100644
--- a/lib/integrations/google_cloud_platform/base_client.rb
+++ b/lib/integrations/google_cloud_platform/base_client.rb
@@ -6,7 +6,7 @@ module Integrations
GLGO_BASE_URL = if Gitlab.staging?
'https://glgo.staging.runway.gitlab.net'
else
- 'http://glgo.runway.gitlab.net/'
+ 'https://glgo.runway.gitlab.net'
end
def initialize(project:, user:)
diff --git a/lib/sidebars/organizations/menus/scope_menu.rb b/lib/sidebars/organizations/menus/scope_menu.rb
index a535be21280..559e57bc171 100644
--- a/lib/sidebars/organizations/menus/scope_menu.rb
+++ b/lib/sidebars/organizations/menus/scope_menu.rb
@@ -27,7 +27,7 @@ module Sidebars
override :serialize_as_menu_item_args
def serialize_as_menu_item_args
super.merge({
- avatar: nil,
+ avatar: context.container.avatar_url(size: 48),
entity_id: context.container.id,
super_sidebar_parent: ::Sidebars::StaticMenu,
item_id: :organization_overview
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1924835a1e7..c6fab517db5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18066,6 +18066,9 @@ msgstr ""
msgid "Drag your designs here or %{linkStart}click to upload%{linkEnd}."
msgstr ""
+msgid "Drop or %{linkStart}upload%{linkEnd} an avatar."
+msgstr ""
+
msgid "Drop or %{linkStart}upload%{linkEnd} file to attach"
msgstr ""
@@ -33869,6 +33872,9 @@ msgstr ""
msgid "Organization|Organization URL successfully changed."
msgstr ""
+msgid "Organization|Organization avatar"
+msgstr ""
+
msgid "Organization|Organization name"
msgstr ""
@@ -50808,6 +50814,9 @@ msgstr ""
msgid "Today"
msgstr ""
+msgid "Todos| What actions create to-do items?"
+msgstr ""
+
msgid "Todos|Added"
msgstr ""
@@ -50820,9 +50829,6 @@ msgstr ""
msgid "Todos|Any Type"
msgstr ""
-msgid "Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item."
-msgstr ""
-
msgid "Todos|Assigned"
msgstr ""
@@ -50880,6 +50886,9 @@ msgstr ""
msgid "Todos|Merge request"
msgstr ""
+msgid "Todos|Not sure where to go next? Take a look at your %{strongStart}%{assignedIssuesLinkStart}assigned issues%{assignedIssuesLinkEnd}%{strongEnd} or %{strongStart}%{mergeRequestLinkStart}merge requests%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}."
+msgstr ""
+
msgid "Todos|Nothing is on your to-do list. Nice work!"
msgstr ""
diff --git a/qa/qa/specs/helpers/fast_quarantine.rb b/qa/qa/specs/helpers/fast_quarantine.rb
new file mode 100644
index 00000000000..5b72c82fa35
--- /dev/null
+++ b/qa/qa/specs/helpers/fast_quarantine.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module QA
+ module Specs
+ module Helpers
+ class FastQuarantine
+ include Support::API
+
+ class << self
+ def configure!
+ return unless ENV["CI"]
+ return if ENV["FAST_QUARANTINE"] == "false"
+ return if ENV["CI_MERGE_REQUEST_LABELS"]&.include?("pipeline:run-flaky-tests")
+
+ Runtime::Logger.debug("Running fast quarantine setup")
+ setup = new
+ setup.fetch_fq_file
+ setup.configure_rspec
+ rescue StandardError => e
+ Runtime::Logger.error("Failed to setup FastQuarantine, error: '#{e.class} - #{e.message}'")
+ end
+ end
+
+ private_class_method :new
+
+ def initialize
+ @logger = Runtime::Logger.logger
+ @fq_filename = "fast_quarantine-gitlab.txt"
+ end
+
+ # Fetch and save fast quarantine file
+ #
+ # @return [void]
+ def fetch_fq_file
+ download_fast_quarantine
+ end
+
+ # Configure rspec
+ #
+ # @return [void]
+ def configure_rspec
+ # Shared tooling that adds relevant rspec configuration
+ require_relative '../../../../spec/support/fast_quarantine'
+ end
+
+ private
+
+ attr_reader :logger, :fq_filename
+
+ # Force path to be relative to ruby process in order to avoid issues when dealing with different execution
+ # contexts of qa docker container and CI runner environment
+ def fq_path
+ @fq_path ||= ENV["RSPEC_FAST_QUARANTINE_PATH"] = File.join(Runtime::Path.qa_root, "tmp", fq_filename)
+ end
+
+ def download_fast_quarantine
+ logger.debug(" downloading fast quarantine file")
+ response = get(
+ "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/rspec/#{fq_filename}",
+ verify_ssl: true
+ )
+ raise "Failed to download fast quarantine file: #{response.code}" if response.code != HTTP_STATUS_OK
+
+ logger.debug(" saving fast quarantine file to '#{fq_path}'")
+ File.write(fq_path, response.body)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/spec_helper.rb b/qa/qa/specs/spec_helper.rb
index d4295ce72e7..e585b660221 100644
--- a/qa/qa/specs/spec_helper.rb
+++ b/qa/qa/specs/spec_helper.rb
@@ -5,9 +5,6 @@ require 'factory_bot'
require_relative '../../qa'
-# Require shared test tooling from Rails test suite
-require_relative '../../../spec/support/fast_quarantine'
-
QA::Specs::QaDeprecationToolkitEnv.configure!
Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
@@ -16,10 +13,12 @@ Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
QA::Support::GitlabAddress.define_gitlab_address_attribute!
QA::Runtime::Browser.configure!
QA::Specs::Helpers::FeatureSetup.configure!
+QA::Specs::Helpers::FastQuarantine.configure!
QA::Runtime::AllureReport.configure!
-QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)
QA::Service::DockerRun::Video.configure!
+QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)
+
# Enable zero monkey patching mode before loading any other RSpec code.
RSpec.configure(&:disable_monkey_patching!)
diff --git a/qa/spec/README.md b/qa/spec/README.md
index b1fc38fb55d..0831ef4f052 100644
--- a/qa/spec/README.md
+++ b/qa/spec/README.md
@@ -1,7 +1,13 @@
# QA framework unit tests
-To run framework unit tests, following command can be used:
+To run all the unit tests under the framework, following command can be used:
```shell
bundle exec rspec -O .rspec_internal
```
+
+To run individual unit test, following command can be used:
+
+```shell
+bundle exec rspec -O .rspec_internal spec/spec_path/file_spec.rb
+```
diff --git a/qa/spec/specs/helpers/fast_quarantine_spec.rb b/qa/spec/specs/helpers/fast_quarantine_spec.rb
new file mode 100644
index 00000000000..d63e5ef822e
--- /dev/null
+++ b/qa/spec/specs/helpers/fast_quarantine_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.describe QA::Specs::Helpers::FastQuarantine do
+ include QA::Support::Helpers::StubEnv
+
+ let(:response) { instance_double(RestClient::Response, code: 200, body: fq_contents) }
+ let(:fq_path) { File.join(QA::Runtime::Path.qa_root, "tmp", "fast_quarantine-gitlab.txt") }
+ let(:fq_contents) { "fast_quarantine_contents" }
+
+ before do
+ stub_env("CI", "true")
+
+ allow(RSpec).to receive(:configure)
+ allow(File).to receive(:write).with(fq_path, fq_contents)
+ allow(RestClient::Request).to receive(:execute).and_return(response)
+
+ # silence log messages during test execution
+ allow(QA::Runtime::Logger).to receive(:logger).and_return(instance_double(ActiveSupport::Logger, debug: nil))
+ allow(QA::Runtime::Logger).to receive(:debug)
+
+ described_class.configure!
+ end
+
+ it "configures fast quarantine" do
+ expect(RSpec).to have_received(:configure)
+ expect(File).to have_received(:write).with(fq_path, fq_contents)
+ expect(RestClient::Request).to have_received(:execute).with(
+ cookies: {},
+ method: :get,
+ url: "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/rspec/fast_quarantine-gitlab.txt",
+ verify_ssl: true
+ )
+ end
+end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 8ae78c5ee35..9c246c21104 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -776,6 +776,45 @@ RSpec.describe UploadsController, feature_category: :groups_and_projects do
end
end
end
+
+ context 'when viewing an organization avatar' do
+ let(:organization_detail) { create(:organization_detail) }
+ let(:organization) { organization_detail.organization }
+
+ subject(:request) do
+ get(
+ :show,
+ params: {
+ model: 'organizations/organization_detail',
+ mounted_as: 'avatar',
+ id: organization.id,
+ filename: 'dk.png'
+ }
+ )
+ end
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'responds with status 200' do
+ request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'content publicly cached'
+ end
+
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ request
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'content publicly cached'
+ end
+ end
end
def post_authorize(verified: true)
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 21dfa1cbc0b..88b3635153c 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -33,11 +33,11 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do
sign_in(user)
end
- it 'shows "Are you looking for things to do?" message' do
+ it 'shows "Not sure where to go next?" message' do
create(:todo, :assigned, :done, user: user, project: project, target: issue, author: user2)
visit dashboard_todos_path
- expect(page).to have_content 'Are you looking for things to do? Take a look at open issues, contribute to a merge request, or mention someone in a comment to automatically assign them a new to-do item.'
+ expect(page).to have_content 'Not sure where to go next? Take a look at your assigned issues or merge requests.'
end
end
end
diff --git a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
index d1c637331a8..eca6d9fdc4a 100644
--- a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
+++ b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
@@ -5,7 +5,11 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
-import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
+import {
+ FORM_FIELD_NAME,
+ FORM_FIELD_ID,
+ FORM_FIELD_AVATAR,
+} from '~/organizations/shared/constants';
import organizationUpdateMutation from '~/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql';
import {
organizationUpdateResponse,
@@ -38,22 +42,27 @@ describe('OrganizationSettings', () => {
},
};
+ const file = new File(['foo'], 'foo.jpg', {
+ type: 'text/plain',
+ });
+
const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUpdateResponse);
const createComponent = ({
handlers = [[organizationUpdateMutation, successfulResponseHandler]],
+ provide = {},
} = {}) => {
mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(OrganizationSettings, {
- provide: defaultProvide,
+ provide: { ...defaultProvide, ...provide },
apolloProvider: mockApollo,
});
};
const findForm = () => wrapper.findComponent(NewEditForm);
- const submitForm = async () => {
- findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' });
+ const submitForm = async (data = {}) => {
+ findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar', avatar: file, ...data });
await nextTick();
};
@@ -75,7 +84,7 @@ describe('OrganizationSettings', () => {
expect(findForm().props()).toMatchObject({
loading: false,
initialFormValues: defaultProvide.organization,
- fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR],
});
});
@@ -108,6 +117,7 @@ describe('OrganizationSettings', () => {
input: {
id: 'gid://gitlab/Organizations::Organization/1',
name: 'Foo bar',
+ avatar: file,
},
});
expect(visitUrlWithAlerts).toHaveBeenCalledWith(window.location.href, [
@@ -162,5 +172,46 @@ describe('OrganizationSettings', () => {
});
});
});
+
+ describe('when organization has avatar', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: { organization: { ...defaultProvide.organization, avatar: 'avatar.jpg' } },
+ });
+ });
+
+ describe('when avatar is explicitly removed', () => {
+ beforeEach(async () => {
+ await submitForm({ avatar: null });
+ await waitForPromises();
+ });
+
+ it('sets `avatar` argument to `null`', () => {
+ expect(successfulResponseHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ name: 'Foo bar',
+ avatar: null,
+ },
+ });
+ });
+ });
+
+ describe('when avatar is not changed', () => {
+ beforeEach(async () => {
+ await submitForm({ avatar: 'avatar.jpg' });
+ await waitForPromises();
+ });
+
+ it('does not pass `avatar` argument', () => {
+ expect(successfulResponseHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ name: 'Foo bar',
+ },
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
index 1fcfc20bf1a..4897a81fc1c 100644
--- a/spec/frontend/organizations/shared/components/new_edit_form_spec.js
+++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
@@ -3,7 +3,13 @@ import { nextTick } from 'vue';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
import OrganizationUrlField from '~/organizations/shared/components/organization_url_field.vue';
-import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '~/organizations/shared/constants';
+import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue';
+import {
+ FORM_FIELD_NAME,
+ FORM_FIELD_ID,
+ FORM_FIELD_PATH,
+ FORM_FIELD_AVATAR,
+} from '~/organizations/shared/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
describe('NewEditForm', () => {
@@ -32,6 +38,7 @@ describe('NewEditForm', () => {
const findNameField = () => wrapper.findByLabelText('Organization name');
const findIdField = () => wrapper.findByLabelText('Organization ID');
const findUrlField = () => wrapper.findComponent(OrganizationUrlField);
+ const findAvatarField = () => wrapper.findComponent(AvatarUploadDropzone);
const setUrlFieldValue = async (value) => {
findUrlField().vm.$emit('input', value);
@@ -53,6 +60,32 @@ describe('NewEditForm', () => {
expect(findUrlField().exists()).toBe(true);
});
+ it('renders `Organization avatar` field', () => {
+ createComponent();
+
+ expect(findAvatarField().props()).toMatchObject({
+ value: null,
+ entity: { [FORM_FIELD_NAME]: '', [FORM_FIELD_PATH]: '', [FORM_FIELD_AVATAR]: null },
+ label: 'Organization avatar',
+ });
+ });
+
+ describe('when `Organization avatar` field is changed', () => {
+ const file = new File(['foo'], 'foo.jpg', {
+ type: 'text/plain',
+ });
+
+ beforeEach(() => {
+ window.URL.revokeObjectURL = jest.fn();
+ createComponent();
+ findAvatarField().vm.$emit('input', file);
+ });
+
+ it('updates `value` prop', () => {
+ expect(findAvatarField().props('value')).toEqual(file);
+ });
+ });
+
it('requires `Organization URL` field to be a minimum of two characters', async () => {
createComponent();
@@ -125,7 +158,9 @@ describe('NewEditForm', () => {
});
it('emits `submit` event with form values', () => {
- expect(wrapper.emitted('submit')).toEqual([[{ name: 'Foo bar', path: 'foo-bar' }]]);
+ expect(wrapper.emitted('submit')).toEqual([
+ [{ name: 'Foo bar', path: 'foo-bar', avatar: null }],
+ ]);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 1b7338744e8..adbf4e1d371 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -858,6 +858,42 @@ describe('ReadyToMerge', () => {
});
});
+ describe('only allow merge if pipeline succeeds', () => {
+ beforeEach(() => {
+ const response = JSON.parse(JSON.stringify(readyToMergeResponse));
+ response.data.project.onlyAllowMergeIfPipelineSucceeds = true;
+ response.data.project.mergeRequest.headPipeline = {
+ id: 1,
+ active: true,
+ status: '',
+ path: '',
+ };
+
+ readyToMergeResponseSpy = jest.fn().mockResolvedValueOnce(response);
+ });
+
+ it('hides merge immediately dropdown when subscription returns', async () => {
+ createComponent({ mr: { id: 1 } });
+
+ await waitForPromises();
+
+ expect(findMergeImmediatelyDropdown().exists()).toBe(false);
+
+ mockedSubscription.next({
+ data: {
+ mergeRequestMergeStatusUpdated: {
+ ...readyToMergeResponse.data.project.mergeRequest,
+ headPipeline: { id: 1, active: true, status: '', path: '' },
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ expect(findMergeImmediatelyDropdown().exists()).toBe(false);
+ });
+ });
+
describe('commit message', () => {
it('updates commit message from subscription', async () => {
createComponent({ mr: { id: 1 } });
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/avatar_upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/avatar_upload_dropzone_spec.js
new file mode 100644
index 00000000000..6313bf588a0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/upload_dropzone/avatar_upload_dropzone_spec.js
@@ -0,0 +1,116 @@
+import { GlAvatar, GlButton, GlTruncate } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+
+describe('AvatarUploadDropzone', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ entity: { id: 1, name: 'Foo' },
+ value: null,
+ label: 'Avatar',
+ };
+
+ const file = new File(['foo'], 'foo.jpg', {
+ type: 'text/plain',
+ });
+ const file2 = new File(['bar'], 'bar.jpg', {
+ type: 'text/plain',
+ });
+ const blob = 'blob:http://127.0.0.1:3000/0046cf8c-ea21-4720-91ef-2e354d570c75';
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(AvatarUploadDropzone, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ window.URL.createObjectURL = jest.fn().mockImplementation(() => blob);
+ window.URL.revokeObjectURL = jest.fn();
+ });
+
+ it('renders `GlAvatar` with correct props', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlAvatar).props()).toMatchObject({
+ entityId: defaultPropsData.entity.id,
+ entityName: defaultPropsData.entity.name,
+ shape: AVATAR_SHAPE_OPTION_RECT,
+ size: 96,
+ src: null,
+ });
+ });
+
+ it('renders label', () => {
+ createComponent();
+
+ expect(wrapper.findByText(defaultPropsData.label).exists()).toBe(true);
+ });
+
+ describe('when `value` prop is updated', () => {
+ beforeEach(() => {
+ createComponent();
+
+ // setProps is justified here because we are testing the component's
+ // reactive behavior which constitutes an exception
+ // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
+ wrapper.setProps({ value: file });
+ });
+
+ it('updates `GlAvatar` `src` prop', () => {
+ expect(wrapper.findComponent(GlAvatar).props('src')).toBe(blob);
+ });
+
+ it('renders remove button', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+
+ it('renders truncated file name', () => {
+ expect(wrapper.findComponent(GlTruncate).props('text')).toBe('foo.jpg');
+ });
+
+ it('does not render upload dropzone', () => {
+ expect(findUploadDropzone().exists()).toBe(false);
+ });
+
+ describe('when `value` prop is updated a second time', () => {
+ beforeEach(() => {
+ wrapper.setProps({ value: file2 });
+ });
+
+ it('revokes the object URL of the previous avatar', () => {
+ expect(window.URL.revokeObjectURL).toHaveBeenCalledWith(blob);
+ });
+ });
+
+ describe('when avatar is removed', () => {
+ beforeEach(() => {
+ findButton().vm.$emit('click');
+ });
+
+ it('emits `input` event with `null` payload', () => {
+ expect(wrapper.emitted('input')).toEqual([[null]]);
+ });
+ });
+ });
+
+ describe('when `UploadDropzone` emits `change` event', () => {
+ beforeEach(() => {
+ createComponent();
+ findUploadDropzone().vm.$emit('change', file);
+ });
+
+ it('emits `input` event', () => {
+ expect(wrapper.emitted('input')).toEqual([[file]]);
+ });
+ });
+});
diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb
index 594013d515b..a3613e29da9 100644
--- a/spec/helpers/organizations/organization_helper_spec.rb
+++ b/spec/helpers/organizations/organization_helper_spec.rb
@@ -31,13 +31,18 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
end
it 'returns expected json' do
+ expect(organization).to receive(:avatar_url).with(size: 128).and_return('avatar.jpg')
expect(
Gitlab::Json.parse(
helper.organization_show_app_data(organization)
)
).to eq(
{
- 'organization' => { 'id' => organization.id, 'name' => organization.name },
+ 'organization' => {
+ 'id' => organization.id,
+ 'name' => organization.name,
+ 'avatar_url' => 'avatar.jpg'
+ },
'groups_and_projects_organization_path' => '/-/organizations/default/groups_and_projects',
'new_group_path' => new_group_path,
'new_project_path' => new_project_path,
@@ -107,12 +112,14 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
describe '#organization_settings_general_app_data' do
it 'returns expected json' do
+ expect(organization).to receive(:avatar_url).with(size: 192).and_return('avatar.jpg')
expect(Gitlab::Json.parse(helper.organization_settings_general_app_data(organization))).to eq(
{
'organization' => {
'id' => organization.id,
'name' => organization.name,
- 'path' => organization.path
+ 'path' => organization.path,
+ 'avatar' => 'avatar.jpg'
},
'organizations_path' => organizations_path,
'root_url' => root_url
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 21b3b8e6927..c76694b60d3 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -674,23 +674,15 @@ RSpec.describe API::Helpers, feature_category: :shared do
let(:send_authorized_project_scope) { helper.authorized_project_scope?(project) }
- where(:job_token_authentication, :route_setting, :feature_flag, :same_job_project, :expected_result) do
- false | false | false | false | true
- false | false | false | true | true
- false | false | true | false | true
- false | false | true | true | true
- false | true | false | false | true
- false | true | false | true | true
- false | true | true | false | true
- false | true | true | true | true
- true | false | false | false | true
- true | false | false | true | true
- true | false | true | false | true
- true | false | true | true | true
- true | true | false | false | false
- true | true | false | true | false
- true | true | true | false | false
- true | true | true | true | true
+ where(:job_token_authentication, :route_setting, :same_job_project, :expected_result) do
+ false | false | false | true
+ false | false | true | true
+ false | true | false | true
+ false | true | true | true
+ true | false | false | true
+ true | false | true | true
+ true | true | false | false
+ true | true | true | true
end
with_them do
@@ -699,9 +691,6 @@ RSpec.describe API::Helpers, feature_category: :shared do
allow(helper).to receive(:route_authentication_setting).and_return(job_token_scope: route_setting ? :project : nil)
allow(helper).to receive(:current_authenticated_job).and_return(job)
allow(job).to receive(:project).and_return(same_job_project ? project : other_project)
-
- stub_feature_flags(ci_job_token_scope: false)
- stub_feature_flags(ci_job_token_scope: project) if feature_flag
end
it 'returns the expected result' do
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index 35f2a99ee87..04154b72453 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -23,6 +23,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge, feature_category: :continuous_
end
end
+ describe '.visible?' do
+ it 'always returns true' do
+ expect(described_class.visible?).to be_truthy
+ end
+ end
+
describe '.matching?' do
subject { described_class.matching?(name, config) }
diff --git a/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb b/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb
index 999889a72ee..974531a2dec 100644
--- a/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb
+++ b/spec/lib/sidebars/organizations/menus/scope_menu_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Sidebars::Organizations::Menus::ScopeMenu, feature_category: :navigation do
- let_it_be(:organization) { build(:organization) }
+ let_it_be(:organization_detail) { build(:organization_detail) }
+ let_it_be(:organization) { organization_detail.organization }
let_it_be(:user) { build(:user) }
let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) }
@@ -11,7 +12,7 @@ RSpec.describe Sidebars::Organizations::Menus::ScopeMenu, feature_category: :nav
let(:menu) { described_class.new(context) }
let(:extra_attrs) do
{
- avatar: nil,
+ avatar: organization.avatar_url(size: 48),
entity_id: organization.id,
super_sidebar_parent: ::Sidebars::StaticMenu,
item_id: :organization_overview
diff --git a/spec/models/container_registry/protection/rule_spec.rb b/spec/models/container_registry/protection/rule_spec.rb
index 1706fcf76ae..6c86a889901 100644
--- a/spec/models/container_registry/protection/rule_spec.rb
+++ b/spec/models/container_registry/protection/rule_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_category: :container_registry do
+ using RSpec::Parameterized::TableSyntax
+
it_behaves_like 'having unique enum values'
describe 'relationships' do
@@ -51,4 +53,192 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
it { is_expected.to validate_presence_of(:push_protected_up_to_access_level) }
end
end
+
+ describe '.for_repository_path' do
+ let_it_be(:container_registry_protection_rule) do
+ create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container')
+ end
+
+ let_it_be(:protection_rule_with_wildcard_start) do
+ create(:container_registry_protection_rule, repository_path_pattern: '*my-scope/my_container-with-wildcard-start')
+ end
+
+ let_it_be(:protection_rule_with_wildcard_end) do
+ create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container-with-wildcard-end*')
+ end
+
+ let_it_be(:protection_rule_with_wildcard_middle) do
+ create(:container_registry_protection_rule,
+ repository_path_pattern: 'my-scope/*my_container-with-wildcard-middle')
+ end
+
+ let_it_be(:protection_rule_with_wildcard_double) do
+ create(:container_registry_protection_rule,
+ repository_path_pattern: '**my-scope/**my_container-with-wildcard-double**')
+ end
+
+ let_it_be(:protection_rule_with_underscore) do
+ create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container-with_____underscore')
+ end
+
+ let_it_be(:protection_rule_with_regex_chars) do
+ create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container-with-regex-chars.+')
+ end
+
+ let(:repository_path) { container_registry_protection_rule.repository_path_pattern }
+
+ subject { described_class.for_repository_path(repository_path) }
+
+ context 'with several container registry protection rule scenarios' do
+ where(:repository_path, :expected_container_registry_protection_rules) do
+ 'my-scope/my_container' | [ref(:container_registry_protection_rule)]
+ 'my-scope/my2container' | []
+ 'my-scope/my_container-2' | []
+
+ # With wildcard pattern at the start
+ 'my-scope/my_container-with-wildcard-start' | [ref(:protection_rule_with_wildcard_start)]
+ 'my-scope/my_container-with-wildcard-start-any' | []
+ 'prefix-my-scope/my_container-with-wildcard-start' | [ref(:protection_rule_with_wildcard_start)]
+ 'prefix-my-scope/my_container-with-wildcard-start-any' | []
+
+ # With wildcard pattern at the end
+ 'my-scope/my_container-with-wildcard-end' | [ref(:protection_rule_with_wildcard_end)]
+ 'my-scope/my_container-with-wildcard-end:1234567890' | [ref(:protection_rule_with_wildcard_end)]
+ 'prefix-my-scope/my_container-with-wildcard-end' | []
+ 'prefix-my-scope/my_container-with-wildcard-end:1234567890' | []
+
+ # With wildcard pattern in the middle
+ 'my-scope/my_container-with-wildcard-middle' | [ref(:protection_rule_with_wildcard_middle)]
+ 'my-scope/any-my_container-with-wildcard-middle' | [ref(:protection_rule_with_wildcard_middle)]
+ 'my-scope/any-my_container-my_container-wildcard-middle-any' | []
+
+ # With double wildcard pattern
+ 'my-scope/my_container-with-wildcard-double' | [ref(:protection_rule_with_wildcard_double)]
+ 'prefix-my-scope/any-my_container-with-wildcard-double-any' | [ref(:protection_rule_with_wildcard_double)]
+ '****my-scope/****my_container-with-wildcard-double****' | [ref(:protection_rule_with_wildcard_double)]
+ 'prefix-@other-scope/any-my_container-with-wildcard-double-any' | []
+
+ # With underscore
+ 'my-scope/my_container-with_____underscore' | [ref(:protection_rule_with_underscore)]
+ 'my-scope/my_container-with_any_underscore' | []
+
+ 'my-scope/my_container-with-regex-chars.+' | [ref(:protection_rule_with_regex_chars)]
+ 'my-scope/my_container-with-regex-chars.' | []
+ 'my-scope/my_container-with-regex-chars' | []
+ 'my-scope/my_container-with-regex-chars-any' | []
+
+ # Special cases
+ nil | []
+ '' | []
+ 'any_container' | []
+ end
+
+ with_them do
+ it { is_expected.to match_array(expected_container_registry_protection_rules) }
+ end
+ end
+
+ context 'with multiple matching container registry protection rules' do
+ let!(:container_registry_protection_rule_second_match) do
+ create(:container_registry_protection_rule, repository_path_pattern: "#{repository_path}*")
+ end
+
+ it {
+ is_expected.to contain_exactly(container_registry_protection_rule_second_match,
+ container_registry_protection_rule)
+ }
+ end
+ end
+
+ describe '.for_push_exists?' do
+ subject do
+ project
+ .container_registry_protection_rules
+ .for_push_exists?(
+ access_level: access_level,
+ repository_path: repository_path
+ )
+ end
+
+ context 'when the repository path matches multiple protection rules' do
+ # The abbreviation `crpr` stands for container registry protection rule
+ let_it_be(:project_with_crpr) { create(:project) }
+ let_it_be(:project_without_crpr) { create(:project) }
+
+ let_it_be(:protection_rule_for_developer) do
+ create(:container_registry_protection_rule,
+ repository_path_pattern: 'my-scope/my-container-stage*',
+ project: project_with_crpr,
+ push_protected_up_to_access_level: :developer
+ )
+ end
+
+ let_it_be(:protection_rule_for_maintainer) do
+ create(:container_registry_protection_rule,
+ repository_path_pattern: 'my-scope/my-container-prod*',
+ project: project_with_crpr,
+ push_protected_up_to_access_level: :maintainer
+ )
+ end
+
+ let_it_be(:protection_rule_for_owner) do
+ create(:container_registry_protection_rule,
+ repository_path_pattern: 'my-scope/my-container-release*',
+ project: project_with_crpr,
+ push_protected_up_to_access_level: :owner
+ )
+ end
+
+ let_it_be(:protection_rule_overlapping_for_developer) do
+ create(:container_registry_protection_rule,
+ repository_path_pattern: 'my-scope/my-container-*',
+ project: project_with_crpr,
+ push_protected_up_to_access_level: :developer
+ )
+ end
+
+ where(:project, :access_level, :repository_path, :push_protected) do
+ ref(:project_with_crpr) | Gitlab::Access::REPORTER | 'my-scope/my-container-stage-sha-1234' | true
+ ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-stage-sha-1234' | true
+ ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-stage-sha-1234' | false
+ ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-stage-sha-1234' | false
+ ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-stage-sha-1234' | false
+ ref(:project_with_crpr) | Gitlab::Access::ADMIN | 'my-scope/my-container-stage-sha-1234' | false
+
+ ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-prod-sha-1234' | true
+ ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-prod-sha-1234' | true
+ ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-prod-sha-1234' | false
+ ref(:project_with_crpr) | Gitlab::Access::ADMIN | 'my-scope/my-container-prod-sha-1234' | false
+
+ ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-release-v1' | true
+ ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-release-v1' | true
+ ref(:project_with_crpr) | Gitlab::Access::ADMIN | 'my-scope/my-container-release-v1' | false
+
+ ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-any-suffix' | true
+ ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-any-suffix' | false
+ ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-any-suffix' | false
+
+ # For non-matching repository_path
+ ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/non-matching-container' | false
+
+ # For no access level
+ ref(:project_with_crpr) | Gitlab::Access::NO_ACCESS | 'my-scope/my-container-prod-sha-1234' | true
+
+ # Edge cases
+ ref(:project_with_crpr) | 0 | '' | false
+ ref(:project_with_crpr) | nil | nil | false
+ ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | nil | false
+ ref(:project_with_crpr) | nil | 'my-scope/non-matching-container' | false
+
+ # For projects that have no container registry protection rules
+ ref(:project_without_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-prod-sha-1234' | false
+ ref(:project_without_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-prod-sha-1234' | false
+ ref(:project_without_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-prod-sha-1234' | false
+ end
+
+ with_them do
+ it { is_expected.to eq push_protected }
+ end
+ end
+ end
end
diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb
index 756024b6437..053a32281aa 100644
--- a/spec/models/organizations/organization_spec.rb
+++ b/spec/models/organizations/organization_spec.rb
@@ -61,6 +61,7 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
it { is_expected.to delegate_method(:description).to(:organization_detail) }
it { is_expected.to delegate_method(:avatar).to(:organization_detail) }
it { is_expected.to delegate_method(:avatar_url).to(:organization_detail) }
+ it { is_expected.to delegate_method(:remove_avatar!).to(:organization_detail) }
end
describe 'nested attributes' do
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
index 86e2b288890..312700b1dcf 100644
--- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
@@ -115,8 +115,8 @@ RSpec.describe 'get board lists', feature_category: :team_planning do
let(:issue_params) { { filters: { or: { assignee_usernames: [user.username, another_user.username] } } } }
it 'returns correctly filtered issues' do
- issue1.assignee_ids = user.id
- issue2.assignee_ids = another_user.id
+ IssueAssignee.create!(issue_id: issue1.id, user_id: user.id)
+ IssueAssignee.create!(issue_id: issue2.id, user_id: another_user.id)
subject
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index 7797e8e9402..c9bba26524c 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -65,7 +65,6 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
shared_context 'using job token' do
before do
stub_exclusive_lease
- stub_feature_flags(ci_job_token_scope: true)
end
subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
@@ -74,29 +73,15 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
shared_context 'using job token from another project' do
before do
stub_exclusive_lease
- stub_feature_flags(ci_job_token_scope: true)
end
subject { public_send(method, api(url), params: { job_token: job2.token }) }
end
- shared_context 'using job token while ci_job_token_scope feature flag is disabled' do
- before do
- stub_exclusive_lease
- stub_feature_flags(ci_job_token_scope: false)
- end
-
- subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
- end
-
shared_examples 'rejected job token scopes' do
include_context 'using job token from another project' do
it_behaves_like 'rejected container repository access', :maintainer, :forbidden
end
-
- include_context 'using job token while ci_job_token_scope feature flag is disabled' do
- it_behaves_like 'rejected container repository access', :maintainer, :forbidden
- end
end
describe 'GET /projects/:id/registry/repositories' do
diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb
index 63840b4c30b..c63ca7608c5 100644
--- a/spec/routing/uploads_routing_spec.rb
+++ b/spec/routing/uploads_routing_spec.rb
@@ -79,6 +79,19 @@ RSpec.describe 'Uploads', 'routing' do
end
end
+ context 'for organizations' do
+ it 'allows fetching organization avatars' do
+ expect(get('/uploads/-/system/organizations/organization_detail/avatar/1/test.jpg')).to route_to(
+ controller: 'uploads',
+ action: 'show',
+ model: 'organizations/organization_detail',
+ id: '1',
+ filename: 'test.jpg',
+ mounted_as: 'avatar'
+ )
+ end
+ end
+
it 'does not allow creating uploads for other models' do
unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w[personal_snippet user abuse_report]
diff --git a/spec/services/organizations/update_service_spec.rb b/spec/services/organizations/update_service_spec.rb
index 148840770db..630bfdfe1d7 100644
--- a/spec/services/organizations/update_service_spec.rb
+++ b/spec/services/organizations/update_service_spec.rb
@@ -60,6 +60,14 @@ RSpec.describe Organizations::UpdateService, feature_category: :cell do
it_behaves_like 'updating an organization'
end
+ context 'when avatar is set to nil' do
+ let_it_be(:organization_detail) { create(:organization_detail, organization: organization) }
+ let(:extra_params) { { avatar: nil } }
+ let(:description) { organization_detail.description }
+
+ it_behaves_like 'updating an organization'
+ end
+
include_examples 'updating an organization'
context 'when the organization is not updated' do