Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-22 18:15:17 +00:00
parent 8099b2824b
commit de0e57e387
79 changed files with 1564 additions and 546 deletions

View File

@ -9,7 +9,7 @@ export default function initDatePickers() {
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker',
theme: 'gl-datepicker-theme animate-picker',
format: 'yyyy-mm-dd',
container: $datePicker.parent().get(0),
parse: (dateString) => parsePikadayDate(dateString),

View File

@ -1,6 +1,8 @@
<script>
import { debounce, uniq } from 'lodash';
import { GlDropdownDivider, GlDropdownItem, GlCollapsibleListbox } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__, sprintf } from '~/locale';
import { convertEnvironmentScope } from '../utils';
export default {
@ -10,7 +12,12 @@ export default {
GlDropdownItem,
GlCollapsibleListbox,
},
mixins: [glFeatureFlagsMixin()],
props: {
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
environments: {
type: Array,
required: true,
@ -33,24 +40,52 @@ export default {
},
filteredEnvironments() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.environments.filter((environment) => {
return environment.toLowerCase().includes(lowerCasedSearchTerm);
});
},
isEnvScopeLimited() {
return this.glFeatures?.ciLimitEnvironmentScope;
},
searchedEnvironments() {
// If FF is enabled, search query will be fired so this component will already
// receive filtered environments during the refetch.
// If FF is disabled, search the existing list of environments in the frontend
let filtered = this.isEnvScopeLimited ? this.environments : this.filteredEnvironments;
return this.environments
.filter((environment) => {
return environment.toLowerCase().includes(lowerCasedSearchTerm);
})
.map((environment) => ({
value: environment,
text: environment,
}));
// If there is no search term, make sure to include *
if (this.isEnvScopeLimited && !this.searchTerm) {
filtered = uniq([...filtered, '*']);
}
return filtered.sort().map((environment) => ({
value: environment,
text: environment,
}));
},
shouldShowSearchLoading() {
return this.areEnvironmentsLoading && this.isEnvScopeLimited;
},
shouldRenderCreateButton() {
return this.searchTerm && !this.environments.includes(this.searchTerm);
},
shouldRenderDivider() {
return (
(this.isEnvScopeLimited || this.shouldRenderCreateButton) && !this.shouldShowSearchLoading
);
},
environmentScopeLabel() {
return convertEnvironmentScope(this.selectedEnvironmentScope);
},
},
methods: {
debouncedSearch: debounce(function debouncedSearch(searchTerm) {
const newSearchTerm = searchTerm.trim();
this.searchTerm = newSearchTerm;
if (this.isEnvScopeLimited) {
this.$emit('search-environment-scope', newSearchTerm);
}
}, 500),
selectEnvironment(selected) {
this.$emit('select-environment', selected);
this.selectedEnvironment = selected;
@ -60,22 +95,43 @@ export default {
this.selectEnvironment(this.searchTerm);
},
},
i18n: {
maxEnvsNote: s__(
'CiVariable|Maximum of 20 environments listed. For more environments, enter a search query.',
),
},
};
</script>
<template>
<gl-collapsible-listbox
v-model="selectedEnvironment"
block
searchable
:items="filteredEnvironments"
:items="searchedEnvironments"
:searching="shouldShowSearchLoading"
:toggle-text="environmentScopeLabel"
@search="searchTerm = $event.trim()"
@search="debouncedSearch"
@select="selectEnvironment"
>
<template v-if="shouldRenderCreateButton" #footer>
<gl-dropdown-divider />
<gl-dropdown-item data-testid="create-wildcard-button" @click="createEnvironmentScope">
{{ composedCreateButtonLabel }}
</gl-dropdown-item>
<template #footer>
<gl-dropdown-divider v-if="shouldRenderDivider" />
<div v-if="isEnvScopeLimited" data-testid="max-envs-notice">
<gl-dropdown-item class="gl-list-style-none" disabled>
<span class="gl-font-sm">
{{ $options.i18n.maxEnvsNote }}
</span>
</gl-dropdown-item>
</div>
<div v-if="shouldRenderCreateButton">
<!-- TODO: Rethink create wildcard button. https://gitlab.com/gitlab-org/gitlab/-/issues/396928 -->
<gl-dropdown-item
class="gl-list-style-none"
data-testid="create-wildcard-button"
@click="createEnvironmentScope"
>
{{ composedCreateButtonLabel }}
</gl-dropdown-item>
</div>
</template>
</gl-collapsible-listbox>
</template>

View File

@ -74,6 +74,10 @@ export default {
'maskableRegex',
],
props: {
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
areScopedVariablesAvailable: {
type: Boolean,
required: false,
@ -142,7 +146,11 @@ export default {
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
joinedEnvironments() {
environmentsList() {
if (this.glFeatures?.ciLimitEnvironmentScope) {
return this.environments;
}
return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
},
maskedFeedback() {
@ -368,10 +376,12 @@ export default {
</template>
<ci-environments-dropdown
v-if="areScopedVariablesAvailable"
:are-environments-loading="areEnvironmentsLoading"
:selected-environment-scope="variable.environmentScope"
:environments="joinedEnvironments"
:environments="environmentsList"
@select-environment="setEnvironmentScope"
@create-environment-scope="createEnvironmentScope"
@search-environment-scope="$emit('search-environment-scope', $event)"
/>
<gl-form-input v-else :value="$options.i18n.defaultScope" class="gl-w-full" readonly />

View File

@ -9,6 +9,10 @@ export default {
CiVariableModal,
},
props: {
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
areScopedVariablesAvailable: {
type: Boolean,
required: false,
@ -100,6 +104,7 @@ export default {
/>
<ci-variable-modal
v-if="showModal"
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
:hide-environment-scope="hideEnvironmentScope"
@ -110,6 +115,7 @@ export default {
@delete-variable="deleteVariable"
@hideModal="hideModal"
@update-variable="updateVariable"
@search-environment-scope="$emit('search-environment-scope', $event)"
/>
</div>
</div>

View File

@ -6,6 +6,7 @@ import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
ENVIRONMENT_QUERY_LIMIT,
SORT_DIRECTIONS,
UPDATE_MUTATION_ACTION,
environmentFetchErrorText,
@ -162,6 +163,7 @@ export default {
variables() {
return {
fullPath: this.fullPath,
...this.environmentQueryVariables,
};
},
update(data) {
@ -173,10 +175,26 @@ export default {
},
},
computed: {
areEnvironmentsLoading() {
return this.$apollo.queries.environments.loading;
},
environmentQueryVariables() {
if (this.glFeatures?.ciLimitEnvironmentScope) {
return {
first: ENVIRONMENT_QUERY_LIMIT,
search: '',
};
}
return {};
},
isLoading() {
// TODO: Remove areEnvironmentsLoading and show loading icon in dropdown when
// environment query is loading and FF is enabled
// https://gitlab.com/gitlab-org/gitlab/-/issues/396990
return (
(this.$apollo.queries.ciVariables.loading && this.isInitialLoading) ||
this.$apollo.queries.environments.loading ||
this.areEnvironmentsLoading ||
this.isLoadingMoreItems
);
},
@ -228,6 +246,11 @@ export default {
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
async searchEnvironmentScope(searchTerm) {
if (this.glFeatures?.ciLimitEnvironmentScope) {
this.$apollo.queries.environments.refetch({ search: searchTerm });
}
},
async variableMutation(mutationAction, variable) {
try {
const currentMutation = this.mutationData[mutationAction];
@ -264,6 +287,7 @@ export default {
<template>
<ci-variable-settings
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:entity="entity"
:environments="environments"
@ -277,6 +301,7 @@ export default {
@handle-prev-page="handlePrevPage"
@handle-next-page="handleNextPage"
@sort-changed="handleSortChanged"
@search-environment-scope="searchEnvironmentScope"
@update-variable="updateVariable"
/>
</template>

View File

@ -1,6 +1,7 @@
import { __, s__ } from '~/locale';
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const ENVIRONMENT_QUERY_LIMIT = 20;
export const SORT_DIRECTIONS = {
ASC: 'KEY_ASC',

View File

@ -1,7 +1,7 @@
query getProjectEnvironments($fullPath: ID!) {
query getProjectEnvironments($fullPath: ID!, $first: Int, $search: String) {
project(fullPath: $fullPath) {
id
environments {
environments(first: $first, search: $search) {
nodes {
id
name

View File

@ -62,6 +62,8 @@ import {
idleCallback,
allDiscussionWrappersExpanded,
prepareLineForRenamedFile,
parseUrlHashAsFileHash,
isUrlHashNoteLink,
} from './utils';
export const setBaseConfig = ({ commit }, options) => {
@ -101,6 +103,47 @@ export const setBaseConfig = ({ commit }, options) => {
});
};
export const fetchFileByFile = async ({ state, getters, commit }) => {
const isNoteLink = isUrlHashNoteLink(window?.location?.hash);
const id = parseUrlHashAsFileHash(window?.location?.hash, state.currentDiffFileId);
const treeEntry = id
? getters.flatBlobsList.find(({ fileHash }) => fileHash === id)
: getters.flatBlobsList[0];
if (treeEntry && !treeEntry.diffLoaded && !getters.getDiffFileByHash(id)) {
// Overloading "batch" loading indicators so the UI stays mostly the same
commit(types.SET_BATCH_LOADING_STATE, 'loading');
commit(types.SET_RETRIEVING_BATCHES, true);
const urlParams = {
old_path: treeEntry.filePaths.old,
new_path: treeEntry.filePaths.new,
w: state.showWhitespace ? '0' : '1',
view: 'inline',
};
axios
.get(mergeUrlParams({ ...urlParams }, state.endpointDiffForPath))
.then(({ data: diffData }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files });
if (!isNoteLink && !state.currentDiffFileId) {
commit(types.SET_CURRENT_DIFF_FILE, state.diffFiles[0]?.file_hash || '');
}
commit(types.SET_BATCH_LOADING_STATE, 'loaded');
eventHub.$emit('diffFilesModified');
})
.catch(() => {
commit(types.SET_BATCH_LOADING_STATE, 'error');
})
.finally(() => {
commit(types.SET_RETRIEVING_BATCHES, false);
});
}
};
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
let perPage = state.viewDiffsFileByFile ? 1 : 5;
let increaseAmount = 1.4;

View File

@ -559,19 +559,19 @@ export const allDiscussionWrappersExpanded = (diff) => {
return discussionsExpanded;
};
export function isUrlHashNoteLink(urlHash) {
export function isUrlHashNoteLink(urlHash = '') {
const id = urlHash.replace(/^#/, '');
return id.startsWith('note');
}
export function isUrlHashFileHeader(urlHash) {
export function isUrlHashFileHeader(urlHash = '') {
const id = urlHash.replace(/^#/, '');
return id.startsWith('diff-content');
}
export function parseUrlHashAsFileHash(urlHash, currentDiffFileId = '') {
export function parseUrlHashAsFileHash(urlHash = '', currentDiffFileId = '') {
const isNoteLink = isUrlHashNoteLink(urlHash);
let id = urlHash.replace(/^#/, '');

View File

@ -43,6 +43,7 @@ export default {
data-container="body"
data-placement="right"
data-qa-selector="edit_mode_tab"
data-testid="edit-mode-button"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)"
@ -60,6 +61,7 @@ export default {
:aria-label="s__('IDE|Review')"
data-container="body"
data-placement="right"
data-testid="review-mode-button"
type="button"
class="ide-sidebar-link js-ide-review-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.review.name)"
@ -78,6 +80,7 @@ export default {
data-container="body"
data-placement="right"
data-qa-selector="commit_mode_tab"
data-testid="commit-mode-button"
type="button"
class="ide-sidebar-link js-ide-commit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"

View File

@ -99,7 +99,7 @@ export default class IssuableForm {
if ($issuableDueDate.length) {
const calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme animate-picker',
theme: 'gl-datepicker-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
parse: (dateString) => parsePikadayDate(dateString),

View File

@ -14,7 +14,7 @@ export default class OAuthRememberMe {
}
bindEvents() {
$('#remember_me', this.container).on('click', this.toggleRememberMe);
$('#remember_me_omniauth', this.container).on('click', this.toggleRememberMe);
}
toggleRememberMe(event) {

View File

@ -112,7 +112,7 @@ export default {
<template>
<gl-form
class="new-note common-note-form"
class="new-note common-note-form gl-mb-6"
data-testid="saved-reply-form"
@submit.prevent="onSubmit"
>

View File

@ -44,7 +44,7 @@ export default {
</script>
<template>
<div>
<div class="gl-border-t gl-pt-4">
<gl-loading-icon v-if="loading" size="lg" />
<template v-else>
<h5 class="gl-font-lg" data-testid="title">

View File

@ -1,19 +1,19 @@
<script>
import { uniqueId } from 'lodash';
import { GlButton, GlModal, GlModalDirective, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { GlDisclosureDropdown, GlTooltip, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import deleteSavedReplyMutation from '../queries/delete_saved_reply.mutation.graphql';
export default {
components: {
GlButton,
GlDisclosureDropdown,
GlTooltip,
GlModal,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
props: {
reply: {
@ -25,12 +25,32 @@ export default {
return {
isDeleting: false,
modalId: uniqueId('delete-saved-reply-'),
toggleId: uniqueId('actions-toggle-'),
};
},
computed: {
id() {
return getIdFromGraphQLId(this.reply.id);
},
dropdownItems() {
return [
{
text: __('Edit'),
action: () => this.$router.push({ name: 'edit', params: { id: this.id } }),
extraAttrs: {
'data-testid': 'saved-reply-edit-btn',
},
},
{
text: __('Delete'),
action: () => this.$refs['delete-modal'].show(),
extraAttrs: {
'data-testid': 'saved-reply-delete-btn',
class: 'gl-text-red-500!',
},
},
];
},
},
methods: {
onDelete() {
@ -54,34 +74,29 @@ export default {
</script>
<template>
<li class="gl-mb-5">
<li class="gl-pt-4 gl-pb-5 gl-border-b">
<div class="gl-display-flex gl-align-items-center">
<strong data-testid="saved-reply-name">{{ reply.name }}</strong>
<h6 class="gl-mr-3 gl-my-0" data-testid="saved-reply-name">{{ reply.name }}</h6>
<div class="gl-ml-auto">
<gl-button
v-gl-tooltip
:to="{ name: 'edit', params: { id: id } }"
icon="pencil"
:title="__('Edit')"
:aria-label="__('Edit')"
class="gl-mr-3"
data-testid="saved-reply-edit-btn"
/>
<gl-button
v-gl-modal="modalId"
v-gl-tooltip
icon="remove"
:aria-label="__('Delete')"
:title="__('Delete')"
variant="danger"
category="secondary"
data-testid="saved-reply-delete-btn"
<gl-disclosure-dropdown
:items="dropdownItems"
:toggle-id="toggleId"
icon="ellipsis_v"
no-caret
text-sr-only
placement="right"
:toggle-text="__('Saved reply actions')"
:loading="isDeleting"
category="tertiary"
/>
<gl-tooltip :target="toggleId">
{{ __('Saved reply actions') }}
</gl-tooltip>
</div>
</div>
<div class="gl-mt-3 gl-font-monospace">{{ reply.content }}</div>
<gl-modal
ref="delete-modal"
:title="__('Delete saved reply')"
:action-primary="$options.actionPrimary"
:action-secondary="$options.actionSecondary"

View File

@ -20,56 +20,3 @@
}
}
}
.pika-single.gitlab-theme {
.pika-label {
color: $gl-text-color-secondary;
font-size: 14px;
font-weight: $gl-font-weight-normal;
}
th {
padding: 2px 0;
color: $note-disabled-comment-color;
font-weight: $gl-font-weight-normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
}
abbr {
cursor: default;
}
td {
border: 1px solid $calendar-border-color;
&:first-child {
border-left: 0;
}
&:last-child {
border-right: 0;
}
}
.pika-day {
border-radius: 0;
background-color: $white;
text-align: center;
}
.is-today {
.pika-day {
color: inherit;
font-weight: $gl-font-weight-normal;
}
}
.is-selected .pika-day,
.pika-day:hover,
.is-today .pika-day {
background: $gray-darker;
color: $gl-text-color;
box-shadow: none;
}
}

View File

@ -669,8 +669,6 @@ $note-targe3-inside: #ffffd3;
/*
* Calendar
*/
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, 0.1);
$calendar-user-contrib-text: #959494;
/*

View File

@ -14,6 +14,7 @@ module Projects
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_limit_environment_scope, @project)
end
helper_method :highlight_badge

View File

@ -12,7 +12,7 @@ module BreadcrumbsHelper
def breadcrumb_title_link
return @breadcrumb_link if @breadcrumb_link
request.path
request.fullpath
end
def breadcrumb_title(title)

View File

@ -354,7 +354,6 @@ class ProjectPolicy < BasePolicy
enable :metrics_dashboard
enable :read_confidential_issues
enable :read_package
enable :read_product_analytics
enable :read_ci_cd_analytics
enable :read_external_emails
enable :read_grafana

View File

@ -27,6 +27,11 @@
#
module BulkImports
class CreateService
ENTITY_TYPES_MAPPING = {
'group_entity' => 'groups',
'project_entity' => 'projects'
}.freeze
attr_reader :current_user, :params, :credentials
def initialize(current_user, params, credentials)
@ -57,6 +62,7 @@ module BulkImports
def validate!
client.validate_instance_version!
validate_setting_enabled!
client.validate_import_scopes!
end
@ -88,6 +94,28 @@ module BulkImports
end
end
def validate_setting_enabled!
source_full_path, source_type = params[0].values_at(:source_full_path, :source_type)
entity_type = ENTITY_TYPES_MAPPING.fetch(source_type)
if source_full_path =~ /^[0-9]+$/
query = query_type(entity_type)
response = graphql_client.execute(
graphql_client.parse(query.to_s),
{ full_path: source_full_path }
).original_hash
source_entity_identifier = ::GlobalID.parse(response.dig(*query.data_path, 'id')).model_id
else
source_entity_identifier = ERB::Util.url_encode(source_full_path)
end
client.get("/#{entity_type}/#{source_entity_identifier}/export_relations/status")
# the source instance will return a 404 if the feature is disabled as the endpoint won't be available
rescue Gitlab::HTTP::BlockedUrlError
rescue BulkImports::NetworkError
raise ::BulkImports::Error.setting_not_enabled
end
def track_access_level(entity_params)
Gitlab::Tracking.event(
self.class.name,
@ -140,5 +168,20 @@ module BulkImports
token: @credentials[:access_token]
)
end
def graphql_client
@graphql_client ||= BulkImports::Clients::Graphql.new(
url: @credentials[:url],
token: @credentials[:access_token]
)
end
def query_type(entity_type)
if entity_type == 'groups'
BulkImports::Groups::Graphql::GetGroupQuery.new(context: nil)
else
BulkImports::Projects::Graphql::GetProjectQuery.new(context: nil)
end
end
end
end

View File

@ -13,6 +13,6 @@
%span.gl-button-text
= label_for_provider(provider)
- unless hide_remember_me
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me', value: nil) do |c|
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c|
= c.label do
= _('Remember me')

View File

@ -1,6 +0,0 @@
.explore-title.text-center
%h2
= _("Explore GitLab")
%p.lead
= _("Discover projects, groups and snippets. Share your projects with others")
%br

View File

@ -0,0 +1,11 @@
- breadcrumb_title _("Projects")
- page_title _("Explore projects")
= render_dashboard_ultimate_trial(current_user)
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= page_title
.page-title-controls
- if current_user&.can_create_project?
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
= _("New project")

View File

@ -1,15 +1,5 @@
- breadcrumb_title _("Projects")
- page_title _("Explore projects")
- page_canonical_link explore_projects_url
= render_dashboard_ultimate_trial(current_user)
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= page_title
.page-title-controls
- if current_user&.can_create_project?
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
= _("New project")
= render 'explore/projects/head'
= render 'explore/projects/nav'
= render 'projects', projects: @projects

View File

@ -1,14 +1,4 @@
- @hide_top_links = true
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= render_dashboard_ultimate_trial(current_user)
- if current_user
= render 'dashboard/projects_head', project_tab_filter: :explore
- else
= render 'explore/head'
= render 'explore/projects/head'
= render 'explore/projects/nav'
.nothing-here-block

View File

@ -1,14 +1,3 @@
- @hide_top_links = true
- page_title _("Explore projects")
- header_title _("Projects"), dashboard_projects_path
= render_dashboard_ultimate_trial(current_user)
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= page_title
.page-title-controls
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
= _("New project")
= render 'explore/projects/head'
= render 'explore/projects/nav'
= render 'projects', projects: @projects

View File

@ -1,15 +1,3 @@
- @hide_top_links = true
- page_title _("Explore projects")
- header_title _("Projects"), dashboard_projects_path
= render_dashboard_ultimate_trial(current_user)
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= page_title
.page-title-controls
- if current_user&.can_create_project?
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
= _("New project")
= render 'explore/projects/head'
= render 'explore/projects/nav'
= render 'projects', projects: @projects

View File

@ -0,0 +1,8 @@
---
name: ci_limit_environment_scope
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113171
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/395003
milestone: '15.10'
type: development
group: group::pipeline security
default_enabled: false

View File

@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111935
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390330
milestone: '15.9'
type: development
group: group::certify
group: group::product planning
default_enabled: false

View File

@ -0,0 +1,77 @@
- name: Automatically resolve SAST findings when rules are disabled
description: |
On GitLab.com, GitLab SAST now automatically [resolves](https://docs.gitlab.com/ee/user/application_security/vulnerabilities/#vulnerability-status-values) vulnerabilities from the [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep)- and [KICS](https://gitlab.com/gitlab-org/security-products/analyzers/kics)-based analyzers when either you [disable a predefined rule](https://docs.gitlab.com/ee/user/application_security/sast/customize_rulesets.html#disable-predefined-rules), or we remove a rule from the default ruleset. The Vulnerability Management system then leaves a comment explaining that the rule was removed, so you still have a historical record of the vulnerability. This feature is enabled by default on GitLab.com and in GitLab 15.10.
stage: Secure
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/application_security/sast/#automatic-vulnerability-resolution'
image_url: 'https://about.gitlab.com/images/15_10/automatic-resolution.png'
published_at: 2023-03-22
release: 15.10
- name: See all branch-related settings together
description: |
All branch-related protections now display on a single page. To see a unified list of your branches and all their protection methods, go to **Settings > Repository > Branch rules**. Each branch shows the merge request approvals, security approvals, protected branches, and status checks configured for it. It is now easier to see a holistic view of a specific branch's protections. We hope this change helps you discover, use, and monitor these settings more easily. We'd love your feedback [in issue #388149](https://gitlab.com/gitlab-org/gitlab/-/issues/388149).
stage: Create
self-managed: false
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/project/repository/branches/'
image_url: 'https://img.youtube.com/vi/AUrwtjIr124/hqdefault.jpg'
published_at: 2023-03-22
release: 15.10
- name: Create and switch branches in the Web IDE Beta
description: |
When you open the Web IDE Beta from a repository or merge request, the currently selected branch is used by default. You can create a new branch with your changes or, if you're not on a protected branch, commit to the current branch. Starting with GitLab 15.10, you can now also create a new branch any time while making changes or switch branches in the Web IDE Beta. This way, you can boost your productivity by not having to close the Web IDE Beta to switch contexts.
stage: Create
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/project/web_ide_beta/#switch-branches'
image_url: 'https://about.gitlab.com/images/15_10/create-web-ide-manage-branches.png'
published_at: 2023-03-22
release: 15.10
- name: Compliance frameworks report
description: |
Previous versions of GitLab provided a compliance report that shows compliance violations. In GitLab 15.10, we've added a compliance framework report so you can quickly see which compliance frameworks have been applied to the projects in your group.
stage: Govern
self-managed: true
gitlab-com: true
available_in: [Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/compliance/compliance_report/#compliance-frameworks-report'
image_url: 'https://about.gitlab.com/images/15_10/govern-compliance-framework-report.png'
published_at: 2023-03-22
release: 15.10
- name: Suggested Reviewers generally available
description: |
Since release in closed beta, Suggested Reviewers has been enabled in over 1,000 projects and suggested over 200,000 reviewers. We've also made the service more reliable and are now making it generally available to all Ultimate customers. Now, GitLab can now recommend a reviewer with [Suggested Reviewers](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#suggested-reviewers). With this feature, machine learning (ML)-powered suggestions appear in the [reviewer dropdown](https://docs.gitlab.com/ee/user/project/merge_requests/getting_started.html#reviewer) in the merge request sidebar. Suggested Reviewers is our [first of many fully available ML features](https://about.gitlab.com/blog/2023/03/16/what-the-ml-ai/) at GitLab.
stage: Modelops
self-managed: false
gitlab-com: true
available_in: [Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#suggested-reviewers'
image_url: 'https://about.gitlab.com/images/15_10/create-code-review-suggested-reviewers.png'
published_at: 2023-03-22
release: 15.10
- name: Create diagrams in wikis by using the diagrams.net editor
description: |
With GitLab 15.10, you can more easily create and edit diagrams in wikis by using the diagrams.net GUI editor. This feature is available in the Markdown editor and the content editor and was implemented in close collaboration with the GitLab wider community.
stage: Plan
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/markdown.html#diagramsnet-editor'
image_url: 'https://img.youtube.com/vi/F6kfhpRN3ZE/hqdefault.jpg'
published_at: 2023-03-22
release: 15.10
- name: Apple App Store integration
description: |
From GitLab 15.10, you can configure and validate your projects with Apple App Store credentials. You can then use those credentials in CI/CD pipelines to automate releases to Test Flight and the App Store. To record your experiences with the App Store integration, see this [feedback issue](https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/feedback/-/issues/10).
stage: Deploy
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: 'https://docs.gitlab.com/ee/user/project/integrations/apple_app_store.html'
image_url: 'https://img.youtube.com/vi/CwzAWVgJeK8/hqdefault.jpg'
published_at: 2023-03-22
release: 15.10

View File

@ -8,7 +8,7 @@ type: reference
# CI/CD minutes quota **(PREMIUM)**
NOTE:
`CI/CD minutes` is being renamed to `compute credits`. During this transition, you might see references in the UI and documentation to `CI minutes`, `CI/CD minutes`, `pipeline minutes`, `CI pipeline minutes`, and `compute credits`. All of these terms refer to compute credits.
`CI/CD minutes` is being renamed to `compute credits`. During this transition, you might see references in the UI and documentation to `CI/CD minutes`, `CI minutes`, `pipeline minutes`, `CI pipeline minutes`, `pipeline minutes quota`, and `compute credits`. For more information, see [issue 5218](https://gitlab.com/gitlab-com/Product/-/issues/5218).
Administrators can limit the amount of time that projects can use to run jobs on
[shared runners](../runners/runners_scope.md#shared-runners) each month. This limit

View File

@ -286,8 +286,7 @@ CI/CD is always uppercase. No need to spell it out on first use.
## CI/CD minutes
Use **CI/CD minutes** instead of **CI minutes**, **pipeline minutes**, **pipeline minutes quota**, or
**CI pipeline minutes**. This decision was made in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342813).
Do not use **CI/CD minutes**. This term was renamed to [**compute credits**](#compute-credits).
## click
@ -311,6 +310,19 @@ Use **From the command line** to introduce commands.
Hyphenate when using as an adjective. For example, **a command-line tool**.
## compute credits
Use **compute credits** instead of these (or similar) terms:
- **CI/CD minutes**
- **CI minutes**
- **pipeline minutes**
- **CI pipeline minutes**
- **pipeline minutes quota**
As of March, 2022, this language is still being standardized in the documentation and UI.
For more information, see [issue 5218](https://gitlab.com/gitlab-com/Product/-/issues/5218).
## confirmation dialog
Use **confirmation dialog** to describe the dialog box that asks you to confirm your action. For example:

View File

@ -307,7 +307,7 @@ To enable automatic linking for SAML, see the [SAML setup instructions](saml.md#
## Create an external providers list
You can define a list of external OmniAuth providers.
Users who create accounts or sign in to GitLab through the listed providers do not get access to [internal projects](../user/public_access.md#internal-projects-and-groups).
Users who create accounts or sign in to GitLab through the listed providers do not get access to [internal projects](../user/public_access.md#internal-projects-and-groups) and are marked as [external users](../user/admin_area/external_users.md).
To define the external providers list, use the full name of the provider,
for example, `google_oauth2` for Google. For provider names, see the

View File

@ -94,7 +94,7 @@ If an incident template is configured for the project, then the template content
Comments are displayed in threads, but can be displayed chronologically
[by toggling on the recent updates view](#recent-updates-view).
When you make changes to an incident, GitLab creates system notes and
When you make changes to an incident, GitLab creates [system notes](../../user/project/system_notes.md) and
displays them below the summary.
### Metrics **(PREMIUM)**

View File

@ -48,6 +48,7 @@ Additionally, users can be set as external users using:
- [SAML groups](../../integration/saml.md#external-groups).
- [LDAP groups](../../administration/auth/ldap/ldap_synchronization.md#external-groups).
- the [External providers list](../../integration/omniauth.md#create-an-external-providers-list).
## Set a new user to external

View File

@ -10,6 +10,7 @@ type: reference
To manage labels for the GitLab instance, select **Labels** (**{labels}**) from the Admin Area sidebar. For more details on how to manage labels, see [Labels](../project/labels.md).
Labels created in the Admin Area are automatically added to new projects.
They are not available to new groups.
Updating or adding labels in the Admin Area does not modify labels in existing projects.
![Default label set](img/admin_labels_v14_7.png)

View File

@ -5,13 +5,13 @@ group: Composition Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# License compliance (deprecated) **(ULTIMATE)**
# License Compliance (deprecated) **(ULTIMATE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5483) in GitLab 11.0.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387561) in GitLab 15.9.
WARNING:
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387561) in GitLab 15.9. Users should migrate over to use the [new method of license scanning](../license_scanning_of_cyclonedx_files/index.md) prior to GitLab 16.0.
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387561) in GitLab 15.9. You should instead migrate to use [License approval policies](../license_approval_policies.md) and the [new method of license scanning](../license_scanning_of_cyclonedx_files/index.md) prior to GitLab 16.0.
If you're using [GitLab CI/CD](../../../ci/index.md), you can use License Compliance to search your
project's dependencies for their licenses. You can then decide whether to allow or deny the use of
@ -49,6 +49,38 @@ that you can later download and analyze.
WARNING:
License Compliance Scanning does not support run-time installation of compilers and interpreters.
## Enable License Compliance
To enable License Compliance in your project's pipeline, either:
- Enable [Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance)
(provided by [Auto DevOps](../../../topics/autodevops/index.md)).
- Include the [`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml) in your `.gitlab-ci.yml` file.
License Compliance is not supported when GitLab is run with FIPS mode enabled.
### Include the License Scanning template
Prerequisites:
- [GitLab Runner](../../../ci/runners/index.md) available, with the
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). If you're using the
shared runners on GitLab.com, this is enabled by default.
- License Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the
`.gitlab-ci.yml` file, the `test` stage is required.
- [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) must be disabled.
To [include](../../../ci/yaml/index.md#includetemplate) the
[`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml), add it to your `.gitlab-ci.yml` file:
```yaml
include:
- template: Security/License-Scanning.gitlab-ci.yml
```
The included template creates a `license_scanning` job in your CI/CD pipeline and scans your
dependencies to find their licenses.
## License expressions
GitLab has limited support for [composite licenses](https://spdx.github.io/spdx-spec/v2-draft/SPDX-license-expressions/).
@ -89,39 +121,7 @@ The reported licenses might be incomplete or inaccurate.
| Rust | [Cargo](https://crates.io/) |
| PHP | [Composer](https://getcomposer.org/) |
## Enable License Compliance
To enable License Compliance in your project's pipeline, either:
- Enable [Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance)
(provided by [Auto DevOps](../../../topics/autodevops/index.md)).
- Include the [`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml) in your `.gitlab-ci.yml` file.
License Compliance is not supported when GitLab is run with FIPS mode enabled.
### Include the License Scanning template
Prerequisites:
- [GitLab Runner](../../../ci/runners/index.md) available, with the
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). If you're using the
shared runners on GitLab.com, this is enabled by default.
- License Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the
`.gitlab-ci.yml` file, the `test` stage is required.
- [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) must be disabled.
To [include](../../../ci/yaml/index.md#includetemplate) the
[`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml), add it to your `.gitlab-ci.yml` file:
```yaml
include:
- template: Security/License-Scanning.gitlab-ci.yml
```
The included template creates a `license_scanning` job in your CI/CD pipeline and scans your
dependencies to find their licenses.
### Available CI/CD variables
## Available CI/CD variables
License Compliance can be configured using CI/CD variables.
@ -141,7 +141,7 @@ License Compliance can be configured using CI/CD variables.
| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address to download the analyzer from. |
| `SETUP_CMD` | no | Custom setup for the dependency installation (experimental). |
### Installing custom dependencies
## Installing custom dependencies
> Introduced in GitLab 11.4.
@ -169,7 +169,7 @@ variables:
In this example, `my-custom-install-script.sh` is a shell script at the root
directory of your project.
### Working with Monorepos
## Working with Monorepos
Depending on your language, you may need to specify the path to the individual
projects of a monorepo using the `LICENSE_FINDER_CLI_OPTS` variable. Passing in
@ -184,7 +184,7 @@ variables:
LICENSE_FINDER_CLI_OPTS: "--aggregate_paths=relative-path/to/sub-project/one relative-path/to/sub-project/two"
```
### Overriding the template
## Overriding the template
WARNING:
Beginning in GitLab 13.0, the use of [`only` and `except`](../../../ci/yaml/index.md#only--except)
@ -203,7 +203,7 @@ license_scanning:
CI_DEBUG_TRACE: "true"
```
### Configuring Maven projects
## Configuring Maven projects
The License Compliance tool provides a `MAVEN_CLI_OPTS` CI/CD variable which can hold
the command line arguments to pass to the `mvn install` command which is executed under the hood.
@ -227,7 +227,7 @@ to explicitly add `-DskipTests` to your options.
If you still need to run tests during `mvn install`, add `-DskipTests=false` to
`MAVEN_CLI_OPTS`.
#### Using private Maven repositories
### Using private Maven repositories
If you have a private Maven repository which requires login credentials,
you can use the `MAVEN_CLI_OPTS` CI/CD variable.
@ -250,13 +250,13 @@ Alternatively, you can use a Java key store to verify the TLS connection. For in
generate a key store file, see the
[Maven Guide to Remote repository access through authenticated HTTPS](https://maven.apache.org/guides/mini/guide-repository-ssl.html).
### Selecting the version of Java
## Selecting the version of Java
License Compliance uses Java 8 by default. You can specify a different Java version using `LM_JAVA_VERSION`.
`LM_JAVA_VERSION` only accepts versions: 8, 11, 14, 15.
### Selecting the version of Python
## Selecting the version of Python
> - [Introduced](https://gitlab.com/gitlab-org/security-products/license-management/-/merge_requests/36) in GitLab 12.0.
> - In [GitLab 12.2](https://gitlab.com/gitlab-org/gitlab/-/issues/12032), Python 3.5 became the default.
@ -277,22 +277,22 @@ license_scanning:
`LM_PYTHON_VERSION` or `ASDF_PYTHON_VERSION` can be used to specify the desired version of Python. When both variables are specified `LM_PYTHON_VERSION` takes precedence.
### Custom root certificates for Python
## Custom root certificates for Python
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).
#### Using private Python repositories
### Using private Python repositories
If you have a private Python repository you can use the `PIP_INDEX_URL` [CI/CD variable](#available-cicd-variables)
to specify its location.
### Configuring npm projects
## Configuring npm projects
You can configure npm projects by using an [`.npmrc`](https://docs.npmjs.com/configuring-npm/npmrc.html/)
file.
#### Using private npm registries
### Using private npm registries
If you have a private npm registry you can use the
[`registry`](https://docs.npmjs.com/using-npm/config/#registry)
@ -304,7 +304,7 @@ For example:
registry = https://npm.example.com
```
#### Custom root certificates for npm
### Custom root certificates for npm
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).
@ -318,12 +318,12 @@ For example:
strict-ssl = false
```
### Configuring Yarn projects
## Configuring Yarn projects
You can configure Yarn projects by using a [`.yarnrc.yml`](https://yarnpkg.com/configuration/yarnrc/)
file.
#### Using private Yarn registries
### Using private Yarn registries
If you have a private Yarn registry you can use the
[`npmRegistryServer`](https://yarnpkg.com/configuration/yarnrc/#npmRegistryServer)
@ -335,17 +335,17 @@ For example:
npmRegistryServer: "https://npm.example.com"
```
#### Custom root certificates for Yarn
### Custom root certificates for Yarn
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).
### Configuring Bower projects
## Configuring Bower projects
You can configure Bower projects by using a [`.bowerrc`](https://bower.io/docs/config/#bowerrc-specification)
file.
#### Using private Bower registries
### Using private Bower registries
If you have a private Bower registry you can use the
[`registry`](https://bower.io/docs/config/#bowerrc-specification)
@ -359,16 +359,16 @@ For example:
}
```
#### Custom root certificates for Bower
### Custom root certificates for Bower
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables), or by
specifying a `ca` setting in a [`.bowerrc`](https://bower.io/docs/config/#bowerrc-specification)
file.
### Configuring Bundler projects
## Configuring Bundler projects
#### Using private Bundler registries
### Using private Bundler registries
If you have a private Bundler registry you can use the
[`source`](https://bundler.io/man/gemfile.5.html#GLOBAL-SOURCES)
@ -380,7 +380,7 @@ For example:
source "https://gems.example.com"
```
#### Custom root certificates for Bundler
### Custom root certificates for Bundler
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables), or by
@ -388,9 +388,9 @@ specifying a [`BUNDLE_SSL_CA_CERT`](https://bundler.io/v2.0/man/bundle-config.1.
[variable](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
in the job definition.
### Configuring Cargo projects
## Configuring Cargo projects
#### Using private Cargo registries
### Using private Cargo registries
If you have a private Cargo registry you can use the
[`registries`](https://doc.rust-lang.org/cargo/reference/registries.html)
@ -403,7 +403,7 @@ For example:
my-registry = { index = "https://my-intranet:8080/git/index" }
```
#### Custom root certificates for Cargo
### Custom root certificates for Cargo
To supply a custom root certificate to complete TLS verification, do one of the following:
@ -412,9 +412,9 @@ To supply a custom root certificate to complete TLS verification, do one of the
[variable](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
in the job definition.
### Configuring Composer projects
## Configuring Composer projects
#### Using private Composer registries
### Using private Composer registries
If you have a private Composer registry you can use the
[`repositories`](https://getcomposer.org/doc/05-repositories.md)
@ -437,7 +437,7 @@ For example:
}
```
#### Custom root certificates for Composer
### Custom root certificates for Composer
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables), or by
@ -445,7 +445,7 @@ specifying a [`COMPOSER_CAFILE`](https://getcomposer.org/doc/03-cli.md#composer-
[variable](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
in the job definition.
### Configuring Conan projects
## Configuring Conan projects
You can configure [Conan](https://conan.io/) projects by adding a `.conan` directory to your
project root. The project root serves as the [`CONAN_USER_HOME`](https://docs.conan.io/en/latest/reference/env_vars.html#conan-user-home).
@ -474,7 +474,7 @@ NOTE:
`license_scanning` image ships with [Mono](https://www.mono-project.com/) and [MSBuild](https://github.com/mono/msbuild#microsoftbuild-msbuild).
Additional setup may be required to build packages for this project configuration.
#### Using private Conan registries
### Using private Conan registries
By default, [Conan](https://conan.io/) uses the `conan-center` remote. For example:
@ -508,7 +508,7 @@ example:
If credentials are required to authenticate then you can configure a [protected CI/CD variable](../../../ci/variables/index.md#protect-a-cicd-variable)
following the naming convention described in the [`CONAN_LOGIN_USERNAME` documentation](https://docs.conan.io/en/latest/reference/env_vars.html#conan-login-username-conan-login-username-remote-name).
#### Custom root certificates for Conan
### Custom root certificates for Conan
You can provide custom certificates by adding a `.conan/cacert.pem` file to the project root and
setting [`CA_CERT_PATH`](https://docs.conan.io/en/latest/reference/env_vars.html#conan-cacert-path)
@ -518,7 +518,7 @@ If you specify the `ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-
variable's X.509 certificates are installed in the Docker image's default trust store and Conan is
configured to use this as the default `CA_CERT_PATH`.
### Configuring Go projects
## Configuring Go projects
To configure [Go modules](https://github.com/golang/go/wiki/Modules)
based projects, specify [CI/CD variables](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
@ -528,14 +528,14 @@ If a project has [vendored](https://pkg.go.dev/cmd/go#hdr-Vendor_Directories) it
then the combination of the `vendor` directory and `mod.sum` file are used to detect the software
licenses associated with the Go module dependencies.
#### Using private Go registries
### Using private Go registries
You can use the [`GOPRIVATE`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
and [`GOPROXY`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
environment variables to control where modules are sourced from. Alternatively, you can use
[`go mod vendor`](https://go.dev/ref/mod#tmp_28) to vendor a project's modules.
#### Custom root certificates for Go
### Custom root certificates for Go
You can specify the [`-insecure`](https://pkg.go.dev/cmd/go/internal/get) flag by exporting the
[`GOFLAGS`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
@ -550,7 +550,7 @@ license_scanning:
GOFLAGS: '-insecure'
```
#### Using private NuGet registries
### Using private NuGet registries
If you have a private NuGet registry you can add it as a source
by adding it to the [`packageSources`](https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#package-source-sections)
@ -568,7 +568,7 @@ For example:
</configuration>
```
#### Custom root certificates for NuGet
### Custom root certificates for NuGet
You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).

View File

@ -18,13 +18,14 @@ Other 3rd party scanners may also be used as long as they produce a CycloneDX fi
This method of scanning is also capable of parsing and identifying over 500 different types of licenses, as defined in [the SPDX list](https://spdx.org/licenses/).
Licenses not in the SPDX list are reported as "Unknown". License information can also be extracted from packages that are dual-licensed, or have multiple different licenses that apply.
To enable license detection using Dependency Scanning in a project,
include the `Jobs/Dependency-Scanning.gitlab-ci.yml` template in its CI configuration,
but do not include the `Jobs/License-Scanning.gitlab-ci.yml` template.
## Enable license scanning
## Requirements
Prerequisites:
The license scanning requirements are the same as those for [Dependency Scanning](../../application_security/dependency_scanning/index.md#requirements).
- Enable [Dependency Scanning](../../application_security/dependency_scanning/index.md#configuration).
From the `.gitlab-ci.yml` file, remove the deprecated line `Jobs/License-Scanning.gitlab-ci.yml`, if
it's present.
## Supported languages and package managers
@ -103,11 +104,6 @@ License scanning is supported for the following languages and package managers:
The supported files and versions are the ones supported by
[Dependency Scanning](../../application_security/dependency_scanning/index.md#supported-languages-and-package-managers).
## Configuration
To enable license scanning of CycloneDX files,
you must configure [Dependency Scanning](../../application_security/dependency_scanning/index.md#configuration).
## License expressions
GitLab has limited support for [composite licenses](https://spdx.github.io/spdx-spec/v2-draft/SPDX-license-expressions/).

View File

@ -499,7 +499,7 @@ For information on automatically managing GitLab group membership, see [SAML Gro
The [Generated passwords for users created through integrated authentication](../../../security/passwords_for_integrated_authentication_methods.md) guide provides an overview of how GitLab generates and sets passwords for users created via SAML SSO for Groups.
### NameID
## NameID
GitLab.com uses the SAML NameID to identify users. The NameID element:
@ -516,7 +516,7 @@ The relevant field name and recommended value for supported providers are in the
WARNING:
Once users have signed into GitLab using the SSO SAML setup, changing the `NameID` breaks the configuration and potentially locks users out of the GitLab group.
#### NameID Format
### NameID Format
We recommend setting the NameID format to `Persistent` unless using a field (such as email) that requires a different format.
Most NameID formats can be used, except `Transient` due to the temporary nature of this format.

View File

@ -119,7 +119,6 @@ To edit an OKR:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.7 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) to feature flag named `work_items_mvc` in GitLab 15.8. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.10.
> - Changing activity sort order [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.8.
> - Filtering activity [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389971) in GitLab 15.10.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.10.

View File

@ -112,6 +112,43 @@ to work with all third-party registries in the same predictable way. If you use
Registry, this workaround is not required because we implemented a special tag delete operation. In
this case, you can expect cleanup policies to be consistent and predictable.
#### Example cleanup policy workflow
The interaction between the keep and remove rules for the cleanup policy can be complex.
For example, with a project with this cleanup policy configuration:
- **Keep the most recent**: 1 tag per image name.
- **Keep tags matching**: `production-.*`
- **Remove tags older than**: 7 days.
- **Remove tags matching**: `.*`.
And a container repository with these tags:
- `latest`, published 2 hours ago.
- `production-v44`, published 3 days ago.
- `production-v43`, published 6 days ago.
- `production-v42`, published 11 days ago.
- `dev-v44`, published 2 days ago.
- `dev-v43`, published 5 day ago.
- `dev-v42`, published 10 days ago.
- `v44`, published yesterday.
- `v43`, published 12 days ago.
- `v42`, published 20 days ago.
In this example, the tags that would be deleted in the next cleanup run are `dev-v42`, `v43`, and `v42`.
You can interpret the rules as applying with this precedence:
1. The keep rules have highest precedence. Tags must be kept when they match **any** rule.
- The `latest` tag must be kept, because `latest` tags are always kept.
- The `production-v44`, `production-v43`, and `production-v42` tags must be kept,
because they match the **Keep tags matching** rule.
- The `v44` tag must be kept because it's the most recent, matching the **Keep the most recent** rule.
1. The remove rules have lower precedence, and tags are only deleted if **all** rules match.
For the tags not matching any keep rules (`dev-44`, `dev-v43`, `dev-v42`, `v43`, and `v42`):
- `dev-44` and `dev-43` do **not** match the **Remove tags older than**, and are kept.
- `dev-v42`, `v43`, and `v42` match both **Remove tags older than** and **Remove tags matching**
rules, so these three tags can be deleted.
### Create a cleanup policy
You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the UI.

View File

@ -601,7 +601,7 @@ eventually pick it up. When they're done, they move it to **Done**, to close the
issue.
This process can be seen clearly when visiting an issue. With every move
to another list, the label changes and a system note is recorded.
to another list, the label changes and a [system note](system_notes.md) is recorded.
![issue board system notes](img/issue_board_system_notes_v13_6.png)

View File

@ -103,7 +103,7 @@ When bulk editing issues in a group, you can edit the following attributes:
## Move an issue
When you move an issue, it's closed and copied to the target project.
The original issue is not deleted. A system note, which indicates
The original issue is not deleted. A [system note](../system_notes.md), which indicates
where it came from and went to, is added to both issues.
Be careful when moving an issue to a project with different access rules. Before moving the issue, make sure it does not contain sensitive data.

View File

@ -125,7 +125,7 @@ project, from the GitLab user interface:
## View system notes for cherry-picked commits
When you cherry-pick a merge commit in the GitLab UI or API, GitLab adds a system note
When you cherry-pick a merge commit in the GitLab UI or API, GitLab adds a [system note](../system_notes.md)
to the related merge request thread in the format **{cherry-pick-commit}**
`[USER]` **picked the changes into the branch** `[BRANCHNAME]` with commit** `[SHA]` `[DATE]`:

View File

@ -0,0 +1,56 @@
---
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# System notes **(FREE)**
System notes are short descriptions that help you understand the history of events
that occur during the life cycle of a GitLab object, such as:
- [Alerts](../../operations/incident_management/alerts.md).
- [Designs](issues/design_management.md).
- [Issues](issues/index.md).
- [Merge requests](merge_requests/index.md).
- [Objectives and key results](../okrs.md) (OKRs).
- [Tasks](../tasks.md).
GitLab logs information about events triggered by Git or the GitLab application
in system notes.
## Show or filter system notes
By default, system notes do not display. When displayed, they are shown oldest first.
If you change the filter or sort options, your selection is remembered across sections.
The filtering options are:
- **Show all activity** displays both comments and history.
- **Show comments only** hides system notes.
- **Show history only** hides user comments.
### On an epic
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Epics** (**{epic}**).
1. Identify your desired epic, and select its title.
1. Go to the **Activity** section.
1. For **Sort or filter**, select **Show all activity**.
### On an issue
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Issues** and find your issue.
1. Go to **Activity**.
1. For **Sort or filter**, select **Show all activity**.
### On a merge request
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Merge requests** and find your merge request.
1. Go to **Activity**.
1. For **Sort or filter**, select **Show all activity**.
## Related topics
- The [Notes API](../../api/notes.md) can add system notes to objects in GitLab.

View File

@ -15,6 +15,10 @@ module Backup
repositories_paths: 'REPOSITORIES_PATHS'
}.freeze
YAML_PERMITTED_CLASSES = [
ActiveSupport::TimeWithZone, ActiveSupport::TimeZone, Symbol, Time
].freeze
TaskDefinition = Struct.new(
:enabled, # `true` if the task can be used. Treated as `true` when not specified.
:human_name, # Name of the task used for logging.
@ -247,7 +251,9 @@ module Backup
end
def read_backup_information
@backup_information ||= YAML.load_file(File.join(backup_path, MANIFEST_NAME))
@backup_information ||= YAML.safe_load_file(
File.join(backup_path, MANIFEST_NAME),
permitted_classes: YAML_PERMITTED_CLASSES)
end
def write_backup_information

View File

@ -165,6 +165,8 @@ module BulkImports
raise ::BulkImports::NetworkError.new("Unsuccessful response #{response.code} from #{response.request.path.path}. Body: #{response.parsed_response}", response: response)
rescue Gitlab::HTTP::BlockedUrlError => e
raise e
rescue *Gitlab::HTTP::HTTP_ERRORS => e
raise ::BulkImports::NetworkError, e
end

View File

@ -3,8 +3,7 @@
module BulkImports
class Error < StandardError
def self.unsupported_gitlab_version
self.new("Unsupported GitLab version. Source instance must run GitLab version #{BulkImport::MIN_MAJOR_VERSION} " \
"or later.")
self.new("Unsupported GitLab version. Minimum supported version is #{BulkImport::MIN_MAJOR_VERSION}.")
end
def self.scope_validation_failure
@ -19,5 +18,11 @@ module BulkImports
def self.destination_full_path_validation_failure(full_path)
self.new("Import aborted as '#{full_path}' already exists. Change the destination and try again.")
end
def self.setting_not_enabled
self.new("Group import disabled on source or destination instance. " \
"Ask an administrator to enable it on both instances and try again."
)
end
end
end

View File

@ -26,7 +26,7 @@ module Sidebars
override :active_routes
def active_routes
{ page: [link, explore_root_path] }
{ page: [link, explore_root_path, starred_explore_projects_path, trending_explore_projects_path] }
end
end
end

View File

@ -9158,6 +9158,9 @@ msgstr ""
msgid "CiVariable|Define a CI/CD variable in the UI"
msgstr ""
msgid "CiVariable|Maximum of 20 environments listed. For more environments, enter a search query."
msgstr ""
msgid "CiVariable|New environment"
msgstr ""
@ -15122,9 +15125,6 @@ msgstr ""
msgid "Discover GitLab Geo"
msgstr ""
msgid "Discover projects, groups and snippets. Share your projects with others"
msgstr ""
msgid "Discover|Check your application for security vulnerabilities that may lead to unauthorized access, data leaks, and denial of services."
msgstr ""
@ -17256,9 +17256,6 @@ msgstr ""
msgid "Explore"
msgstr ""
msgid "Explore GitLab"
msgstr ""
msgid "Explore groups"
msgstr ""
@ -35339,6 +35336,9 @@ msgstr ""
msgid "ProtectedEnvironments|Number of approvals must be between 1 and 5"
msgstr ""
msgid "ProtectedEnvironments|Required approval count"
msgstr ""
msgid "ProtectedEnvironments|Save"
msgstr ""
@ -38284,6 +38284,9 @@ msgstr ""
msgid "Saved replies can be used when creating comments inside issues, merge requests, and epics."
msgstr ""
msgid "Saved reply actions"
msgstr ""
msgid "Saving"
msgstr ""

View File

@ -55,8 +55,8 @@
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
"@gitlab/svgs": "3.26.0",
"@gitlab/ui": "58.2.0",
"@gitlab/svgs": "3.28.0",
"@gitlab/ui": "58.2.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230223005157",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",

View File

@ -3,6 +3,18 @@
require 'spec_helper'
RSpec.describe 'User explores projects', feature_category: :user_profile do
describe '"All" tab' do
it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :explore_projects_path, :projects
end
describe '"Most starred" tab' do
it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :starred_explore_projects_path, :projects
end
describe '"Trending" tab' do
it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :trending_explore_projects_path, :projects
end
context 'when some projects exist' do
let_it_be(:archived_project) { create(:project, :archived) }
let_it_be(:internal_project) { create(:project, :internal) }

View File

@ -14,6 +14,7 @@ RSpec.describe 'Profile > Saved replies > User deletes saved reply', :js,
it 'shows the user a list of their saved replies' do
visit profile_saved_replies_path
click_button 'Saved reply actions'
find('[data-testid="saved-reply-delete-btn"]').click
page.within('.gl-modal') do

View File

@ -16,6 +16,7 @@ RSpec.describe 'Profile > Saved replies > User updated saved reply', :js,
end
it 'shows the user a list of their saved replies' do
click_button 'Saved reply actions'
find('[data-testid="saved-reply-edit-btn"]').click
find('[data-testid="saved-reply-name-input"]').set('test')

View File

@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe 'Upload Dropzone Field', feature_category: :integrations do
include_context 'project integration activation'
it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures do
it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures,
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/398636' do
visit_project_integration('Apple App Store Connect')
expect(page).to have_content('Drag your Private Key file here or click to upload.')

View File

@ -8,40 +8,30 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
let_it_be(:work_item) { create(:work_item, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:milestones) { create_list(:milestone, 25, project: project) }
let(:work_items_path) { project_work_items_path(project, work_items_path: work_item.iid, iid_path: true) }
context 'for signed in user' do
before do
project.add_developer(user)
sign_in(user)
visit work_items_path
end
context 'with internal id' do
before do
visit project_work_items_path(project, work_items_path: work_item.iid, iid_path: true)
it 'uses IID path in breadcrumbs' do
within('[data-testid="breadcrumb-current-link"]') do
expect(page).to have_link('Work Items', href: work_items_path)
end
it_behaves_like 'work items title'
it_behaves_like 'work items status'
it_behaves_like 'work items assignees'
it_behaves_like 'work items labels'
it_behaves_like 'work items comments'
it_behaves_like 'work items description'
it_behaves_like 'work items milestone'
end
context 'with global id' do
before do
stub_feature_flags(use_iid_in_work_items_path: false)
visit project_work_items_path(project, work_items_path: work_item.id)
end
it_behaves_like 'work items status'
it_behaves_like 'work items assignees'
it_behaves_like 'work items labels'
it_behaves_like 'work items comments'
it_behaves_like 'work items description'
end
it_behaves_like 'work items title'
it_behaves_like 'work items status'
it_behaves_like 'work items assignees'
it_behaves_like 'work items labels'
it_behaves_like 'work items comments'
it_behaves_like 'work items description'
it_behaves_like 'work items milestone'
end
context 'for signed in owner' do
@ -50,7 +40,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
sign_in(user)
visit project_work_items_path(project, work_items_path: work_item.id)
visit work_items_path
end
it_behaves_like 'work items invite members'

View File

@ -1,5 +1,5 @@
import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { allEnvironments } from '~/ci/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
@ -7,7 +7,11 @@ describe('Ci environments dropdown', () => {
let wrapper;
const envs = ['dev', 'prod', 'staging'];
const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
const defaultProps = {
areEnvironmentsLoading: false,
environments: envs,
selectedEnvironmentScope: '',
};
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
@ -15,13 +19,19 @@ describe('Ci environments dropdown', () => {
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
wrapper = mount(CiEnvironmentsDropdown, {
const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => {
wrapper = mountExtended(CiEnvironmentsDropdown, {
propsData: {
...defaultProps,
...props,
},
provide: {
glFeatures: {
ciLimitEnvironmentScope: enableFeatureFlag,
},
},
});
findListbox().vm.$emit('search', searchTerm);
@ -40,19 +50,32 @@ describe('Ci environments dropdown', () => {
});
describe('Search term is empty', () => {
beforeEach(() => {
createComponent({ props: { environments: envs } });
});
describe.each`
featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices
${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
`(
'when ciLimitEnvironmentScope feature flag is $flagStatus',
({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => {
beforeEach(() => {
createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag });
});
it('renders all environments when search term is empty', () => {
expect(findListboxItemByIndex(0).text()).toBe(envs[0]);
expect(findListboxItemByIndex(1).text()).toBe(envs[1]);
expect(findListboxItemByIndex(2).text()).toBe(envs[2]);
});
it(`${defaultEnvStatus} * in listbox`, () => {
expect(findListboxItemByIndex(0).text()).toBe(firstItemValue);
});
it('does not display active checkmark on the inactive stage', () => {
expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
});
it('renders all environments', () => {
expect(findListboxItemByIndex(envIndices[0]).text()).toBe(envs[0]);
expect(findListboxItemByIndex(envIndices[1]).text()).toBe(envs[1]);
expect(findListboxItemByIndex(envIndices[2]).text()).toBe(envs[2]);
});
it('does not display active checkmark', () => {
expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
});
},
);
});
describe('when `*` is the value of selectedEnvironmentScope props', () => {
@ -68,46 +91,91 @@ describe('Ci environments dropdown', () => {
});
});
describe('Environments found', () => {
describe('When ciLimitEnvironmentScope feature flag is disabled', () => {
const currentEnv = envs[2];
beforeEach(() => {
createComponent({ searchTerm: currentEnv });
createComponent();
});
it('renders only the environment searched for', () => {
it('filters on the frontend and renders only the environment searched for', async () => {
await findListbox().vm.$emit('search', currentEnv);
expect(findAllListboxItems()).toHaveLength(1);
expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
});
it('does not display create button', () => {
expect(findCreateWildcardButton().exists()).toBe(false);
it('does not emit event when searching', async () => {
expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
await findListbox().vm.$emit('search', currentEnv);
expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
});
describe('Custom events', () => {
describe('when selecting an environment', () => {
const itemIndex = 0;
it('does not display note about max environments shown', () => {
expect(findMaxEnvNote().exists()).toBe(false);
});
});
beforeEach(() => {
createComponent();
});
describe('When ciLimitEnvironmentScope feature flag is enabled', () => {
const currentEnv = envs[2];
it('emits `select-environment` when an environment is clicked', () => {
findListbox().vm.$emit('select', envs[itemIndex]);
expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
});
beforeEach(() => {
createComponent({ enableFeatureFlag: true });
});
it('renders environments passed down to it', async () => {
await findListbox().vm.$emit('search', currentEnv);
expect(findAllListboxItems()).toHaveLength(envs.length);
});
it('emits event when searching', async () => {
expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
await findListbox().vm.$emit('search', currentEnv);
expect(wrapper.emitted('search-environment-scope')).toHaveLength(2);
expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]);
});
it('renders loading icon while search query is loading', async () => {
createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } });
expect(findListbox().props('searching')).toBe(true);
});
it('displays note about max environments shown', () => {
expect(findMaxEnvNote().exists()).toBe(true);
});
});
describe('Custom events', () => {
describe('when selecting an environment', () => {
const itemIndex = 0;
beforeEach(() => {
createComponent();
});
describe('when creating a new environment from a search term', () => {
const search = 'new-env';
beforeEach(() => {
createComponent({ searchTerm: search });
});
it('emits `select-environment` when an environment is clicked', () => {
findListbox().vm.$emit('select', envs[itemIndex]);
it('emits create-environment-scope', () => {
findCreateWildcardButton().vm.$emit('click');
expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
});
expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
});
});
describe('when creating a new environment from a search term', () => {
const search = 'new-env';
beforeEach(() => {
createComponent({ searchTerm: search });
});
it('emits create-environment-scope', () => {
findCreateWildcardButton().vm.$emit('click');
expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
});
});
});

View File

@ -10,10 +10,12 @@ import {
EVENT_LABEL,
EVENT_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
groupString,
instanceString,
projectString,
variableOptions,
} from '~/ci/ci_variable_list/constants';
import { mockVariablesWithScopes } from '../mocks';
import { mockEnvs, mockVariablesWithScopes, mockVariablesWithUniqueScopes } from '../mocks';
import ModalStub from '../stubs';
describe('Ci variable modal', () => {
@ -42,12 +44,13 @@ describe('Ci variable modal', () => {
};
const defaultProps = {
areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
variable: [],
variables: [],
};
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
@ -349,6 +352,42 @@ describe('Ci variable modal', () => {
expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
describe('when feature flag is enabled', () => {
beforeEach(() => {
createComponent({
props: {
environments: mockEnvs,
variables: mockVariablesWithUniqueScopes(projectString),
},
provide: { glFeatures: { ciLimitEnvironmentScope: true } },
});
});
it('does not merge environment scope sources', () => {
const expectedLength = mockEnvs.length;
expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
});
});
describe('when feature flag is disabled', () => {
const mockGroupVariables = mockVariablesWithUniqueScopes(groupString);
beforeEach(() => {
createComponent({
props: {
environments: mockEnvs,
variables: mockGroupVariables,
},
});
});
it('merges environment scope sources', () => {
const expectedLength = mockGroupVariables.length + mockEnvs.length;
expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
});
});
});
describe('and section is hidden', () => {

View File

@ -1,4 +1,3 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
@ -16,6 +15,7 @@ describe('Ci variable table', () => {
let wrapper;
const defaultProps = {
areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
entity: 'project',
environments: mapEnvironmentNames(mockEnvs),
@ -54,10 +54,10 @@ describe('Ci variable table', () => {
it('passes props down correctly to the ci modal', async () => {
createComponent();
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props()).toEqual({
areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
@ -74,15 +74,13 @@ describe('Ci variable table', () => {
});
it('passes down ADD mode when receiving an empty variable', async () => {
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
it('passes down EDIT mode when receiving a variable', async () => {
findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
await nextTick();
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
@ -98,25 +96,21 @@ describe('Ci variable table', () => {
});
it('shows modal when adding a new variable', async () => {
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().exists()).toBe(true);
});
it('shows modal when updating a variable', async () => {
findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
await nextTick();
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().exists()).toBe(true);
});
it('hides modal when receiving the event from the modal', async () => {
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
await findCiVariableTable().vm.$emit('set-selected-variable');
findCiVariableModal().vm.$emit('hideModal');
await nextTick();
await findCiVariableModal().vm.$emit('hideModal');
expect(findCiVariableModal().exists()).toBe(false);
});
@ -133,11 +127,9 @@ describe('Ci variable table', () => {
${'update-variable'}
${'delete-variable'}
`('bubbles up the $eventName event', async ({ eventName }) => {
findCiVariableTable().vm.$emit('set-selected-variable');
await nextTick();
await findCiVariableTable().vm.$emit('set-selected-variable');
findCiVariableModal().vm.$emit(eventName, newVariable);
await nextTick();
await findCiVariableModal().vm.$emit(eventName, newVariable);
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
});
@ -154,10 +146,23 @@ describe('Ci variable table', () => {
${'handle-next-page'} | ${undefined}
${'sort-changed'} | ${{ sortDesc: true }}
`('bubbles up the $eventName event', async ({ args, eventName }) => {
findCiVariableTable().vm.$emit(eventName, args);
await nextTick();
await findCiVariableTable().vm.$emit(eventName, args);
expect(wrapper.emitted(eventName)).toEqual([[args]]);
});
});
describe('environment events', () => {
beforeEach(() => {
createComponent();
});
it('bubbles up the search event', async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
await findCiVariableModal().vm.$emit('search-environment-scope', 'staging');
expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
});
});
});

View File

@ -20,6 +20,7 @@ import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_v
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
ENVIRONMENT_QUERY_LIMIT,
UPDATE_MUTATION_ACTION,
environmentFetchErrorText,
genericMutationErrorText,
@ -111,11 +112,11 @@ describe('Ci Variable Shared Component', () => {
${true} | ${'enabled'}
${false} | ${'disabled'}
`('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
const featureFlagProvide = isVariablePagesEnabled
const pagesFeatureFlagProvide = isVariablePagesEnabled
? { glFeatures: { ciVariablesPages: true } }
: {};
describe('while queries are being fetch', () => {
describe('while queries are being fetched', () => {
beforeEach(() => {
createComponentWithApollo({ isLoading: true });
});
@ -133,7 +134,7 @@ describe('Ci Variable Shared Component', () => {
mockVariables.mockResolvedValue(mockProjectVariables);
await createComponentWithApollo({
provide: { ...createProjectProvide(), ...featureFlagProvide },
provide: { ...createProjectProvide(), ...pagesFeatureFlagProvide },
});
});
@ -163,7 +164,7 @@ describe('Ci Variable Shared Component', () => {
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
mockVariables.mockRejectedValue();
await createComponentWithApollo({ provide: featureFlagProvide });
await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
});
it('calls createAlert with the expected error message', () => {
@ -176,7 +177,7 @@ describe('Ci Variable Shared Component', () => {
mockEnvironments.mockRejectedValue();
mockVariables.mockResolvedValue(mockProjectVariables);
await createComponentWithApollo({ provide: featureFlagProvide });
await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
});
it('calls createAlert with the expected error message', () => {
@ -187,33 +188,91 @@ describe('Ci Variable Shared Component', () => {
describe('environment query', () => {
describe('when there is an environment key in queryData', () => {
beforeEach(async () => {
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
mockVariables.mockResolvedValue(mockProjectVariables);
beforeEach(() => {
mockEnvironments
.mockResolvedValueOnce(mockProjectEnvironments)
.mockResolvedValueOnce(mockProjectEnvironments);
mockVariables.mockResolvedValue(mockProjectVariables);
});
it('environments are fetched', async () => {
await createComponentWithApollo({
props: { ...createProjectProps() },
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
expect(mockEnvironments).toHaveBeenCalled();
});
describe('when Limit Environment Scope FF is enabled', () => {
beforeEach(async () => {
await createComponentWithApollo({
props: { ...createProjectProps() },
provide: {
glFeatures: {
ciLimitEnvironmentScope: true,
ciVariablesPages: isVariablePagesEnabled,
},
},
});
});
it('initial query is called with the correct variables', () => {
expect(mockEnvironments).toHaveBeenCalledWith({
first: ENVIRONMENT_QUERY_LIMIT,
fullPath: '/namespace/project/',
search: '',
});
});
it(`refetches environments when search term is present`, async () => {
expect(mockEnvironments).toHaveBeenCalledTimes(1);
expect(mockEnvironments).toHaveBeenCalledWith(expect.objectContaining({ search: '' }));
await findCiSettings().vm.$emit('search-environment-scope', 'staging');
expect(mockEnvironments).toHaveBeenCalledTimes(2);
expect(mockEnvironments).toHaveBeenCalledWith(
expect.objectContaining({ search: 'staging' }),
);
});
});
it('is executed', () => {
expect(mockVariables).toHaveBeenCalled();
describe('when Limit Environment Scope FF is disabled', () => {
beforeEach(async () => {
await createComponentWithApollo({
props: { ...createProjectProps() },
provide: pagesFeatureFlagProvide,
});
});
it('initial query is called with the correct variables', async () => {
expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' });
});
it(`does not refetch environments when search term is present`, async () => {
expect(mockEnvironments).toHaveBeenCalledTimes(1);
await findCiSettings().vm.$emit('search-environment-scope', 'staging');
expect(mockEnvironments).toHaveBeenCalledTimes(1);
});
});
});
describe('when there isnt an environment key in queryData', () => {
describe("when there isn't an environment key in queryData", () => {
beforeEach(async () => {
mockVariables.mockResolvedValue(mockGroupVariables);
await createComponentWithApollo({
props: { ...createGroupProps() },
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
});
it('is skipped', () => {
expect(mockVariables).not.toHaveBeenCalled();
it('fetching environments is skipped', () => {
expect(mockEnvironments).not.toHaveBeenCalled();
});
});
});
@ -227,7 +286,7 @@ describe('Ci Variable Shared Component', () => {
await createComponentWithApollo({
customHandlers: [[getGroupVariables, mockVariables]],
props: groupProps,
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
});
it.each`
@ -299,7 +358,7 @@ describe('Ci Variable Shared Component', () => {
await createComponentWithApollo({
customHandlers: [[getAdminVariables, mockVariables]],
props: createInstanceProps(),
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
});
@ -359,10 +418,11 @@ describe('Ci Variable Shared Component', () => {
await createComponentWithApollo({
customHandlers,
props,
provide: { ...provide, ...featureFlagProvide },
provide: { ...provide, ...pagesFeatureFlagProvide },
});
expect(findCiSettings().props()).toEqual({
areEnvironmentsLoading: false,
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
pageInfo: defaultProps.pageInfo,
@ -385,7 +445,7 @@ describe('Ci Variable Shared Component', () => {
`('when $bool it $text', async ({ bool }) => {
await createComponentWithApollo({
props: { ...createInstanceProps(), refetchAfterMutation: bool },
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
@ -418,7 +478,7 @@ describe('Ci Variable Shared Component', () => {
await createComponentWithApollo({
customHandlers: [[getGroupVariables, mockVariables]],
props: { ...createGroupProps() },
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
} catch (e) {
error = e;
@ -433,7 +493,7 @@ describe('Ci Variable Shared Component', () => {
await createComponentWithApollo({
customHandlers: [[getGroupVariables, mockVariables]],
props: { ...createGroupProps(), queryData: { wrongKey: {} } },
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
} catch (e) {
error = e;
@ -455,7 +515,7 @@ describe('Ci Variable Shared Component', () => {
try {
await createComponentWithApollo({
props: { ...createGroupProps() },
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
} catch (e) {
error = e;
@ -469,7 +529,7 @@ describe('Ci Variable Shared Component', () => {
try {
await createComponentWithApollo({
props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
provide: featureFlagProvide,
provide: pagesFeatureFlagProvide,
});
} catch (e) {
error = e;

View File

@ -56,6 +56,11 @@ export const mockVariablesWithScopes = (kind) =>
return { ...variable, environmentScope: '*' };
});
export const mockVariablesWithUniqueScopes = (kind) =>
mockVariables(kind).map((variable) => {
return { ...variable, environmentScope: variable.value };
});
const createDefaultVars = ({ withScope = true, kind } = {}) => {
let base = mockVariables(kind);

View File

@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import Cookies from '~/lib/utils/cookies';
import waitForPromises from 'helpers/wait_for_promises';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
@ -24,6 +25,7 @@ import {
} from '~/lib/utils/http_status';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import diffsEventHub from '~/diffs/event_hub';
import { diffMetadata } from '../mock_data/diff_metadata';
jest.mock('~/alert');
@ -135,6 +137,112 @@ describe('DiffsStoreActions', () => {
});
});
describe('fetchFileByFile', () => {
beforeEach(() => {
window.location.hash = 'e334a2a10f036c00151a04cea7938a5d4213a818';
});
it('should do nothing if there is no tree entry for the file ID', () => {
return testAction(diffActions.fetchFileByFile, {}, { flatBlobsList: [] }, [], []);
});
it('should do nothing if the tree entry for the file ID has already been marked as loaded', () => {
return testAction(
diffActions.fetchFileByFile,
{},
{
flatBlobsList: [
{ fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', diffLoaded: true },
],
},
[],
[],
);
});
describe('when a tree entry exists for the file, but it has not been marked as loaded', () => {
let state;
let commit;
let hubSpy;
const endpointDiffForPath = '/diffs/set/endpoint/path';
const diffForPath = mergeUrlParams(
{
old_path: 'old/123',
new_path: 'new/123',
w: '1',
view: 'inline',
},
endpointDiffForPath,
);
const treeEntry = {
fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
filePaths: { old: 'old/123', new: 'new/123' },
};
const fileResult = {
diff_files: [{ file_hash: 'e334a2a10f036c00151a04cea7938a5d4213a818' }],
};
const getters = {
flatBlobsList: [treeEntry],
getDiffFileByHash(hash) {
return state.diffFiles?.find((entry) => entry.file_hash === hash);
},
};
beforeEach(() => {
commit = jest.fn();
state = {
endpointDiffForPath,
diffFiles: [],
};
getters.flatBlobsList = [treeEntry];
hubSpy = jest.spyOn(diffsEventHub, '$emit');
});
it('does nothing if the file already exists in the loaded diff files', () => {
state.diffFiles = fileResult.diff_files;
return testAction(diffActions.fetchFileByFile, state, getters, [], []);
});
it('does some standard work every time', async () => {
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
await diffActions.fetchFileByFile({ state, getters, commit });
expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loading');
expect(commit).toHaveBeenCalledWith(types.SET_RETRIEVING_BATCHES, true);
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(commit).toHaveBeenCalledWith(types.SET_DIFF_DATA_BATCH, fileResult);
expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loaded');
expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
});
it.each`
urlHash | diffFiles | expected
${treeEntry.fileHash} | ${[]} | ${''}
${'abcdef1234567890'} | ${fileResult.diff_files} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
`(
"sets the current file to the first diff file ('$id') if it's not a note hash and there isn't a current ID set",
async ({ urlHash, diffFiles, expected }) => {
window.location.hash = urlHash;
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
state.diffFiles = diffFiles;
await diffActions.fetchFileByFile({ state, getters, commit });
// wait for the mocked network request to return and start processing the .then
await waitForPromises();
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, expected);
},
);
});
});
describe('fetchDiffFilesBatch', () => {
it('should fetch batch diff files', () => {
const endpointBatch = '/fetch/diffs_batch';

View File

@ -1,5 +1,5 @@
<div id="oauth-container">
<input id="remember_me" type="checkbox" />
<input id="remember_me_omniauth" type="checkbox" />
<form method="post" action="http://example.com/">
<button class="js-oauth-login twitter" type="submit">

View File

@ -1,14 +1,20 @@
import { nextTick } from 'vue';
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
const { edit, ...VIEW_OBJECTS_WITHOUT_EDIT } = leftSidebarViews;
const MODES_WITHOUT_EDIT = Object.keys(VIEW_OBJECTS_WITHOUT_EDIT);
const MODES = Object.keys(leftSidebarViews);
describe('IDE ActivityBar component', () => {
let wrapper;
let store;
const findChangesBadge = () => wrapper.findComponent(GlBadge);
const findModeButton = (mode) => wrapper.findByTestId(`${mode}-mode-button`);
const mountComponent = (state) => {
store = createStore();
@ -19,45 +25,43 @@ describe('IDE ActivityBar component', () => {
...state,
});
wrapper = shallowMount(ActivityBar, { store });
wrapper = shallowMountExtended(ActivityBar, { store });
};
describe('updateActivityBarView', () => {
beforeEach(() => {
mountComponent();
jest.spyOn(wrapper.vm, 'updateActivityBarView').mockImplementation(() => {});
});
it('calls updateActivityBarView with edit value on click', () => {
wrapper.find('.js-ide-edit-mode').trigger('click');
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
});
it('calls updateActivityBarView with commit value on click', () => {
wrapper.find('.js-ide-commit-mode').trigger('click');
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
});
it('calls updateActivityBarView with review value on click', () => {
wrapper.find('.js-ide-review-mode').trigger('click');
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
});
});
describe('active item', () => {
it('sets edit item active', () => {
mountComponent();
// Test that mode button does not have 'active' class before click,
// and does have 'active' class after click
const testSettingActiveItem = async (mode) => {
const button = findModeButton(mode);
expect(wrapper.find('.js-ide-edit-mode').classes()).toContain('active');
expect(button.classes('active')).toBe(false);
button.trigger('click');
await nextTick();
expect(button.classes('active')).toBe(true);
};
it.each(MODES)('is initially set to %s mode', (mode) => {
mountComponent({ currentActivityView: leftSidebarViews[mode].name });
const button = findModeButton(mode);
expect(button.classes('active')).toBe(true);
});
it('sets commit item active', () => {
mountComponent({ currentActivityView: leftSidebarViews.commit.name });
it.each(MODES_WITHOUT_EDIT)('is correctly set after clicking %s mode button', async (mode) => {
mountComponent();
expect(wrapper.find('.js-ide-commit-mode').classes()).toContain('active');
testSettingActiveItem(mode);
});
it('is correctly set after clicking edit mode button', async () => {
// The default currentActivityView is leftSidebarViews.edit.name,
// so for the 'edit' mode, we pass a different currentActivityView.
mountComponent({ currentActivityView: leftSidebarViews.review.name });
testSettingActiveItem('edit');
});
});
@ -65,7 +69,6 @@ describe('IDE ActivityBar component', () => {
it('is rendered when files are staged', () => {
mountComponent({ stagedFiles: [{ path: '/path/to/file' }] });
expect(findChangesBadge().exists()).toBe(true);
expect(findChangesBadge().text()).toBe('1');
});

View File

@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { keepAlive } from 'helpers/keep_alive_component_helper';
import { viewerTypes } from '~/ide/constants';
import IdeTree from '~/ide/components/ide_tree.vue';
import { createStore } from '~/ide/stores';
import { createStoreOptions } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
@ -13,42 +13,72 @@ describe('IdeTree', () => {
let store;
let wrapper;
beforeEach(() => {
store = createStore();
const actionSpies = {
updateViewer: jest.fn(),
};
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'main';
store.state.projects.abcproject = { ...projectData };
Vue.set(store.state.trees, 'abcproject/main', {
tree: [file('fileName')],
loading: false,
const testState = {
currentProjectId: 'abcproject',
currentBranchId: 'main',
projects: {
abcproject: { ...projectData },
},
trees: {
'abcproject/main': {
tree: [file('fileName')],
loading: false,
},
},
};
const createComponent = (replaceState) => {
const defaultStore = createStoreOptions();
store = new Vuex.Store({
...defaultStore,
state: {
...defaultStore.state,
...testState,
replaceState,
},
actions: {
...defaultStore.actions,
...actionSpies,
},
});
wrapper = mount(keepAlive(IdeTree), {
wrapper = mount(IdeTree, {
store,
});
};
beforeEach(() => {
createComponent();
});
it('renders list of files', () => {
expect(wrapper.text()).toContain('fileName');
afterEach(() => {
actionSpies.updateViewer.mockClear();
});
describe('renders properly', () => {
it('renders list of files', () => {
expect(wrapper.text()).toContain('fileName');
});
});
describe('activated', () => {
let inititializeSpy;
beforeEach(async () => {
inititializeSpy = jest.spyOn(wrapper.findComponent(IdeTree).vm, 'initialize');
store.state.viewer = 'diff';
await wrapper.vm.reactivate();
beforeEach(() => {
createComponent({
viewer: viewerTypes.diff,
});
});
it('re initializes the component', () => {
expect(inititializeSpy).toHaveBeenCalled();
expect(actionSpies.updateViewer).toHaveBeenCalled();
});
it('updates viewer to "editor" by default', () => {
expect(store.state.viewer).toBe('editor');
expect(actionSpies.updateViewer).toHaveBeenCalledWith(expect.any(Object), viewerTypes.edit);
});
});
});

View File

@ -17,19 +17,16 @@ describe('OAuthRememberMe', () => {
resetHTMLFixture();
});
it('adds the "remember_me" query parameter to all OAuth login buttons', () => {
$('#oauth-container #remember_me').click();
it('adds and removes the "remember_me" query parameter from all OAuth login buttons', () => {
$('#oauth-container #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.github')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.facebook')).toBe(
'http://example.com/?redirect_fragment=L1&remember_me=1',
);
});
it('removes the "remember_me" query parameter from all OAuth login buttons', () => {
$('#oauth-container #remember_me').click();
$('#oauth-container #remember_me').click();
$('#oauth-container #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/');
expect(findFormAction('.github')).toBe('http://example.com/');

View File

@ -2,43 +2,130 @@
exports[`Saved replies list item component renders list item 1`] = `
<li
class="gl-mb-5"
class="gl-pt-4 gl-pb-5 gl-border-b"
>
<div
class="gl-display-flex gl-align-items-center"
>
<strong
<h6
class="gl-mr-3 gl-my-0"
data-testid="saved-reply-name"
>
test
</strong>
</h6>
<div
class="gl-ml-auto"
>
<gl-button-stub
aria-label="Edit"
buttontextclasses=""
category="primary"
class="gl-mr-3"
data-testid="saved-reply-edit-btn"
icon="pencil"
size="medium"
title="Edit"
to="[object Object]"
variant="default"
/>
<div
class="gl-new-dropdown gl-disclosure-dropdown"
>
<button
aria-controls="base-dropdown-5"
aria-labelledby="actions-toggle-3"
class="btn btn-default btn-md gl-button btn-default-tertiary gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
data-testid="base-dropdown-toggle"
id="actions-toggle-3"
listeners="[object Object]"
type="button"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_v-icon"
role="img"
>
<use
href="#ellipsis_v"
/>
</svg>
<span
class="gl-button-text"
>
<span
class="gl-new-dropdown-button-text gl-sr-only"
>
Saved reply actions
</span>
<!---->
</span>
</button>
<div
class="gl-new-dropdown-panel"
data-testid="base-dropdown-menu"
id="base-dropdown-5"
>
<div
class="gl-new-dropdown-inner"
>
<ul
aria-labelledby="actions-toggle-3"
class="gl-new-dropdown-contents"
data-testid="disclosure-content"
id="disclosure-4"
tabindex="-1"
>
<li
class="gl-new-dropdown-item"
data-testid="disclosure-dropdown-item"
tabindex="0"
>
<button
class="gl-new-dropdown-item-content"
data-testid="saved-reply-edit-btn"
tabindex="-1"
type="button"
>
<span
class="gl-new-dropdown-item-text-wrapper"
>
Edit
</span>
</button>
</li>
<li
class="gl-new-dropdown-item"
data-testid="disclosure-dropdown-item"
tabindex="0"
>
<button
class="gl-new-dropdown-item-content gl-text-red-500!"
data-testid="saved-reply-delete-btn"
tabindex="-1"
type="button"
>
<span
class="gl-new-dropdown-item-text-wrapper"
>
Delete
</span>
</button>
</li>
</ul>
</div>
</div>
</div>
<gl-button-stub
aria-label="Delete"
buttontextclasses=""
category="secondary"
data-testid="saved-reply-delete-btn"
icon="remove"
size="medium"
title="Delete"
variant="danger"
/>
<div
class="gl-tooltip"
>
Saved reply actions
</div>
</div>
</div>
@ -48,20 +135,6 @@ exports[`Saved replies list item component renders list item 1`] = `
/assign_reviewer
</div>
<gl-modal-stub
actionprimary="[object Object]"
actionsecondary="[object Object]"
arialabel=""
dismisslabel="Close"
modalclass=""
modalid="delete-saved-reply-2"
size="sm"
title="Delete saved reply"
titletag="h4"
>
<gl-sprintf-stub
message="Are you sure you want to delete %{name}? This action cannot be undone."
/>
</gl-modal-stub>
<!---->
</li>
`;

View File

@ -1,50 +1,154 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import ListItem from '~/saved_replies/components/list_item.vue';
import deleteSavedReplyMutation from '~/saved_replies/queries/delete_saved_reply.mutation.graphql';
let wrapper;
let deleteSavedReplyMutationResponse;
function createComponent(propsData = {}) {
function createMockApolloProvider(requestHandlers = [deleteSavedReplyMutation]) {
Vue.use(VueApollo);
deleteSavedReplyMutationResponse = jest
.fn()
.mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
return shallowMount(ListItem, {
propsData,
directives: {
GlModal: createMockDirective('gl-modal'),
},
apolloProvider: createMockApollo([
[deleteSavedReplyMutation, deleteSavedReplyMutationResponse],
]),
});
return createMockApollo([requestHandlers]);
}
describe('Saved replies list item component', () => {
let wrapper;
let $router;
function createComponent(propsData = {}, apolloProvider = createMockApolloProvider) {
$router = {
push: jest.fn(),
};
return mount(ListItem, {
propsData,
directives: {
GlModal: createMockDirective('gl-modal'),
},
apolloProvider,
mocks: {
$router,
},
});
}
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findModal = () => wrapper.findComponent(GlModal);
it('renders list item', async () => {
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
expect(wrapper.element).toMatchSnapshot();
});
describe('delete button', () => {
it('calls Apollo mutate', async () => {
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer', id: 1 } });
describe('saved reply actions dropdown', () => {
beforeEach(() => {
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
});
wrapper.findComponent(GlModal).vm.$emit('primary');
it('exists', () => {
expect(findDropdown().exists()).toBe(true);
});
await waitForPromises();
it('has correct toggle text', () => {
expect(findDropdown().props('toggleText')).toBe(__('Saved reply actions'));
});
it('has correct amount of dropdown items', () => {
const items = findDropdownItems();
expect(items.exists()).toBe(true);
expect(items).toHaveLength(2);
});
describe('edit option', () => {
it('exists', () => {
const items = findDropdownItems();
const editItem = items.filter((item) => item.text() === __('Edit'));
expect(editItem.exists()).toBe(true);
});
it('shows as first dropdown item', () => {
const items = findDropdownItems();
expect(items.at(0).text()).toBe(__('Edit'));
});
});
describe('delete option', () => {
it('exists', () => {
const items = findDropdownItems();
const deleteItem = items.filter((item) => item.text() === __('Delete'));
expect(deleteItem.exists()).toBe(true);
});
it('shows as first dropdown item', () => {
const items = findDropdownItems();
expect(items.at(1).text()).toBe(__('Delete'));
});
});
});
describe('Delete modal', () => {
let deleteSavedReplyMutationResponse;
beforeEach(() => {
deleteSavedReplyMutationResponse = jest
.fn()
.mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
const apolloProvider = createMockApolloProvider([
deleteSavedReplyMutation,
deleteSavedReplyMutationResponse,
]);
wrapper = createComponent(
{ reply: { name: 'test', content: '/assign_reviewer', id: 1 } },
apolloProvider,
);
});
it('exists', () => {
expect(findModal().exists()).toBe(true);
});
it('has correct title', () => {
expect(findModal().props('title')).toBe(__('Delete saved reply'));
});
it('delete button calls Apollo mutate', async () => {
await findModal().vm.$emit('primary');
expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 });
});
it('cancel button does not trigger Apollo mutation', async () => {
await findModal().vm.$emit('secondary');
expect(deleteSavedReplyMutationResponse).not.toHaveBeenCalled();
});
});
describe('Dropdown Edit', () => {
beforeEach(() => {
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
});
it('click triggers router push', async () => {
const editComponent = findDropdownItems().at(0);
await editComponent.find('button').trigger('click');
expect($router.push).toHaveBeenCalled();
});
});
});

View File

@ -10,7 +10,7 @@ RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composi
before do
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
stub_licensed_features(ci_private_catalog: false)
stub_licensed_features(ci_namespace_catalog: false)
end
it 'user cannot view the Catalog in CE regardless of permissions' do

View File

@ -77,7 +77,9 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
before do
allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'))
allow(YAML).to receive(:safe_load_file).with(
File.join(Gitlab.config.backup.path, 'backup_information.yml'),
permitted_classes: described_class::YAML_PERMITTED_CLASSES)
.and_return(backup_information)
end
@ -603,14 +605,16 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
expect(Kernel).not_to have_received(:system).with(*pack_tar_cmdline)
expect(YAML.load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'))).to include(
backup_created_at: backup_time.localtime,
db_version: be_a(String),
gitlab_version: Gitlab::VERSION,
installation_type: Gitlab::INSTALLATION_TYPE,
skipped: 'tar',
tar_version: be_a(String)
)
expect(YAML.safe_load_file(
File.join(Gitlab.config.backup.path, 'backup_information.yml'),
permitted_classes: described_class::YAML_PERMITTED_CLASSES)).to include(
backup_created_at: backup_time.localtime,
db_version: be_a(String),
gitlab_version: Gitlab::VERSION,
installation_type: Gitlab::INSTALLATION_TYPE,
skipped: 'tar',
tar_version: be_a(String)
)
expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp'))
end
end
@ -629,8 +633,10 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
end
before do
allow(YAML).to receive(:load_file).and_call_original
allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'))
allow(YAML).to receive(:safe_load_file).and_call_original
allow(YAML).to receive(:safe_load_file).with(
File.join(Gitlab.config.backup.path, 'backup_information.yml'),
permitted_classes: described_class::YAML_PERMITTED_CLASSES)
.and_return(backup_information)
end
@ -892,12 +898,13 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
.with(a_string_matching('Non tarred backup found '))
expect(progress).to have_received(:puts)
.with(a_string_matching("Backup #{backup_id} is done"))
expect(YAML.load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'))).to include(
backup_created_at: backup_time,
full_backup_id: full_backup_id,
gitlab_version: Gitlab::VERSION,
skipped: 'something,tar'
)
expect(YAML.safe_load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'),
permitted_classes: described_class::YAML_PERMITTED_CLASSES)).to include(
backup_created_at: backup_time,
full_backup_id: full_backup_id,
gitlab_version: Gitlab::VERSION,
skipped: 'something,tar'
)
end
context 'on version mismatch' do
@ -943,7 +950,8 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
allow(Gitlab::BackupLogger).to receive(:info)
allow(task1).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'task1.tar.gz'))
allow(task2).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz'))
allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'))
allow(YAML).to receive(:safe_load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'),
permitted_classes: described_class::YAML_PERMITTED_CLASSES)
.and_return(backup_information)
allow(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
allow(Rake::Task['cache:clear']).to receive(:invoke)

View File

@ -13,10 +13,6 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
let(:response_double) { double }
let(:version) { '14.0.0' }
before do
stub_const('BulkImports::MINIMUM_COMPATIBLE_MAJOR_VERSION', version)
end
describe 'source instance validation' do
before do
allow(graphql_client_double).to receive(:execute)
@ -37,7 +33,7 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
let(:version) { '13.0.0' }
it 'raises an error' do
expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab version. Source instance must run GitLab version #{BulkImport::MIN_MAJOR_VERSION} or later.")
expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab version. Minimum supported version is 14.")
end
end
end

View File

@ -93,6 +93,9 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}
end
let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[:entities][0][:source_type]) }
let(:source_entity_identifier) { ERB::Util.url_encode(params[:entities][0][:source_full_path]) }
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
allow(instance)
@ -103,6 +106,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
.to receive(:instance_enterprise)
.and_return(false)
end
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
.to_return(status: 200, body: "", headers: {})
end
shared_examples 'starting a new migration' do
@ -271,12 +276,41 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}
end
it 'returns blocked url error' do
it 'returns blocked url message in the error' do
request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Validation failed: Url is blocked: Only allowed schemes are http, https')
expect(json_response['message']).to include("Url is blocked: Only allowed schemes are http, https")
end
end
context 'when source instance setting is disabled' do
let(:params) do
{
configuration: {
url: 'http://gitlab.example',
access_token: 'access_token'
},
entities: [
source_type: 'group_entity',
source_full_path: 'full_path',
destination_slug: 'destination_slug',
destination_namespace: 'destination_namespace'
]
}
end
it 'returns blocked url error' do
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
.to_return(status: 404, body: "", headers: {})
request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to include("Group import disabled on source or destination instance. " \
"Ask an administrator to enable it on both instances and try again.")
end
end

View File

@ -35,6 +35,9 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
]
end
let(:source_entity_identifier) { ERB::Util.url_encode(params[0][:source_full_path]) }
let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[0][:source_type]) }
subject { described_class.new(user, params, credentials) }
describe '#execute' do
@ -59,6 +62,34 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
end
end
context 'when direct transfer setting query returns a 404' do
it 'raises a ServiceResponse::Error' do
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token')
.to_return(
status: 200,
body: source_version.to_json,
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
.to_return(status: 404)
expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
expect(client).to receive(:get).and_raise(BulkImports::Error.setting_not_enabled)
end
result = subject.execute
expect(result).to be_a(ServiceResponse)
expect(result).to be_error
expect(result.message)
.to eq(
"Group import disabled on source or destination instance. " \
"Ask an administrator to enable it on both instances and try again."
)
end
end
context 'when required scopes are not present' do
it 'returns ServiceResponse with error if token does not have api scope' do
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
@ -68,9 +99,13 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
body: source_version.to_json,
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
.to_return(
status: 200
)
allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
allow(client).to receive(:validate_instance_version!).and_raise(BulkImports::Error.scope_validation_failure)
allow(client).to receive(:validate_import_scopes!).and_raise(BulkImports::Error.scope_validation_failure)
end
result = subject.execute
@ -90,6 +125,10 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token')
.to_return(status: 200, body: source_version.to_json, headers: { 'Content-Type' => 'application/json' })
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
.to_return(
status: 200
)
stub_request(:get, 'http://gitlab.example/api/v4/personal_access_tokens/self?private_token=token')
.to_return(
status: 200,
@ -169,6 +208,10 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
allow(instance).to receive(:instance_version).and_return(source_version)
allow(instance).to receive(:instance_enterprise).and_return(false)
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
.to_return(
status: 200
)
end
end
@ -325,6 +368,105 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
end
end
describe '.validate_setting_enabled!' do
let(:entity_source_id) { 'gid://gitlab/Model/12345' }
let(:graphql_client) { instance_double(BulkImports::Clients::Graphql) }
let(:http_client) { instance_double(BulkImports::Clients::HTTP) }
let(:http_response) { double(code: 200, success?: true) } # rubocop:disable RSpec/VerifiedDoubles
before do
allow(BulkImports::Clients::HTTP).to receive(:new).and_return(http_client)
allow(BulkImports::Clients::Graphql).to receive(:new).and_return(graphql_client)
allow(http_client).to receive(:instance_version).and_return(status: 200)
allow(http_client).to receive(:instance_enterprise).and_return(false)
allow(http_client).to receive(:validate_instance_version!).and_return(source_version)
allow(http_client).to receive(:validate_import_scopes!).and_return(true)
end
context 'when the source_type is a group' do
context 'when the source_full_path contains only integer characters' do
let(:query_string) { BulkImports::Groups::Graphql::GetGroupQuery.new(context: nil).to_s }
let(:graphql_response) do
double(original_hash: { 'data' => { 'group' => { 'id' => entity_source_id } } }) # rubocop:disable RSpec/VerifiedDoubles
end
let(:params) do
[
{
source_type: 'group_entity',
source_full_path: '67890',
destination_slug: 'destination-group-1',
destination_namespace: 'destination1'
}
]
end
before do
allow(graphql_client).to receive(:parse).with(query_string)
allow(graphql_client).to receive(:execute).and_return(graphql_response)
allow(http_client).to receive(:get)
.with("/groups/12345/export_relations/status")
.and_return(http_response)
stub_request(:get, "http://gitlab.example/api/v4/groups/12345/export_relations/status?page=1&per_page=30&private_token=token")
.to_return(status: 200, body: "", headers: {})
end
it 'makes a graphql request using the group full path and an http request with the correct id' do
expect(graphql_client).to receive(:parse).with(query_string)
expect(graphql_client).to receive(:execute).and_return(graphql_response)
expect(http_client).to receive(:get).with("/groups/12345/export_relations/status")
subject.execute
end
end
end
context 'when the source_type is a project' do
context 'when the source_full_path contains only integer characters' do
let(:query_string) { BulkImports::Projects::Graphql::GetProjectQuery.new(context: nil).to_s }
let(:graphql_response) do
double(original_hash: { 'data' => { 'project' => { 'id' => entity_source_id } } }) # rubocop:disable RSpec/VerifiedDoubles
end
let(:params) do
[
{
source_type: 'project_entity',
source_full_path: '67890',
destination_slug: 'destination-group-1',
destination_namespace: 'destination1'
}
]
end
before do
allow(graphql_client).to receive(:parse).with(query_string)
allow(graphql_client).to receive(:execute).and_return(graphql_response)
allow(http_client).to receive(:get)
.with("/projects/12345/export_relations/status")
.and_return(http_response)
stub_request(:get, "http://gitlab.example/api/v4/projects/12345/export_relations/status?page=1&per_page=30&private_token=token")
.to_return(status: 200, body: "", headers: {})
end
it 'makes a graphql request using the group full path and an http request with the correct id' do
expect(graphql_client).to receive(:parse).with(query_string)
expect(graphql_client).to receive(:execute).and_return(graphql_response)
expect(http_client).to receive(:get).with("/projects/12345/export_relations/status")
subject.execute
end
end
end
end
describe '.validate_destination_full_path' do
context 'when the source_type is a group' do
context 'when the provided destination_slug already exists in the destination_namespace' do

View File

@ -116,7 +116,7 @@ module LoginHelpers
visit new_user_session_path
expect(page).to have_content('Sign in with')
check 'remember_me' if remember_me
check 'remember_me_omniauth' if remember_me
click_button "oauth-login-#{provider}"
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
RSpec.shared_examples 'an "Explore" page with sidebar and breadcrumbs' do |page_path, menu_label|
before do
visit send(page_path)
end
let(:sidebar_css) { 'aside.nav-sidebar[aria-label="Explore"]' }
let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" }
it 'shows the "Explore" sidebar' do
expect(page).to have_css(sidebar_css)
end
it 'shows the correct sidebar menu item as active' do
within(sidebar_css) do
expect(page).to have_css(active_menu_item_css)
end
end
describe 'breadcrumbs' do
it 'has "Explore" as its root breadcrumb' do
within '.breadcrumbs-list' do
expect(page).to have_css("li:first a[href=\"#{explore_root_path}\"]", text: 'Explore')
end
end
end
end

View File

@ -107,7 +107,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
with_them do
before do
allow(Kernel).to receive(:system).and_return(true)
allow(YAML).to receive(:load_file).and_return({ gitlab_version: Gitlab::VERSION })
allow(YAML).to receive(:safe_load_file).and_return({ gitlab_version: Gitlab::VERSION })
allow(File).to receive(:delete).with(backup_restore_pid_path).and_return(1)
allow(File).to receive(:open).and_call_original
allow(File).to receive(:open).with(backup_restore_pid_path, any_args).and_yield(pid_file)
@ -158,7 +158,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
context 'when restore matches gitlab version' do
before do
allow(YAML).to receive(:load_file)
allow(YAML).to receive(:safe_load_file)
.and_return({ gitlab_version: gitlab_version })
expect_next_instance_of(::Backup::Manager) do |instance|
backup_types.each do |subtask|
@ -212,7 +212,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
allow(Kernel).to receive(:system).and_return(true)
allow(FileUtils).to receive(:cp_r).and_return(true)
allow(FileUtils).to receive(:mv).and_return(true)
allow(YAML).to receive(:load_file)
allow(YAML).to receive(:safe_load_file)
.and_return({ gitlab_version: Gitlab::VERSION })
expect_next_instance_of(::Backup::Manager) do |instance|

View File

@ -8,9 +8,10 @@ require 'logger'
module CloudProfilerAgent
GoogleCloudProfiler = ::Google::Cloud::Profiler::V2
# We temporarily removed wall time profiling here, since it does not work properly.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/397060
PROFILE_TYPES = {
CPU: :cpu,
WALL: :wall
CPU: :cpu
}.freeze
# This regexp will ensure the service name is valid.
# See https://cloud.google.com/ruby/docs/reference/google-cloud-profiler-v2/latest/Google-Cloud-Profiler-V2-Deployment#Google__Cloud__Profiler__V2__Deployment_target_instance_

View File

@ -1122,15 +1122,15 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
"@gitlab/svgs@3.26.0":
version "3.26.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.26.0.tgz#0324301f2aada66c39259ff050a567947db34515"
integrity sha512-tidak1UyCsrJ2jylybChNjJNnXUSoQ7rLzxMV2NJ/l2JiDDG7Bh8gn2CL2gk2icWa4Z2/DUfu2EhAAtdJtE0fQ==
"@gitlab/svgs@3.28.0":
version "3.28.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.28.0.tgz#e7f49c5d2144b3a4e8edbe366731f0a054d01ec5"
integrity sha512-H4m9jeZEByIp7k2U3aCgM+wNF4I681JEhpUtWsnxaa8vsX2K/maBD8/q7M2hXrTBLAeisGldN12pLfJRRZr/QQ==
"@gitlab/ui@58.2.0":
version "58.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-58.2.0.tgz#3fa8f34ab77783feba0d71abd8db644322796c4c"
integrity sha512-LWuNTzLd7+FoZxFt5i3C3/0hVOVIlnZbAZgQ6WwDbTlFn1f/slN7B5L89kleVE1OkgLLoqFuYwRUDPI0eySmvQ==
"@gitlab/ui@58.2.1":
version "58.2.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-58.2.1.tgz#5bd4889c2c6e32ba56a8766083c3d34b441e6288"
integrity sha512-4OmhjVZhIYL150pCZOK14dT8X9wJKkrVw8L4KlYCJ+iSoodoFt2s1h2eE0zQ4O0zZ1BMN++augb26bWnWo8cqQ==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.23.1"