Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-12-19 12:10:37 +00:00
parent 17295c75a1
commit a4db97517a
57 changed files with 953 additions and 318 deletions

View File

@ -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

View File

@ -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"

View File

@ -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 }}

View File

@ -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,
},
});

View File

@ -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) {

View File

@ -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">{{

View File

@ -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.')),

View File

@ -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">

View File

@ -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;

View File

@ -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>

View File

@ -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';

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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 }]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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',

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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`.

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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).

View File

@ -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:

View File

@ -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**.

View File

@ -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

View File

@ -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:)

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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!)

View File

@ -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
```

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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',
},
});
});
});
});
});
});

View File

@ -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 }],
]);
});
});

View File

@ -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 } });

View File

@ -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]]);
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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