Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-09-08 15:09:24 +00:00
parent 73e7987ee3
commit 132b8909c8
85 changed files with 960 additions and 701 deletions

View File

@ -1,6 +1,5 @@
---
Capybara/TestidFinders:
Details: grace period
Exclude:
- 'ee/spec/features/admin/admin_dev_ops_reports_spec.rb'
- 'ee/spec/features/admin/admin_merge_requests_approvals_spec.rb'

View File

@ -1,6 +1,5 @@
---
Cop/IgnoredColumns:
Details: grace period
Exclude:
- 'app/models/loose_foreign_keys/deleted_record.rb'
- 'ee/lib/ee/gitlab/background_migration/create_vulnerability_links.rb'

View File

@ -1,7 +1,6 @@
---
# Cop supports --autocorrect.
FactoryBot/CreateList:
Details: grace period
Exclude:
- 'ee/spec/controllers/ee/search_controller_spec.rb'
- 'ee/spec/controllers/projects/licenses_controller_spec.rb'

View File

@ -1,7 +1,6 @@
---
# Cop supports --autocorrect.
Gitlab/StrongMemoizeAttr:
Details: grace period
Exclude:
- 'app/components/pajamas/avatar_component.rb'
- 'app/controllers/application_controller.rb'

View File

@ -1,6 +1,5 @@
---
InternalAffairs/UseRestrictOnSend:
Details: grace period
Exclude:
- 'rubocop/cop/gitlab/feature_available_usage.rb'
- 'rubocop/cop/migration/add_concurrent_foreign_key.rb'

View File

@ -1,7 +1,6 @@
---
# Cop supports --autocorrect.
Lint/AssignmentInCondition:
Details: grace period
Exclude:
- 'app/controllers/concerns/uploads_actions.rb'
- 'app/controllers/concerns/verifies_with_email.rb'

View File

@ -1,7 +1,6 @@
---
# Cop supports --autocorrect.
Lint/RedundantSafeNavigation:
Details: grace period
Exclude:
- 'app/controllers/import/base_controller.rb'
- 'app/graphql/resolvers/users_resolver.rb'

View File

@ -1,7 +1,6 @@
---
# Cop supports --autocorrect.
Lint/RedundantStringCoercion:
Details: grace period
Exclude:
- 'ee/bin/geo_log_cursor'
- 'ee/db/fixtures/development/31_devops_adoption.rb'

View File

@ -1,6 +1,5 @@
---
Migration/AvoidFinalizeBackgroundMigration:
Details: grace period
Exclude:
- 'db/post_migrate/20220502015011_clean_up_fix_merge_request_diff_commit_users.rb'
- 'db/post_migrate/20220525131557_cleanup_backfill_integrations_enable_ssl_verification.rb'

View File

@ -1,7 +1,6 @@
---
# Cop supports --autocorrect.
Performance/RegexpMatch:
Details: grace period
Exclude:
- 'config/initializers/wikicloth_redos_patch.rb'
- 'ee/app/controllers/concerns/audit_events/enforces_valid_date_params.rb'

View File

@ -1,6 +1,5 @@
---
Rails/OutputSafety:
Details: grace period
Exclude:
- 'app/controllers/concerns/confirm_email_warning.rb'
- 'app/controllers/concerns/web_hooks/hook_actions.rb'

View File

@ -1,6 +1,5 @@
---
# Cop supports --autocorrect.
RSpec/BeforeAll:
Details: grace period
Exclude:
- 'ee/spec/support/shared_examples/finders/security/findings_finder_shared_examples.rb'

View File

@ -1,6 +1,5 @@
---
RSpec/BeforeAllRoleAssignment:
Details: grace period
Exclude:
- 'ee/spec/components/namespaces/free_user_cap/non_owner_notification_alert_component_spec.rb'
- 'ee/spec/components/namespaces/free_user_cap/notification_alert_component_spec.rb'

View File

@ -1,6 +1,5 @@
---
RSpec/ContextWording:
Details: grace period
Exclude:
- 'ee/spec/controllers/admin/application_settings_controller_spec.rb'
- 'ee/spec/controllers/admin/audit_logs_controller_spec.rb'

View File

@ -1,6 +1,5 @@
---
RSpec/FactoryBot/ExcessiveCreateList:
Details: grace period
Exclude:
- 'ee/spec/controllers/groups/hooks_controller_spec.rb'
- 'ee/spec/features/search/elastic/global_search_spec.rb'

View File

@ -1,6 +1,5 @@
---
RSpec/UselessDynamicDefinition:
Details: grace period
Exclude:
- 'ee/spec/factories/ci/builds.rb'
- 'ee/spec/factories/ci/job_artifacts.rb'

View File

@ -1,7 +1,6 @@
---
# Cop supports --autocorrect.
Style/RedundantParentheses:
Details: grace period
Exclude:
- 'spec/graphql/types/ci/job_kind_enum_spec.rb'
- 'spec/lib/gitlab/import_export/command_line_util_spec.rb'

View File

@ -0,0 +1,14 @@
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
const APPLICATION_SETTINGS_PATH = '/api/:version/application/settings';
export function getApplicationSettings() {
const url = buildApiUrl(APPLICATION_SETTINGS_PATH);
return axios.get(url);
}
export function updateApplicationSettings(data) {
const url = buildApiUrl(APPLICATION_SETTINGS_PATH);
return axios.put(url, data);
}

View File

@ -1,5 +1,8 @@
<script>
import DefaultActions from 'jh_else_ce/blob/components/blob_header_default_actions.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import { SIMPLE_BLOB_VIEWER } from './constants';
@ -11,6 +14,21 @@ export default {
DefaultActions,
BlobFilepath,
TableOfContents,
WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'),
},
apollo: {
currentUser: {
query: userInfoQuery,
error() {
this.$emit('error');
},
},
gitpodEnabled: {
query: applicationInfoQuery,
error() {
this.$emit('error');
},
},
},
props: {
blob: {
@ -52,10 +70,26 @@ export default {
required: false,
default: false,
},
showForkSuggestion: {
type: Boolean,
required: false,
default: false,
},
projectPath: {
type: String,
required: false,
default: '',
},
projectId: {
type: String,
required: false,
default: '',
},
},
data() {
return {
viewer: this.hideViewerSwitcher ? null : this.activeViewerType,
gitpodEnabled: false,
};
},
computed: {
@ -65,12 +99,18 @@ export default {
showDefaultActions() {
return !this.hideDefaultActions;
},
showWebIdeLink() {
return !this.blob.archived && this.blob.editBlobPath;
},
isEmpty() {
return this.blob.rawSize === '0';
},
blobSwitcherDocIcon() {
return this.blob.richViewer?.fileType === 'csv' ? 'table' : 'document';
},
projectIdAsNumber() {
return getIdFromGraphQLId(this.projectId);
},
},
watch: {
viewer(newVal, oldVal) {
@ -100,6 +140,27 @@ export default {
<div class="gl-display-flex gl-flex-wrap file-actions">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
<web-ide-link
v-if="showWebIdeLink"
:show-edit-button="!isBinary"
class="gl-mr-3"
:edit-url="blob.editBlobPath"
:web-ide-url="blob.ideEditPath"
:needs-to-fork="showForkSuggestion"
:show-pipeline-editor-button="Boolean(blob.pipelineEditorPath)"
:pipeline-editor-url="blob.pipelineEditorPath"
:gitpod-url="blob.gitpodBlobUrl"
:show-gitpod-button="gitpodEnabled"
:gitpod-enabled="currentUser && currentUser.gitpodEnabled"
:project-path="projectPath"
:project-id="projectIdAsNumber"
:user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath"
:user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath"
is-blob
disable-fork-modal
v-on="$listeners"
/>
<slot name="actions"></slot>
<default-actions

View File

@ -116,6 +116,6 @@ export default class CreateItemDropdown {
}
close() {
this.$dropdown.data('deprecatedJQueryDropdown').close();
this.$dropdown.data('deprecatedJQueryDropdown')?.close();
}
}

View File

@ -88,6 +88,11 @@ export default {
required: false,
default: undefined,
},
showUsers: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
@ -152,6 +157,9 @@ export default {
return labelPieces.join(', ') || this.label;
},
fossWithMergeAccess() {
return !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
},
dropdownToggleClass() {
return {
'gl-text-gray-500!': this.toggleLabel === this.label,
@ -247,13 +255,18 @@ export default {
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
id,
name,
username,
avatar_url,
type: LEVEL_TYPES.USER,
}));
// Has to be checked against server response
// because the selected item can be in filter results
if (this.showUsers) {
this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
id,
name,
username,
avatar_url,
type: LEVEL_TYPES.USER,
}));
}
}
this.deployKeys = deployKeysResponse.map((response) => {
@ -351,23 +364,38 @@ export default {
return [...added, ...removed, ...preserved];
},
onItemClick(item) {
this.toggleSelection(this.selected[item.type], item);
this.toggleSelection(item);
this.emitUpdate();
},
toggleSelection(arr, item) {
toggleSelection(item) {
if (item.id === ACCESS_LEVEL_NONE) {
arr.splice(0, arr.length, item);
this.selected[LEVEL_TYPES.ROLE] = [item];
return;
}
const itemIndex = arr.findIndex(({ id }) => id === item.id);
if (itemIndex === -1) {
arr.push(item);
this.unselectNone(arr);
} else arr.splice(itemIndex, 1);
const itemSelected = this.isSelected(item);
if (itemSelected) {
this.selected[item.type] = this.selected[item.type].filter(({ id }) => id !== item.id);
return;
}
// We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
// remove all preselected items before selecting this item
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
if (this.fossWithMergeAccess) this.clearSelection();
else if (item.type === LEVEL_TYPES.ROLE) this.unselectNone();
this.selected[item.type].push(item);
},
unselectNone(arr) {
const noneIndex = arr.findIndex(({ id }) => id === ACCESS_LEVEL_NONE);
if (noneIndex > -1) arr.splice(noneIndex, 1);
unselectNone() {
this.selected[LEVEL_TYPES.ROLE] = this.selected[LEVEL_TYPES.ROLE].filter(
({ id }) => id !== ACCESS_LEVEL_NONE,
);
},
clearSelection() {
Object.values(LEVEL_TYPES).forEach((level) => {
this.selected[level] = [];
});
},
isSelected(item) {
return this.selected[item.type].some((selected) => selected.id === item.id);

View File

@ -4,16 +4,15 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale';
import AccessDropdown from '~/projects/settings/access_dropdown';
import { initToggle } from '~/toggles';
import { expandSection } from '~/settings_panels';
import { scrollToElement } from '~/lib/utils/common_utils';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import {
BRANCH_RULES_ANCHOR,
PROTECTED_BRANCHES_ANCHOR,
IS_PROTECTED_BRANCH_CREATED,
ACCESS_LEVELS,
LEVEL_TYPES,
} from './constants';
export default class ProtectedBranchCreate {
@ -21,14 +20,17 @@ export default class ProtectedBranchCreate {
this.hasLicense = options.hasLicense;
this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
this.currentProjectUserDefaults = {};
this.buildDropdowns();
this.forcePushToggle = initToggle(document.querySelector('.js-force-push-toggle'));
if (this.hasLicense) {
this.codeOwnerToggle = initToggle(document.querySelector('.js-code-owner-toggle'));
}
this.selectedItems = {
[ACCESS_LEVELS.PUSH]: [],
[ACCESS_LEVELS.MERGE]: [],
};
this.initDropdowns();
this.showSuccessAlertIfNeeded();
this.bindEvents();
}
@ -37,29 +39,26 @@ export default class ProtectedBranchCreate {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
initDropdowns() {
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToMergeDropdown,
accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback,
const allowedToMergeSelector = 'js-allowed-to-merge';
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = this.buildDropdown({
selector: allowedToMergeSelector,
accessLevel: ACCESS_LEVELS.MERGE,
hasLicense: this.hasLicense,
accessLevelsData: gon.merge_access_levels,
testId: 'allowed-to-merge-dropdown',
});
// Allowed to Push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToPushDropdown,
accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback,
const allowedToPushSelector = 'js-allowed-to-push';
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = this.buildDropdown({
selector: allowedToPushSelector,
accessLevel: ACCESS_LEVELS.PUSH,
hasLicense: this.hasLicense,
accessLevelsData: gon.push_access_levels,
testId: 'allowed-to-push-dropdown',
});
this.createItemDropdown = new CreateItemDropdown({
@ -71,14 +70,40 @@ export default class ProtectedBranchCreate {
});
}
buildDropdown({ selector, accessLevel, accessLevelsData, testId }) {
const [el] = this.$form.find(`.${selector}`);
if (!el) return undefined;
const projectId = gon.current_project_id;
const dropdown = initAccessDropdown(el, {
toggleClass: `${selector} gl-form-input-lg`,
hasLicense: this.hasLicense,
searchEnabled: el.dataset.filter !== undefined,
showUsers: projectId !== undefined,
block: true,
accessLevel,
accessLevelsData,
testId,
});
dropdown.$on('select', (selected) => {
this.selectedItems[accessLevel] = selected;
this.onSelectCallback();
});
dropdown.$on('shown', () => {
this.createItemDropdown.close();
});
return dropdown;
}
// Enable submit button after selecting an option
onSelect() {
const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
const toggle = !(
this.$form.find('input[name="protected_branch[name]"]').val() &&
$allowedToMerge.length &&
$allowedToPush.length
this.selectedItems[ACCESS_LEVELS.MERGE].length &&
this.selectedItems[ACCESS_LEVELS.PUSH].length
);
this.$form.find('button[type="submit"]').attr('disabled', toggle);
@ -137,32 +162,8 @@ export default class ProtectedBranchCreate {
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach((item) => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
});
} else if (item.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: item.access_level,
});
} else if (item.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: item.group_id,
});
} else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
levelAttributes.push({
deploy_key_id: item.deploy_key_id,
});
}
});
formData.protected_branch[`${accessLevel}_attributes`] = levelAttributes;
Object.values(ACCESS_LEVELS).forEach((level) => {
formData.protected_branch[`${level}_attributes`] = this.selectedItems[level];
});
return formData;

View File

@ -7,11 +7,9 @@ import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constant
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { redirectTo, getLocationHash } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import LineHighlighter from '~/blob/line_highlighter';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
@ -19,8 +17,6 @@ import { addBlameLink } from '~/blob/blob_blame_link';
import highlightMixin from '~/repository/mixins/highlight_mixin';
import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import ForkSuggestion from './fork_suggestion.vue';
@ -34,7 +30,6 @@ export default {
GlLoadingIcon,
GlButton,
ForkSuggestion,
WebIdeLink,
CodeIntelligence,
AiGenie: () => import('ee_component/ai/components/ai_genie.vue'),
},
@ -61,18 +56,6 @@ export default {
this.userPermissions = project.userPermissions;
},
},
gitpodEnabled: {
query: applicationInfoQuery,
error() {
this.displayError();
},
},
currentUser: {
query: userInfoQuery,
error() {
this.displayError();
},
},
project: {
query: blobInfoQuery,
variables() {
@ -90,6 +73,7 @@ export default {
const repository = data.project?.repository || {};
this.blobInfo = repository.blobs?.nodes[0] || {};
this.isEmptyRepository = repository.empty;
this.projectId = data.project?.id;
const usePlain = this.$route?.query?.plain === '1'; // When the 'plain' URL param is present, its value determines which viewer to render
const urlHash = getLocationHash(); // If there is a code line hash in the URL we render with the simple viewer
@ -131,13 +115,13 @@ export default {
isRenderingLegacyTextViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
project: DEFAULT_BLOB_INFO.project,
gitpodEnabled: DEFAULT_BLOB_INFO.gitpodEnabled,
currentUser: DEFAULT_BLOB_INFO.currentUser,
useFallback: false,
pathLocks: DEFAULT_BLOB_INFO.pathLocks,
userPermissions: DEFAULT_BLOB_INFO.userPermissions,
blobInfo: {},
isEmptyRepository: false,
projectId: null,
};
},
computed: {
@ -218,9 +202,6 @@ export default {
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
projectIdAsNumber() {
return getIdFromGraphQLId(this.project?.id);
},
},
watch: {
// Watch the URL 'plain' query value to know if the viewer needs changing.
@ -324,31 +305,15 @@ export default {
:has-render-error="hasRenderError"
:show-path="false"
:override-copy="true"
:show-fork-suggestion="showForkSuggestion"
:project-path="projectPath"
:project-id="projectId"
@viewer-changed="handleViewerChanged"
@copy="onCopy"
@edit="editBlob"
@error="displayError"
>
<template #actions>
<web-ide-link
v-if="!blobInfo.archived"
:show-edit-button="!isBinaryFileType"
class="gl-mr-3"
:edit-url="blobInfo.editBlobPath"
:web-ide-url="blobInfo.ideEditPath"
:needs-to-fork="showForkSuggestion"
:show-pipeline-editor-button="Boolean(blobInfo.pipelineEditorPath)"
:pipeline-editor-url="blobInfo.pipelineEditorPath"
:gitpod-url="blobInfo.gitpodBlobUrl"
:show-gitpod-button="gitpodEnabled"
:gitpod-enabled="currentUser && currentUser.gitpodEnabled"
:project-path="projectPath"
:project-id="projectIdAsNumber"
:user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath"
:user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath"
is-blob
disable-fork-modal
@edit="editBlob"
/>
<blob-button-group
v-if="isLoggedIn && !blobInfo.archived"
:path="path"

View File

@ -27,12 +27,6 @@ export const PDF_MAX_PAGE_LIMIT = 50;
export const ROW_APPEAR_DELAY = 150;
export const DEFAULT_BLOB_INFO = {
gitpodEnabled: false,
currentUser: {
gitpodEnabled: false,
preferencesGitpodPath: null,
profileEnableGitpodPath: null,
},
userPermissions: {
pushCode: false,
downloadCode: false,

View File

@ -8,6 +8,7 @@ export * from './api/tags_api';
export * from './api/alert_management_alerts_api';
export * from './api/harbor_registry';
export * from './api/environments_api';
export * from './api/application_settings_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests.

View File

@ -387,20 +387,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
# NOTE: Remove this disable with add_prepared_state_to_mr FF removal
# rubocop: disable Metrics/AbcSize
def show_merge_request
close_merge_request_if_no_source_project
@merge_request.check_mergeability(async: true)
# NOTE: Remove the created_at check when removing the FF check
if ::Feature.enabled?(:add_prepared_state_to_mr, @merge_request.project) &&
@merge_request.created_at < 5.minutes.ago &&
!@merge_request.prepared?
@merge_request.prepare
end
respond_to do |format|
format.html do
# use next to appease Rubocop
@ -444,7 +434,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
end
# rubocop: enable Metrics/AbcSize
def render_html_page
preload_assignees_for_render(@merge_request)

View File

@ -371,6 +371,7 @@ class MergeRequest < ApplicationRecord
scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) }
scope :recently_unprepared, -> { where(prepared_at: nil).where(created_at: 2.hours.ago..).order(:created_at, :id) } # id is the tie-breaker
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))

View File

@ -7,7 +7,6 @@ module WorkItems
self.table_name = 'work_item_parent_links'
MAX_CHILDREN = 100
PARENT_TYPES = [:issue, :incident].freeze
belongs_to :work_item
belongs_to :work_item_parent, class_name: 'WorkItem'
@ -122,3 +121,5 @@ module WorkItems
end
end
end
WorkItems::ParentLink.prepend_mod

View File

@ -76,13 +76,13 @@ module IssuableLinks
target_issuables.map do |referenced_object|
link = relate_issuables(referenced_object)
if link.valid?
after_create_for(link)
else
if link.errors.any?
@errors << _("%{ref} cannot be added: %{error}") % {
ref: referenced_object.to_reference,
error: link.errors.messages.values.flatten.to_sentence
}
else
after_create_for(link)
end
link

View File

@ -9,7 +9,7 @@
= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control gl-form-input gl-form-input-sm'
.form-group.gl-form-group{ role: 'group' }
= f.gitlab_ui_checkbox_component :can_create_group, s_('AdminUsers|Can create group')
= f.gitlab_ui_checkbox_component :can_create_group, s_('AdminUsers|Can create top level group')
= f.gitlab_ui_checkbox_component :private_profile, s_('AdminUsers|Private profile')
%fieldset.form-group.gl-form-group

View File

@ -71,7 +71,7 @@
= render_if_exists 'admin/users/provisioned_by', user: @user
%li
%span.light= _('Can create groups:')
%span.light= _('Can create top level groups:')
%strong
= @user.can_create_group ? _('Yes') : _('No')
%li

View File

@ -1,14 +1,9 @@
- content_for :merge_access_levels do
.merge_access_levels-container
= dropdown_tag(_('Select'),
options: { toggle_class: 'js-allowed-to-merge wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_merge_dropdown_content', dropdown_testid: 'allowed-to-merge-dropdown',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'select_allowed_to_merge_dropdown' }})
.js-allowed-to-merge
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag(_('Select'),
options: { toggle_class: "js-allowed-to-push js-multiselect wide",
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_push_dropdown_content' , dropdown_testid: 'allowed-to-push-dropdown',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'select_allowed_to_push_dropdown' }})
.js-allowed-to-push
= render 'protected_branches/shared/create_protected_branch', protected_branch_entity: protected_branch_entity

View File

@ -570,6 +570,15 @@
:weight: 1
:idempotent: false
:tags: []
- :name: cronjob:merge_requests_ensure_prepared
:worker_name: MergeRequests::EnsurePreparedWorker
:feature_category: :code_review_workflow
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:metrics_global_metrics_update
:worker_name: Metrics::GlobalMetricsUpdateWorker
:feature_category: :metrics

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module MergeRequests
class EnsurePreparedWorker
include ApplicationWorker
include CronjobQueue
feature_category :code_review_workflow
idempotent!
deduplicate :until_executed
data_consistency :sticky
JOBS_PER_10_SECONDS = 5
def perform
return unless Feature.enabled?(:ensure_merge_requests_prepared)
scope = MergeRequest.recently_unprepared
iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)
index = 0
iterator.each_batch(of: JOBS_PER_10_SECONDS) do |merge_requests|
index += 1
NewMergeRequestWorker.bulk_perform_in_with_contexts(index * 10.seconds,
merge_requests,
arguments_proc: ->(merge_request) { [merge_request.id, merge_request.author_id] },
context_proc: ->(merge_request) { { project: merge_request.project } }
)
end
end
end
end

View File

@ -2,22 +2,23 @@
class NewMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include NewIssuable
data_consistency :always
sidekiq_options retry: 3
include NewIssuable
idempotent!
deduplicate :until_executed
feature_category :code_review_workflow
urgency :high
worker_resource_boundary :cpu
weight 2
def perform(merge_request_id, user_id)
return unless objects_found?(merge_request_id, user_id)
return if issuable.prepared?
MergeRequests::AfterCreateService
.new(project: issuable.target_project, current_user: user)

View File

@ -1,8 +1,8 @@
---
name: explain_current_blob
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128342/
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420959
milestone: '16.3'
name: clickhouse_ci_analytics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130211
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/424498
milestone: '16.4'
type: development
group: group::ai framework
group: group::runner
default_enabled: false

View File

@ -1,8 +1,8 @@
---
name: add_prepared_state_to_mr
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109967
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389249
milestone: '15.9'
name: ensure_merge_requests_prepared
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121999
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413884
milestone: '16.4'
type: development
group: group::code review
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: push_ai_to_load_identified_issue_json
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128342
rollout_issue_url:
milestone: '16.3'
type: development
group: group::ai framework
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: search_issues_hide_archived_projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124846
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416483
milestone: '16.2'
type: development
group: group::global search
default_enabled: false

View File

@ -685,6 +685,9 @@ Settings.cron_jobs['object_storage_delete_stale_direct_uploads_worker']['job_cla
Settings.cron_jobs['service_desk_custom_email_verification_cleanup'] ||= {}
Settings.cron_jobs['service_desk_custom_email_verification_cleanup']['cron'] ||= '*/2 * * * *'
Settings.cron_jobs['service_desk_custom_email_verification_cleanup']['job_class'] = 'ServiceDesk::CustomEmailVerificationCleanupWorker'
Settings.cron_jobs['ensure_merge_requests_prepared_worker'] ||= {}
Settings.cron_jobs['ensure_merge_requests_prepared_worker']['cron'] ||= '*/30 * * * *'
Settings.cron_jobs['ensure_merge_requests_prepared_worker']['job_class'] ||= 'MergeRequests::EnsurePreparedWorker'
Gitlab.ee do
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= {}

View File

@ -1,7 +1,7 @@
---
- title: "Old versions of JSON web tokens are deprecated"
announcement_milestone: "15.9" # (required) The milestone when this feature was first announced as deprecated.
removal_milestone: "16.5" # (required) The milestone when this feature is planned to be removed
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
breaking_change: true # (required) Change to false if this is not a breaking change.
reporter: dhershkovitch # (required) GitLab username of the person reporting the change
stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
@ -32,9 +32,9 @@
- CI/CD jobs that use the `id_tokens` keyword can use ID tokens with `secrets:vault`,
and will not have any `CI_JOB_JWT*` tokens available.
- Jobs that do not use the `id_tokens` keyword will continue to have the `CI_JOB_JWT*`
tokens available until GitLab 16.5.
tokens available until GitLab 17.0.
In GitLab 16.5, the deprecated tokens will be completely removed and will no longer
In GitLab 17.0, the deprecated tokens will be completely removed and will no longer
be available in CI/CD jobs.
#

View File

@ -19,7 +19,7 @@
#
- title: "HashiCorp Vault integration will no longer use CI_JOB_JWT by default"
announcement_milestone: "15.9" # (required) The milestone when this feature was first announced as deprecated.
removal_milestone: "16.5" # (required) The milestone when this feature is planned to be removed
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
breaking_change: true # (required) Change to false if this is not a breaking change.
reporter: dhershkovitch # (required) GitLab username of the person reporting the change
stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
@ -40,7 +40,7 @@
- CI/CD jobs that use the `id_tokens` keyword can use ID tokens with `secrets:vault`,
and will not have any `CI_JOB_JWT*` tokens available.
- Jobs that do not use the `id_tokens` keyword will continue to have the `CI_JOB_JWT*`
tokens available until GitLab 16.5.
tokens available until GitLab 17.0.
# If an End of Support period applies, the announcement should be shared with GitLab Support
# in the `#spt_managers` channel in Slack, and mention `@gitlab-com/support` in this MR.
#

View File

@ -0,0 +1,33 @@
-- source table for CI analytics, almost useless on it's own, but it's a basis for creating materialized views
CREATE TABLE ci_finished_builds
(
id UInt64 DEFAULT 0,
project_id UInt64 DEFAULT 0,
pipeline_id UInt64 DEFAULT 0,
status LowCardinality(String) DEFAULT '',
--- Fields to calculate timings
created_at DateTime64(6, 'UTC') DEFAULT now(),
queued_at DateTime64(6, 'UTC') DEFAULT now(),
finished_at DateTime64(6, 'UTC') DEFAULT now(),
started_at DateTime64(6, 'UTC') DEFAULT now(),
runner_id UInt64 DEFAULT 0,
runner_manager_system_xid String DEFAULT '',
--- Runner fields
runner_run_untagged Boolean DEFAULT FALSE,
runner_type UInt8 DEFAULT 0,
runner_manager_version LowCardinality(String) DEFAULT '',
runner_manager_revision LowCardinality(String) DEFAULT '',
runner_manager_platform LowCardinality(String) DEFAULT '',
runner_manager_architecture LowCardinality(String) DEFAULT '',
--- Materialized columns
duration Int64 MATERIALIZED age('second', started_at, finished_at),
queueing_duration Int64 MATERIALIZED age('second', queued_at, started_at)
--- This table is incomplete, we'll add more fields before starting the data migration
)
ENGINE = ReplacingMergeTree -- Using ReplacingMergeTree just in case we accidentally insert the same data twice
ORDER BY (status, runner_type, project_id, finished_at, id)
PARTITION BY toYear(finished_at)

View File

@ -0,0 +1,11 @@
CREATE TABLE ci_finished_builds_aggregated_queueing_delay_percentiles
(
status LowCardinality(String) DEFAULT '',
runner_type UInt8 DEFAULT 0,
started_at_bucket DateTime64(6, 'UTC') DEFAULT now(),
count_builds AggregateFunction(count),
queueing_duration_quantile AggregateFunction(quantile, Int64)
)
ENGINE = AggregatingMergeTree()
ORDER BY (started_at_bucket, status, runner_type)

View File

@ -0,0 +1,12 @@
CREATE MATERIALIZED VIEW ci_finished_builds_aggregated_queueing_delay_percentiles_mv
TO ci_finished_builds_aggregated_queueing_delay_percentiles
AS
SELECT
status,
runner_type,
toStartOfInterval(started_at, INTERVAL 5 minute) AS started_at_bucket,
countState(*) as count_builds,
quantileState(queueing_duration) AS queueing_duration_quantile
FROM ci_finished_builds
GROUP BY status, runner_type, started_at_bucket

View File

@ -198,16 +198,16 @@ You must be an administrator to manually add emails to users:
The [Cohorts](user_cohorts.md) tab displays the monthly cohorts of new users and their activities over time.
## Prevent a user from creating groups
## Prevent a user from creating top level groups
By default, users can create groups. To prevent a user from creating a top level group:
By default, users can create top level groups. To prevent a user from creating a top level group:
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
1. Select **Overview > Users**.
1. Locate the user and select them.
1. Select **Edit**.
1. Clear the **Can create group** checkbox.
1. Clear the **Can create top level group** checkbox.
1. Select **Save changes**.
It is also possible to [limit which roles can create a subgroup within a group](../user/group/subgroups/index.md#change-who-can-create-subgroups).

View File

@ -188,29 +188,29 @@ successfully, you must replicate their data using some other means.
|Feature | Replicated (added in GitLab version) | Verified (added in GitLab version) | GitLab-managed object storage replication (added in GitLab version) | GitLab-managed object storage verification (added in GitLab version) | Notes |
|:--------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------|:---------------------------------------------------------------------------|:--------------------------------------------------------------------|:----------------------------------------------------------------|:------|
|[Application data in PostgreSQL](../../postgresql/index.md) | **Yes** (10.2) | **Yes** (10.2) | N/A | N/A | |
|[Project repository](../../../user/project/repository/index.md) | **Yes** (10.2) | **Yes** (10.7) | N/A | N/A | Migrated to [self-service framework](../../../development/geo/framework.md) in 16.2. See GitLab issue [#367925](https://gitlab.com/gitlab-org/gitlab/-/issues/367925) for more details.<br /><br />Behind feature flag `geo_project_repository_replication`, enabled by default in (16.3).<br /><br /> All projects, including [archived projects](../../../user/project/settings/index.md#archive-a-project), are replicated. |
|[Project wiki repository](../../../user/project/wiki/index.md) | **Yes** (10.2)<sup>2</sup> | **Yes** (10.7)<sup>2</sup> | N/A | N/A | Migrated to [self-service framework](../../../development/geo/framework.md) in 15.11. See GitLab issue [#367925](https://gitlab.com/gitlab-org/gitlab/-/issues/367925) for more details.<br /><br />Behind feature flag `geo_project_wiki_repository_replication`, enabled by default in (15.11). |
|[Group wiki repository](../../../user/project/wiki/group.md) | [**Yes** (13.10)](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | [**Yes** (16.3)](https://gitlab.com/gitlab-org/gitlab/-/issues/323897) | N/A | N/A | Behind feature flag `geo_group_wiki_repository_replication`, enabled by default. |
|[Application data in PostgreSQL](../../postgresql/index.md) | **Yes** (10.2) | **Yes** (10.2) | Not applicable | Not applicable | |
|[Project repository](../../../user/project/repository/index.md) | **Yes** (10.2) | **Yes** (10.7) | Not applicable | Not applicable | Migrated to [self-service framework](../../../development/geo/framework.md) in 16.2. See GitLab issue [#367925](https://gitlab.com/gitlab-org/gitlab/-/issues/367925) for more details.<br /><br />Behind feature flag `geo_project_repository_replication`, enabled by default in (16.3).<br /><br /> All projects, including [archived projects](../../../user/project/settings/index.md#archive-a-project), are replicated. |
|[Project wiki repository](../../../user/project/wiki/index.md) | **Yes** (10.2)<sup>2</sup> | **Yes** (10.7)<sup>2</sup> | Not applicable | Not applicable | Migrated to [self-service framework](../../../development/geo/framework.md) in 15.11. See GitLab issue [#367925](https://gitlab.com/gitlab-org/gitlab/-/issues/367925) for more details.<br /><br />Behind feature flag `geo_project_wiki_repository_replication`, enabled by default in (15.11). |
|[Group wiki repository](../../../user/project/wiki/group.md) | [**Yes** (13.10)](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | [**Yes** (16.3)](https://gitlab.com/gitlab-org/gitlab/-/issues/323897) | Not applicable | Not applicable | Behind feature flag `geo_group_wiki_repository_replication`, enabled by default. |
|[Uploads](../../uploads.md) | **Yes** (10.2) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_upload_replication`, enabled by default. Verification was behind the feature flag `geo_upload_verification`, removed in 14.8. |
|[LFS objects](../../lfs/index.md) | **Yes** (10.2) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification was behind the feature flag `geo_lfs_object_verification`, removed in 14.7. |
|[Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | N/A | N/A | |
|[Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | N/A | N/A | |
|[Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | Not applicable | Not applicable | |
|[Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | Not applicable | Not applicable | |
|[CI job artifacts](../../../ci/jobs/job_artifacts.md) | **Yes** (10.4) | **Yes** (14.10) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Verification is behind the feature flag `geo_job_artifact_replication`, enabled by default in 14.10. |
|[CI Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/ci/pipeline_artifact.rb) | [**Yes** (13.11)](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | [**Yes** (13.11)](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Persists additional artifacts after a pipeline completes. |
|[CI Secure Files](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/ci/secure_file.rb) | [**Yes** (15.3)](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91430) | [**Yes** (15.3)](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91430) | [**Yes** (15.3)](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91430) | [No](object_storage.md#verification-of-files-in-object-storage) | Verification is behind the feature flag `geo_ci_secure_file_replication`, enabled by default in 15.3. |
|[Container Registry](../../packages/container_registry.md) | **Yes** (12.3)<sup>1</sup> | **Yes** (15.10) | **Yes** (12.3)<sup>1</sup> | **Yes** (15.10) | See [instructions](container_registry.md) to set up the Container Registry replication. |
|[Terraform Module Registry](../../../user/packages/terraform_module_registry/index.md) | **Yes** (14.0) | **Yes** (14.0) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Project designs repository](../../../user/project/issues/design_management.md) | **Yes** (12.7) | **Yes** (16.1) | N/A | N/A | Designs also require replication of LFS objects and Uploads. |
|[Project designs repository](../../../user/project/issues/design_management.md) | **Yes** (12.7) | **Yes** (16.1) | Not applicable | Not applicable | Designs also require replication of LFS objects and Uploads. |
|[Package Registry](../../../user/packages/package_registry/index.md) | **Yes** (13.2) | **Yes** (13.10) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | **Yes** (13.12) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_terraform_state_version_replication`, enabled by default. Verification was behind the feature flag `geo_terraform_state_version_verification`, which was removed in 14.0. |
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification was behind the feature flag `geo_merge_request_diff_verification`, removed in 14.7.|
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | N/A | N/A | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | Not applicable | Not applicable | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
|[Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
| [Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | **Yes** (15.5) | **Yes**(15.5) | **Yes** (15.5) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication/Verification is handled via the Uploads data type. | |
|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | **Yes** (15.5) | **Yes** (15.5) | **Yes** (15.5) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication/Verification is handled via the Uploads data type. |
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | N/A | N/A | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | Not applicable | Not applicable | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|[Elasticsearch integration](../../../integration/advanced_search/elasticsearch.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | No | Not planned because further product discovery is required and Elasticsearch (ES) clusters can be rebuilt. Secondaries use the same ES cluster as the primary. |
|[Dependency Proxy Images](../../../user/packages/dependency_proxy/index.md) | [**Yes** (15.7)](https://gitlab.com/groups/gitlab-org/-/epics/8833) | [**Yes** (15.7)](https://gitlab.com/groups/gitlab-org/-/epics/8833) | [**Yes** (15.7)](https://gitlab.com/groups/gitlab-org/-/epics/8833) | [No](object_storage.md#verification-of-files-in-object-storage) | |
|[Vulnerability Export](../../../user/application_security/vulnerability_report/index.md#export-vulnerability-details) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/3111) | No | No | No | Not planned because they are ephemeral and sensitive information. They can be regenerated on demand. |

View File

@ -7,8 +7,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Geo with external PostgreSQL instances **(PREMIUM SELF)**
This document is relevant if you are using a PostgreSQL instance that is not
managed by the Linux package. This includes cloud-managed instances like Amazon RDS (Aurora is not supported), or
manually installed and configured PostgreSQL instances.
managed by the Linux package. This includes
[cloud-managed instances](../../reference_architectures/index.md#recommendation-notes-for-the-database-services),
or manually installed and configured PostgreSQL instances.
Ensure that you are using one of the PostgreSQL versions that
the [Linux package ships with](../../package_information/postgresql_versions.md)

View File

@ -44,7 +44,7 @@ For self-compiled installations:
Administrators can:
- Use the Admin Area to [prevent an existing user from creating top-level groups](../administration/admin_area.md#prevent-a-user-from-creating-groups).
- Use the Admin Area to [prevent an existing user from creating top-level groups](../administration/admin_area.md#prevent-a-user-from-creating-top-level-groups).
- Use the [modify an existing user API endpoint](../api/users.md#user-modification) to change the `can_create_group` setting.
## Prevent users from changing their usernames

View File

@ -7,212 +7,193 @@ type: index, howto
# Migrating from Jenkins **(FREE ALL)**
A lot of GitLab users have successfully migrated to GitLab CI/CD from Jenkins.
We've collected several resources here that you might find informative if you're just getting started.
Think of this page as a "GitLab CI/CD for Jenkins Users" guide.
If you're migrating from Jenkins to GitLab CI/CD, you should be able
to create CI/CD pipelines that do everything you need.
You can start by watching the [Migrating from Jenkins to GitLab](https://www.youtube.com/watch?v=RlEVGOpYF5Y)
video for examples of:
- Converting a Jenkins pipeline into a GitLab CI/CD pipeline.
- Using Auto DevOps to test your code automatically.
## Get started
The following list of recommended steps was created after observing organizations
that were able to quickly complete this migration:
that were able to quickly complete this migration.
1. Start by reading the GitLab CI/CD [Quick Start Guide](../quick_start/index.md) and [important product differences](#important-product-differences).
1. Learn the importance of [managing the organizational transition](#manage-organizational-transition).
1. [Add runners](../runners/index.md) to your GitLab instance.
1. Educate and enable your developers to independently perform the following steps in their projects:
1. Review the [Quick Start Guide](../quick_start/index.md) and [Pipeline Configuration Reference](../yaml/index.md).
1. Use the [Jenkins Wrapper](#jenkinsfile-wrapper) to temporarily maintain fragile Jenkins jobs.
1. Migrate the build and CI jobs and configure them to show results directly in your merge requests. They can use [Auto DevOps](../../topics/autodevops/index.md) as a starting point, and [customize](../../topics/autodevops/customize.md) or [decompose](../../topics/autodevops/customize.md#use-individual-components-of-auto-devops) the configuration as needed.
1. Add [Review Apps](../review_apps/index.md).
1. Migrate the deployment jobs using [cloud deployment templates](../cloud_deployment/index.md), adding [environments](../environments/index.md), and [deploy boards](../../user/project/deploy_boards.md).
1. Work to unwrap any jobs still running with the use of the Jenkins wrapper.
1. Take stock of any common CI/CD job definitions then create and share [templates](#templates) for them.
1. Check the [pipeline efficiency documentation](../pipelines/pipeline_efficiency.md)
to learn how to make your GitLab CI/CD pipelines faster and more efficient.
Engineers that plan to migrate projects to GitLab CI/CD should:
Watch the [Migrating from Jenkins to GitLab](https://www.youtube.com/watch?v=RlEVGOpYF5Y) video for examples of how to:
- Convert a Jenkins pipeline into a GitLab CI/CD pipeline.
- Use Auto DevOps to test your code automatically.
Otherwise, read on for important information that helps you get the ball rolling. Welcome
to GitLab!
- Read about some [key GitLab CI/CD features](#key-gitlab-cicd-features).
- Follow tutorials to create:
- [Your first GitLab pipeline](../quick_start/index.md).
- [A more complex pipeline](../quick_start/tutorial.md) that builds, tests,
and deploys a static site.
- Review the [`.gitlab-ci.yml` keyword reference](../yaml/index.md).
- Ensure [runners](../runners/index.md) are available, either by using shared GitLab.com runners
or installing new runners.
- Migrate build and CI jobs and configure them to show results directly in merge requests.
You can use [Auto DevOps](../../topics/autodevops/index.md) as a starting point,
and [customize](../../topics/autodevops/customize.md) or [decompose](../../topics/autodevops/customize.md#use-individual-components-of-auto-devops)
the configuration as needed.
- Migrate deployment jobs by using [cloud deployment templates](../cloud_deployment/index.md),
[environments](../environments/index.md), and the [GitLab agent for Kubernetes](../../user/clusters/agent/index.md).
- Check if any CI/CD configuration can be reused across different projects, then create
and share [templates](#templates).
- Check the [pipeline efficiency documentation](../pipelines/pipeline_efficiency.md)
to learn how to make your GitLab CI/CD pipelines faster and more efficient.
If you have questions that are not answered here, the [GitLab community forum](https://forum.gitlab.com/)
can be a great resource.
## Manage organizational transition
### Manage organizational changes
An important part of transitioning from Jenkins to GitLab is the cultural and organizational
changes that come with the move, and successfully managing them. A few
things we have found that help this are:
changes that come with the move, and successfully managing them.
- Setting and communicating a clear vision of what your migration goals are helps
A few things that organizations have reported as helping:
- Set and communicate a clear vision of what your migration goals are, which helps
your users understand why the effort is worth it. The value is clear when
the work is done, but people need to be aware while it's in progress too.
- Sponsorship and alignment from the relevant leadership team helps with the point above.
- Spending time educating your users on what's different and sharing this document
with them helps ensure you are successful.
- Finding ways to sequence or delay parts of the migration can help a lot, but you
don't want to leave things in a non-migrated (or partially-migrated) state for too
long. To gain all the benefits of GitLab, moving your existing Jenkins setup over
as-is, including any current problems, isn't enough. You need to take advantage
of the improvements that GitLab offers, and this requires (eventually) updating
your implementation as part of the transition.
- Sponsorship and alignment from the relevant leadership teams helps with the point above.
- Spend time educating your users on what's different, and share this guide
with them.
- Finding ways to sequence or delay parts of the migration can help a lot. Importantly though,
try not to leave things in a non-migrated (or partially-migrated) state for too
long.
- To gain all the benefits of GitLab, moving your existing Jenkins setup over
as-is, including any current problems, isn't enough. Take advantage of the improvements
that GitLab CI/CD offers, and update your implementation as part of the transition.
## JenkinsFile Wrapper
### Key GitLab CI/CD features
We are building a [JenkinsFile Wrapper](https://gitlab.com/gitlab-org/jfr-container-builder/) which
you can use to run a complete Jenkins instance inside of a GitLab job, including plugins. This can help ease the process
of transition, by letting you delay the migration of less urgent pipelines for a period of time.
GitLab CI/CD key features might be different or not exist in Jenkins. For example,
in GitLab:
If you are interested in helping GitLab test the wrapper, join our [public testing issue](https://gitlab.com/gitlab-org/gitlab/-/issues/215675) for instructions and to provide your feedback.
NOTE:
If you have a paid GitLab subscription, the JenkinsFile Wrapper is not packaged with GitLab and falls outside of the scope of support. For more information, see the [Statement of Support](https://about.gitlab.com/support/statement-of-support/).
## Important product differences
Some high level differences between the products worth mentioning are:
- With GitLab you don't need a root `pipeline` keyword to wrap everything.
- The way pipelines are triggered and [trigger other pipelines](../yaml/index.md#trigger)
is different than Jenkins. GitLab pipelines can be triggered:
- on push
- on [schedule](../pipelines/schedules.md)
- from the [GitLab UI](../pipelines/index.md#run-a-pipeline-manually)
- by [API call](../triggers/index.md)
- by [webhook](../triggers/index.md#use-a-webhook)
- by [ChatOps](../chatops/index.md)
- You can control which jobs run in which cases, depending on how they are triggered,
with the [`rules` syntax](../yaml/index.md#rules).
- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different from Jenkins.
- You can reuse pipeline configurations using the [`include` keyword](../yaml/index.md#include)
and [templates](#templates). Your templates can be kept in a central repository (with different
permissions), and then any project can use them. This central project could also
contain scripts or other reusable code.
- You can also use the [`extends` keyword](../yaml/index.md#extends) to reuse configuration
in a single pipeline configuration.
- All jobs in a single stage always run in parallel, and all stages run in sequence.
Certain jobs might break this sequencing as needed with our [directed acyclic graph](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/47063)
feature.
- Pipelines can be triggered with:
- A Git push
- A [Schedule](../pipelines/schedules.md)
- The [GitLab UI](../pipelines/index.md#run-a-pipeline-manually)
- An [API call](../triggers/index.md)
- A [webhook](../triggers/index.md#use-a-webhook)
- You can control which jobs run in which cases with the [`rules` syntax](../yaml/index.md#rules).
- You can reuse pipeline configurations:
- Use the [`extends` keyword](../yaml/index.md#extends) to reuse configuration
in a single pipeline configuration.
- Use the [`include` keyword](../yaml/index.md#include) to reuse configuration across
multiple pipelines and projects.
- Jobs are grouped into stages, and jobs in the same stage can run at the same time.
Stages run in sequence. Jobs can be configured to run outside of the stage ordering with the
[`needs` keyword](../yaml/index.md#needs).
- The [`parallel`](../yaml/index.md#parallel) keyword can automatically parallelize tasks,
like tests that support parallelization.
- Usually all jobs in a single stage run in parallel, and all stages run in sequence.
Different [pipeline architectures](../pipelines/pipeline_architectures.md) allow you to change this behavior.
- The new [`rules` syntax](../yaml/index.md#rules) is the recommended method of
controlling when different jobs run. It is more powerful than the `only/except` syntax.
- One important difference is that jobs run independently of each other and have a
fresh environment in each job. Passing artifacts between jobs is controlled using the
[`artifacts`](../yaml/index.md#artifacts) and [`dependencies`](../yaml/index.md#dependencies)
keywords. When finished, use the planned [Workspaces](https://gitlab.com/gitlab-org/gitlab/-/issues/29265)
feature to persist a common workspace between serial jobs.
- The `.gitlab-ci.yml` file is checked in to the root of your repository, much like a Jenkinsfile, but
is in the YAML format (see [complete reference](../yaml/index.md)) instead of a Groovy DSL. It's most
analogous to the declarative Jenkinsfile format.
- Manual approvals or gates can be set up as [`when:manual` jobs](../jobs/job_control.md#create-a-job-that-must-be-run-manually). These can
also leverage [`protected environments`](../jobs/job_control.md#run-a-job-after-a-delay)
to control who is able to approve them.
- GitLab comes with a [container registry](../../user/packages/container_registry/index.md), so you can use
container images to set up your build environment. For example, set up one pipeline that builds your build environment
itself and publish that to the container registry. Then, have your pipelines use this instead of each building their
own environment, which is slower and may be less consistent. We have extensive documentation on [how to use the Container Registry](../../user/packages/container_registry/index.md).
- A central utilities repository can be a great place to put assorted scheduled jobs
or other manual jobs that function like utilities. Jenkins installations tend to
have a few of these.
especially tests that support parallelization.
- Jobs run independently of each other and have a fresh environment for each job.
Passing artifacts between jobs is controlled using the [`artifacts`](../yaml/index.md#artifacts)
and [`dependencies`](../yaml/index.md#dependencies) keywords.
- The `.gitlab-ci.yml` configuration file exists in your Git repository, like a `Jenkinsfile`,
but is [a YAML file](#yaml-configuration-file), not Groovy.
- GitLab comes with a [container registry](../../user/packages/container_registry/index.md).
You can build and store custom container images to run your jobs in.
## Agents vs. runners
## Runners
Both Jenkins agents and GitLab runners are the hosts that run jobs. To convert the
Jenkins agent, uninstall it and then [install and register the runner](../runners/index.md).
Runners do not require much overhead, so you can size them similarly to the Jenkins
agents you were using.
Like Jenkins agents, GitLab runners are the hosts that run jobs. If you are using GitLab.com,
you can use the [shared runner fleet](../runners/index.md) to run jobs without provisioning
your own runners.
Some important differences in the way runners work in comparison to agents are:
To convert a Jenkins agent for use with GitLab CI/CD, uninstall the agent and then
[install and register a runner](../runners/index.md). Runners do not require much overhead,
so you might be able to use similar provisioning as the Jenkins agents you were using.
- Runners can be set up as [shared across an instance, be added at the group level, or set up at the project level](../runners/runners_scope.md).
They self-select jobs from the scopes you've defined automatically.
- You can also [use tags](../runners/configure_runners.md#use-tags-to-control-which-jobs-a-runner-can-run) for finer control, and
associate runners with specific jobs. For example, you can use a tag for jobs that
Some key details about runners:
- Runners can be [configured](../runners/runners_scope.md) to be shared across an instance,
a group, or dedicated to a single project.
- You can use the [`tags` keyword](../runners/configure_runners.md#use-tags-to-control-which-jobs-a-runner-can-run)
for finer control, and associate runners with specific jobs. For example, you can use a tag for jobs that
require dedicated, more powerful, or specific hardware.
- GitLab has [autoscaling for runners](https://docs.gitlab.com/runner/configuration/autoscale.html).
Use autoscaling to provision runners only when needed and scale down when not needed,
similar to ephemeral agents in Jenkins.
If you are using `gitlab.com`, you can take advantage of our [shared runner fleet](../runners/index.md)
to run jobs without provisioning your own runners. We are investigating making them
[available for self-managed instances](https://gitlab.com/groups/gitlab-org/-/epics/835)
as well.
## YAML configuration file
## Groovy vs. YAML
GitLab pipeline configuration files use the [YAML](https://yaml.org/) format instead of
the [Groovy](https://groovy-lang.org/) format that Jenkins uses.
Jenkins Pipelines are based on [Groovy](https://groovy-lang.org/), so the pipeline specification is written as code.
GitLab works a bit differently, using the more highly structured [YAML](https://yaml.org/) format.
The scripting elements are in `script` blocks separate from the pipeline specification itself.
Using YAML is a strength of GitLab, in that it helps keep the learning curve much simpler to get up and running.
It also avoids some of the problem of unconstrained complexity which can make your Jenkinsfile hard to understand
and manage.
We do of course still value DRY (don't repeat yourself) principles. We want to ensure that
behaviors of your jobs can be codified once and applied as needed. You can use the `extends` syntax to
[reuse configuration in your jobs](../yaml/index.md#extends), and `include` can
be used to [reuse pipeline configurations](../yaml/index.md#include) in pipelines
in different projects:
Using YAML is a strength of GitLab CI/CD, as it is a simple format to understand
and start using. For example, a small configuration file with two jobs and some
shared configuration in a hidden job:
```yaml
.in-docker:
.test-config:
tags:
- docker
image: alpine
- docker-runners
stage: test
rspec:
test-job:
extends:
- .in-docker
- .docker-config
script:
- rake rspec
- bundle exec rake rspec
lint-job:
extends:
- .docker-config
script:
- yarn run prettier
```
## Artifact publishing
In this example:
Artifacts may work a bit differently than you've used them with Jenkins. In GitLab, any job can define
a set of artifacts to be saved by using the `artifacts` keyword. This can be configured to point to a file
or set of files that can then be persisted from job to job. Read more on our detailed
[artifacts documentation](../jobs/job_artifacts.md):
- The commands to run in jobs are added with the [`script` keyword](../yaml/index.md#script).
- The [`extends` keyword](../yaml/index.md#extends) reduces duplication in the configuration
by adding the same `tags` and `stage` configuration defined in `.test-config` to both jobs.
### Artifacts
In GitLab, any job can use the [`artifacts` keyword](../yaml/index.md#artifacts)
to define a set of [artifacts](../jobs/job_artifacts.md) to be stored when a job completes.
Artifacts are files that can be used in later jobs, for example for testing or deployment.
For example:
```yaml
pdf:
script: xelatex mycv.tex
artifacts:
paths:
- ./mycv.pdf
- ./output/
- mycv.pdf
- output/
expire_in: 1 week
```
Additionally, we have package management features like built-in container and package registries that you
can leverage. You can see the complete list of packaging features in the
[Packages and registries](../../user/packages/index.md) documentation.
In this example:
## Integrated features
- The `mycv.pdf` file and all the files in `output/` are stored and could be used
in later jobs.
- To save resources, the artifacts expire and are deleted after one week.
You may have used plugins to get things like code quality, unit tests, and security scanning working in Jenkins.
GitLab takes advantage of our connected ecosystem to automatically pull these kinds of results into
your merge requests, pipeline details pages, and other locations. You may find that you actually don't
need to configure anything to have these appear.
### Scanning features
You might have used plugins for things like code quality, security, or static application scanning
in Jenkins. Tools like these are already available in GitLab and can be used in your
pipeline.
GitLab features including [code quality](../testing/code_quality.md), [security scanning](../../user/application_security/index.md),
[SAST](../../user/application_security/sast/index.md), and many others generate reports
when they complete. These reports can be displayed in merge requests and pipeline details pages.
### Templates
For advanced CI/CD teams, project templates can enable the reuse of pipeline configurations,
as well as encourage inner sourcing.
For organizations with many CI/CD pipelines, you can use project templates to configure
custom CI/CD configuration templates and reuse them across projects.
In self-managed GitLab instances, you can build an [Instance Template Repository](../../administration/settings/instance_template_repository.md).
Development teams across the whole organization can select templates from a dropdown list.
A group maintainer or a group owner is able to set a group to use as the source for the
[custom project templates](../../administration/custom_project_templates.md). This can
be used by all projects in the group. An instance administrator can set a group as
the source for [instance project templates](../../user/group/custom_project_templates.md),
which can be used by projects in that instance.
Group maintainers can configure a group to use as the source for [custom project templates](../../administration/custom_project_templates.md).
These templates can be used by all projects in the group.
An instance administrator can set a group as the source for [instance project templates](../../user/group/custom_project_templates.md),
which can be used by all projects in that instance.
## Convert a declarative Jenkinsfile
@ -360,5 +341,13 @@ my_job:
## Additional resources
For help making your pipelines faster and more efficient, see the
[pipeline efficiency documentation](../pipelines/pipeline_efficiency.md).
- You can use the [JenkinsFile Wrapper](https://gitlab.com/gitlab-org/jfr-container-builder/)
to run a complete Jenkins instance inside of a GitLab CI/CD job, including plugins. Use this tool to
help ease the transition to GitLab CI/CD, by delaying the migration of less urgent pipelines.
NOTE:
The JenkinsFile Wrapper is not packaged with GitLab and falls outside of the scope of support.
For more information, see the [Statement of Support](https://about.gitlab.com/support/statement-of-support/).
- If your tooling outputs packages that you want to make accessible, you can store them
in a [package registry](../../user/packages/index.md).
- Use [review Apps](../review_apps/index.md) to preview changes before merging them.

View File

@ -8,10 +8,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
This tutorial walks you through configuring a progressively more complex CI/CD pipeline
through small, iterative steps. The pipeline is always fully functional,
but it gains more functionality with each step.
but it gains more functionality with each step. The goal is to build, test, and deploy
a documentation site.
When you finish this tutorial, you will have a new project on GitLab.com and a working documentation site on
[Docusaurus](https://docusaurus.io/).
When you finish this tutorial, you will have a new project on GitLab.com and a working documentation site
using [Docusaurus](https://docusaurus.io/).
To complete this tutorial, you will:

View File

@ -59,7 +59,7 @@ A cop is in a _grace period_ if it is enabled and has `Details: grace period` de
On the default branch, offenses from cops in the [grace period](rake_tasks.md#run-rubocop-in-graceful-mode) do not fail the RuboCop CI job. Instead, the job notifies the `#f_rubocop` Slack channel. However, on other branches, the RuboCop job fails.
A grace period can safely be lifted as soon as there are no warnings for 2 weeks in the `#f_rubocop` channel on Slack.
A grace period can safely be lifted as soon as there are no warnings for 1 week in the `#f_rubocop` channel on Slack.
## Enabling a new cop
@ -67,7 +67,7 @@ A grace period can safely be lifted as soon as there are no warnings for 2 weeks
1. [Generate TODOs for the new cop](rake_tasks.md#generate-initial-rubocop-todo-list).
1. [Set the new cop to `grace period`](#cop-grace-period).
1. Create an issue to fix TODOs and encourage community contributions (via ~"quick win" and/or ~"Seeking community contributions"). [See some examples](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=quick%20wins&label_name%5B%5D=static%20code%20analysis&first_page_size=20).
1. Create an issue to remove `grace period` after 2 weeks of silence in the `#f_rubocop` Slack channel. [See an example](https://gitlab.com/gitlab-org/gitlab/-/issues/374903).
1. Create an issue to remove `grace period` after 1 week of silence in the `#f_rubocop` Slack channel. [See an example](https://gitlab.com/gitlab-org/gitlab/-/issues/374903).
## Silenced offenses

View File

@ -522,6 +522,35 @@ In GitLab 17.0, the `DISABLED_WITH_OVERRIDE` value of the `SharedRunnersSetting`
<div class="deprecation breaking-change" data-milestone="17.0">
### HashiCorp Vault integration will no longer use CI_JOB_JWT by default
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">15.9</span>
- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/366798).
</div>
As part of our effort to improve the security of your CI workflows using JWT and OIDC, the native HashiCorp integration is also being updated in GitLab 16.0. Any projects that use the [`secrets:vault`](https://docs.gitlab.com/ee/ci/yaml/#secretsvault) keyword to retrieve secrets from Vault will need to be [configured to use the ID tokens](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#configure-automatic-id-token-authentication). ID tokens were introduced in 15.7.
To prepare for this change, use the new [`id_tokens`](https://docs.gitlab.com/ee/ci/yaml/#id_tokens)
keyword and configure the `aud` claim. Ensure the bound audience is prefixed with `https://`.
In GitLab 15.9 to 15.11, you can [enable the **Limit JSON Web Token (JWT) access**](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#enable-automatic-id-token-authentication)
setting, which prevents the old tokens from being exposed to any jobs and enables
[ID token authentication for the `secrets:vault` keyword](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#configure-automatic-id-token-authentication).
In GitLab 16.0 and later:
- This setting will be removed.
- CI/CD jobs that use the `id_tokens` keyword can use ID tokens with `secrets:vault`,
and will not have any `CI_JOB_JWT*` tokens available.
- Jobs that do not use the `id_tokens` keyword will continue to have the `CI_JOB_JWT*`
tokens available until GitLab 17.0.
</div>
<div class="deprecation breaking-change" data-milestone="17.0">
### Maintainer role providing the ability to change Package settings using GraphQL API
<div class="deprecation-notes">
@ -544,6 +573,48 @@ settings for the group using either the GitLab UI or GraphQL API.
<div class="deprecation breaking-change" data-milestone="17.0">
### Old versions of JSON web tokens are deprecated
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">15.9</span>
- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/366798).
</div>
[ID tokens](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html) with OIDC support
were introduced in GitLab 15.7. These tokens are more configurable than the old JSON web tokens (JWTs), are OIDC compliant,
and only available in CI/CD jobs that explictly have ID tokens configured.
ID tokens are more secure than the old `CI_JOB_JWT*` JSON web tokens which are exposed in every job,
and as a result these old JSON web tokens are deprecated:
- `CI_JOB_JWT`
- `CI_JOB_JWT_V1`
- `CI_JOB_JWT_V2`
To prepare for this change, configure your pipelines to use [ID tokens](https://docs.gitlab.com/ee/ci/yaml/index.html#id_tokens)
instead of the deprecated tokens. For OIDC compliance, the `iss` claim now uses
the fully qualified domain name, for example `https://example.com`, previously
introduced with the `CI_JOB_JWT_V2` token.
In GitLab 15.9 to 15.11, you can [enable the **Limit JSON Web Token (JWT) access**](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#enable-automatic-id-token-authentication)
setting, which prevents the old tokens from being exposed to any jobs and enables
[ID token authentication for the `secrets:vault` keyword](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#configure-automatic-id-token-authentication).
In GitLab 16.0 and later:
- This setting will be removed.
- CI/CD jobs that use the `id_tokens` keyword can use ID tokens with `secrets:vault`,
and will not have any `CI_JOB_JWT*` tokens available.
- Jobs that do not use the `id_tokens` keyword will continue to have the `CI_JOB_JWT*`
tokens available until GitLab 17.0.
In GitLab 17.0, the deprecated tokens will be completely removed and will no longer
be available in CI/CD jobs.
</div>
<div class="deprecation breaking-change" data-milestone="17.0">
### OmniAuth Facebook is deprecated
<div class="deprecation-notes">
@ -984,77 +1055,6 @@ If you have [public or internal](https://docs.gitlab.com/ee/user/public_access.h
Enabling the `ldap_settings_unlock_groups_by_owners` feature flag allowed non-LDAP synced users to be added to a locked LDAP group. This [feature](https://gitlab.com/gitlab-org/gitlab/-/issues/1793) has always been disabled by default and behind a feature flag. We are removing this feature to keep continuity with our SAML integration, and because allowing non-synced group members defeats the "single source of truth" principle of using a directory service. Once this feature is removed, any LDAP group members that are not synced with LDAP will lose access to that group.
</div>
<div class="deprecation breaking-change" data-milestone="16.5">
### HashiCorp Vault integration will no longer use CI_JOB_JWT by default
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">15.9</span>
- Removal in GitLab <span class="milestone">16.5</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/366798).
</div>
As part of our effort to improve the security of your CI workflows using JWT and OIDC, the native HashiCorp integration is also being updated in GitLab 16.0. Any projects that use the [`secrets:vault`](https://docs.gitlab.com/ee/ci/yaml/#secretsvault) keyword to retrieve secrets from Vault will need to be [configured to use the ID tokens](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#configure-automatic-id-token-authentication). ID tokens were introduced in 15.7.
To prepare for this change, use the new [`id_tokens`](https://docs.gitlab.com/ee/ci/yaml/#id_tokens)
keyword and configure the `aud` claim. Ensure the bound audience is prefixed with `https://`.
In GitLab 15.9 to 15.11, you can [enable the **Limit JSON Web Token (JWT) access**](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#enable-automatic-id-token-authentication)
setting, which prevents the old tokens from being exposed to any jobs and enables
[ID token authentication for the `secrets:vault` keyword](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#configure-automatic-id-token-authentication).
In GitLab 16.0 and later:
- This setting will be removed.
- CI/CD jobs that use the `id_tokens` keyword can use ID tokens with `secrets:vault`,
and will not have any `CI_JOB_JWT*` tokens available.
- Jobs that do not use the `id_tokens` keyword will continue to have the `CI_JOB_JWT*`
tokens available until GitLab 16.5.
</div>
<div class="deprecation breaking-change" data-milestone="16.5">
### Old versions of JSON web tokens are deprecated
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">15.9</span>
- Removal in GitLab <span class="milestone">16.5</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/366798).
</div>
[ID tokens](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html) with OIDC support
were introduced in GitLab 15.7. These tokens are more configurable than the old JSON web tokens (JWTs), are OIDC compliant,
and only available in CI/CD jobs that explictly have ID tokens configured.
ID tokens are more secure than the old `CI_JOB_JWT*` JSON web tokens which are exposed in every job,
and as a result these old JSON web tokens are deprecated:
- `CI_JOB_JWT`
- `CI_JOB_JWT_V1`
- `CI_JOB_JWT_V2`
To prepare for this change, configure your pipelines to use [ID tokens](https://docs.gitlab.com/ee/ci/yaml/index.html#id_tokens)
instead of the deprecated tokens. For OIDC compliance, the `iss` claim now uses
the fully qualified domain name, for example `https://example.com`, previously
introduced with the `CI_JOB_JWT_V2` token.
In GitLab 15.9 to 15.11, you can [enable the **Limit JSON Web Token (JWT) access**](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#enable-automatic-id-token-authentication)
setting, which prevents the old tokens from being exposed to any jobs and enables
[ID token authentication for the `secrets:vault` keyword](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#configure-automatic-id-token-authentication).
In GitLab 16.0 and later:
- This setting will be removed.
- CI/CD jobs that use the `id_tokens` keyword can use ID tokens with `secrets:vault`,
and will not have any `CI_JOB_JWT*` tokens available.
- Jobs that do not use the `id_tokens` keyword will continue to have the `CI_JOB_JWT*`
tokens available until GitLab 16.5.
In GitLab 16.5, the deprecated tokens will be completely removed and will no longer
be available in CI/CD jobs.
</div>
</div>

View File

@ -77,7 +77,7 @@ Prerequisites:
- At least the Maintainer role for a group to create subgroups for it.
- The [role determined by a setting](#change-who-can-create-subgroups). These users can create
subgroups even if group creation is
[disabled by an Administrator](../../../administration/admin_area.md#prevent-a-user-from-creating-groups) in the user's settings.
[disabled by an Administrator](../../../administration/admin_area.md#prevent-a-user-from-creating-top-level-groups) in the user's settings.
NOTE:
You cannot host a GitLab Pages subgroup website with a top-level domain name. For example, `subgroupname.example.io`.

View File

@ -21,7 +21,7 @@ type: howto, reference
WARNING:
This feature was [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5.
[An epic exists](https://gitlab.com/groups/gitlab-org/-/epics/2493)
to add this functionality to the [agent](../index.md).
to add this functionality to the [agent](../clusters/agent/index.md).
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `certificate_based_clusters`.

View File

@ -168,10 +168,19 @@ The following events are available for Slack notifications:
| **Wiki page** | A wiki page is created or updated. |
| **Deployment** | A deployment starts or finishes. |
| **Alert** | A new, unique alert is recorded. |
| **Group mention in public** | A group is mentioned in a public context. |
| **Group mention in private** | A group is mentioned in a confidential context. |
| **[Group mention](#trigger-notifications-for-group-mentions) in public** | A group is mentioned in a public context. |
| **[Group mention](#trigger-notifications-for-group-mentions) in private** | A group is mentioned in a confidential context. |
| [**Vulnerability**](../../application_security/vulnerabilities/index.md) | A new, unique vulnerability is recorded. |
### Trigger notifications for group mentions
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/417751) in GitLab 16.4.
To trigger a [notification event](#notification-events) for a group mention, use `@<group_name>` in:
- Issue and merge request descriptions
- Comments on issues, merge requests, and commits
## Troubleshooting
### GitLab for Slack app does not appear in the list of integrations

View File

@ -79,10 +79,19 @@ The following triggers are available for Slack notifications:
| **Wiki page** | A wiki page is created or updated. |
| **Deployment** | A deployment starts or finishes. |
| **Alert** | A new, unique alert is recorded. |
| **Group mention in public** | A group is mentioned in a public context. |
| **Group mention in private** | A group is mentioned in a confidential context. |
| **[Group mention](#trigger-notifications-for-group-mentions) in public** | A group is mentioned in a public context. |
| **[Group mention](#trigger-notifications-for-group-mentions) in private** | A group is mentioned in a confidential context. |
| [**Vulnerability**](../../application_security/vulnerabilities/index.md) | A new, unique vulnerability is recorded. |
## Trigger notifications for group mentions
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/417751) in GitLab 16.4.
To trigger a [notification event](#triggers-for-slack-notifications) for a group mention, use `@<group_name>` in:
- Issue and merge request descriptions
- Comments on issues, merge requests, and commits
## Troubleshooting
If your Slack integration is not working, start troubleshooting by

View File

@ -262,6 +262,12 @@ To change the custom email configuration you must reset and remove it and config
To reset the configuration at any step in the process, select **Reset custom email**.
The credentials are then removed from the database.
### Custom email reply address
External participants can [reply by email](../../../administration/reply_by_email.md) to Service Desk tickets.
GitLab uses an email reply address with a 32-character reply key that corresponds to the ticket.
When a custom email is configured, GitLab generates the reply address from that email.
### Known issues
- Some service providers don't allow SMTP connections any more.

View File

@ -81,6 +81,20 @@ To view a list of files you changed in the Web IDE:
Your `CHANGES`, `STAGED CHANGES`, and `MERGE CHANGES` are displayed.
For more information, see the [VS Code documentation](https://code.visualstudio.com/docs/sourcecontrol/overview#_commit).
## Restore uncommitted changes
You don't have to manually save any file you modify in the Web IDE.
Modified files are automatically staged and can be [committed](#commit-changes).
Uncommitted changes are saved in your browser's local storage and persist
even if you close the browser tab or refresh the Web IDE.
If your uncommitted changes are not available, you can restore the changes from local history.
To restore uncommitted changes in the Web IDE:
1. Press <kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>P</kbd>.
1. In the search box, enter `Local History: Find Entry to Restore`.
1. Select the file that contains the uncommitted changes.
## Upload a new file
To upload a new file in the Web IDE:

View File

@ -190,8 +190,13 @@ module Gitlab
issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
unless default_project_filter
issues = issues.in_projects(project_ids_relation)
.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
project_ids = project_ids_relation
if Feature.enabled?(:search_issues_hide_archived_projects, current_user) && !filters[:include_archived]
project_ids = project_ids.non_archived
end
issues = issues.in_projects(project_ids)
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/420046')
end
apply_sort(issues, scope: 'issues')

View File

@ -3868,7 +3868,7 @@ msgstr ""
msgid "AdminUsers|Bot"
msgstr ""
msgid "AdminUsers|Can create group"
msgid "AdminUsers|Can create top level group"
msgstr ""
msgid "AdminUsers|Cannot sign in or access instance information"
@ -9197,7 +9197,7 @@ msgstr ""
msgid "Can be manually deployed to"
msgstr ""
msgid "Can create groups:"
msgid "Can create top level groups:"
msgstr ""
msgid "Can not delete primary training"
@ -54946,6 +54946,9 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
msgid "already assigned to an epic"
msgstr ""
msgid "already banned from namespace"
msgstr ""

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module QA
FactoryBot.define do
factory :project_label, class: 'QA::Resource::ProjectLabel'
factory :group_label, class: 'QA::Resource::GroupLabel'
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module QA
FactoryBot.define do
factory :project_milestone, class: 'QA::Resource::ProjectMilestone'
factory :group_milestone, class: 'QA::Resource::GroupMilestone'
end
end

View File

@ -5,6 +5,8 @@ module QA
module Project
module Settings
class ProtectedBranches < Page::Base
include Page::Component::ListboxFilter
view 'app/views/protected_branches/shared/_index.html.haml' do
element 'add-protected-branch-button'
end
@ -14,11 +16,9 @@ module QA
element :protected_branch_dropdown_content
end
view 'app/views/protected_branches/_create_protected_branch.html.haml' do
element :select_allowed_to_push_dropdown
element :allowed_to_push_dropdown_content
element :select_allowed_to_merge_dropdown
element :allowed_to_merge_dropdown_content
view 'app/assets/javascripts/protected_branches/protected_branch_create.js' do
element 'allowed-to-push-dropdown'
element 'allowed-to-merge-dropdown'
end
view 'app/views/protected_branches/shared/_create_protected_branch.html.haml' do
@ -50,19 +50,19 @@ module QA
private
def select_allowed(action, allowed)
click_element :"select_allowed_to_#{action}_dropdown"
within_element("allowed-to-#{action}-dropdown") do
click_element ".js-allowed-to-#{action}"
allowed[:roles] = Resource::ProtectedBranch::Roles::NO_ONE unless allowed.key?(:roles)
allowed[:roles] = Resource::ProtectedBranch::Roles::NO_ONE unless allowed.key?(:roles)
within_element(:"allowed_to_#{action}_dropdown_content") do
click_on allowed[:roles][:description]
allowed[:users].each { |user| select_name user.username } if allowed.key?(:users)
allowed[:groups].each { |group| select_name group.name } if allowed.key?(:groups)
end
end
def select_name(name)
fill_element(:dropdown_input_field, name)
fill_element('.gl-search-box-by-type-input', name)
click_on name
end
end

View File

@ -16,16 +16,15 @@ module QA
let(:imported_subgroup) { build(:group, api_client: api_client, sandbox: imported_group, path: subgroup.path) }
before do
Resource::GroupLabel.fabricate_via_api! do |label|
label.api_client = source_admin_api_client
label.group = source_group
label.title = "source-group-#{SecureRandom.hex(4)}"
end
Resource::GroupLabel.fabricate_via_api! do |label|
label.api_client = source_admin_api_client
label.group = subgroup
label.title = "subgroup-#{SecureRandom.hex(4)}"
end
create(:group_label,
api_client: source_admin_api_client,
group: source_group,
title: "source-group-label-#{SecureRandom.hex(4)}")
create(:group_label,
api_client: source_admin_api_client,
group: subgroup,
title: "source-group-label-#{SecureRandom.hex(4)}")
imported_group # trigger import
end
@ -47,12 +46,7 @@ module QA
end
context 'with milestones and badges' do
let(:source_milestone) do
Resource::GroupMilestone.fabricate_via_api! do |milestone|
milestone.api_client = source_admin_api_client
milestone.group = source_group
end
end
let(:source_milestone) { create(:group_milestone, api_client: source_admin_api_client, group: source_group) }
before do
source_milestone

View File

@ -9,13 +9,7 @@ module QA
let!(:tag) { 'v0.0.1' }
let!(:source_project_with_readme) { true }
let!(:milestone) do
Resource::ProjectMilestone.fabricate_via_api! do |resource|
resource.project = source_project
resource.api_client = source_admin_api_client
end
end
let!(:milestone) { create(:project_milestone, project: source_project, api_client: source_admin_api_client) }
let(:source_release) { comparable_release(source_project.releases.find { |r| r[:tag_name] == tag }) }
let(:imported_release) { comparable_release(imported_releases.find { |r| r[:tag_name] == tag }) }
let(:imported_releases) { imported_project.releases }

View File

@ -44,26 +44,14 @@ module QA
end
context 'Group milestone' do
let(:milestone) do
Resource::GroupMilestone.fabricate_via_api! do |milestone|
milestone.group = group
milestone.start_date = start_date
milestone.due_date = due_date
end
end
let(:milestone) { create(:group_milestone, group: group, start_date: start_date, due_date: due_date) }
it_behaves_like 'milestone assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347964'
it_behaves_like 'milestone assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347965'
end
context 'Project milestone' do
let(:milestone) do
Resource::ProjectMilestone.fabricate_via_api! do |milestone|
milestone.project = project
milestone.start_date = start_date
milestone.due_date = due_date
end
end
let(:milestone) { create(:project_milestone, project: project, start_date: start_date, due_date: due_date) }
it_behaves_like 'milestone assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347962'
it_behaves_like 'milestone assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347963'

View File

@ -35,14 +35,9 @@ module QA
) do
gitlab_account_user_name = Resource::User.default.reload!.name
milestone = Resource::ProjectMilestone.fabricate_via_api! do |milestone|
milestone.project = project
end
milestone = create(:project_milestone, project: project)
label = Resource::ProjectLabel.fabricate_via_api! do |label|
label.project = project
label.title = 'foo::label'
end
label = create(:project_label, project: project, title: 'foo::label')
Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request|
merge_request.title = merge_request_title

View File

@ -68,72 +68,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
end
end
context 'when add_prepared_state_to_mr feature flag on' do
before do
stub_feature_flags(add_prepared_state_to_mr: true)
end
context 'when the merge request is not prepared' do
before do
merge_request.update!(prepared_at: nil, created_at: 10.minutes.ago)
end
it 'prepares the merge request' do
expect(NewMergeRequestWorker).to receive(:perform_async)
go
end
context 'when the merge request was created less than 5 minutes ago' do
it 'does not prepare the merge request again' do
travel_to(4.minutes.from_now) do
merge_request.update!(created_at: Time.current - 4.minutes)
expect(NewMergeRequestWorker).not_to receive(:perform_async)
go
end
end
end
context 'when the merge request was created 5 minutes ago' do
it 'prepares the merge request' do
travel_to(6.minutes.from_now) do
merge_request.update!(created_at: Time.current - 6.minutes)
expect(NewMergeRequestWorker).to receive(:perform_async)
go
end
end
end
end
context 'when the merge request is prepared' do
before do
merge_request.update!(prepared_at: Time.current, created_at: 10.minutes.ago)
end
it 'prepares the merge request' do
expect(NewMergeRequestWorker).not_to receive(:perform_async)
go
end
end
end
context 'when add_prepared_state_to_mr feature flag is off' do
before do
stub_feature_flags(add_prepared_state_to_mr: false)
end
it 'does not prepare the merge request again' do
expect(NewMergeRequestWorker).not_to receive(:perform_async)
go
end
end
describe 'as html' do
it 'sets the endpoint_metadata_url' do
go

View File

@ -56,6 +56,14 @@ FactoryBot.define do
state_id { MergeRequest.available_states[:merged] }
end
trait :unprepared do
prepared_at { nil }
end
trait :prepared do
prepared_at { Time.now }
end
trait :with_merged_metrics do
merged

View File

@ -0,0 +1,45 @@
import MockAdapter from 'axios-mock-adapter';
import * as applicationSettingsApi from '~/api/application_settings_api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/application_settings_api.js', () => {
const MOCK_SETTINGS_RES = { test_setting: 'foo' };
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
window.gon = { api_version: 'v7' };
});
afterEach(() => {
mock.restore();
});
describe('getApplicationSettings', () => {
it('fetches application settings', () => {
const expectedUrl = '/api/v7/application/settings';
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, MOCK_SETTINGS_RES);
return applicationSettingsApi.getApplicationSettings().then(({ data }) => {
expect(data).toEqual(MOCK_SETTINGS_RES);
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
});
});
});
describe('updateApplicationSettings', () => {
it('updates application settings', () => {
const expectedUrl = '/api/v7/application/settings';
const MOCK_REQ = { another_setting: 'bar' };
jest.spyOn(axios, 'put');
mock.onPut(expectedUrl).reply(HTTP_STATUS_OK, MOCK_SETTINGS_RES);
return applicationSettingsApi.updateApplicationSettings(MOCK_REQ).then(({ data }) => {
expect(data).toEqual(MOCK_SETTINGS_RES);
expect(axios.put).toHaveBeenCalledWith(expectedUrl, MOCK_REQ);
});
});
});
});

View File

@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
<div
class="file-title-flex-parent js-file-title"
>
<div
class="gl-display-flex"
>
<table-of-contents-stub
class="gl-pr-2"
/>
<blob-filepath-stub
blob="[object Object]"
showpath="true"
/>
</div>
<div
class="file-actions gl-display-flex gl-flex-wrap"
>
<viewer-switcher-stub
docicon="document"
value="simple"
/>
<default-actions-stub
activeviewer="simple"
rawpath="https://testing.com/flightjs/flight/snippets/51/raw"
/>
</div>
</div>
`;

View File

@ -1,4 +1,6 @@
import Vue from 'vue';
import { shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BlobHeader from '~/blob/components/blob_header.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
@ -10,8 +12,14 @@ import {
SIMPLE_BLOB_VIEWER_TITLE,
} from '~/blob/components/constants';
import TableContents from '~/blob/components/table_contents.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import userInfoQuery from '~/blob/queries/user_info.query.graphql';
import applicationInfoQuery from '~/blob/queries/application_info.query.graphql';
import { Blob, userInfoMock, applicationInfoMock } from './mock_data';
import { Blob } from './mock_data';
Vue.use(VueApollo);
describe('Blob Header Default Actions', () => {
let wrapper;
@ -26,14 +34,29 @@ describe('Blob Header Default Actions', () => {
const findBlobFilePath = () => wrapper.findComponent(BlobFilepath);
const findRichTextEditorBtn = () => wrapper.findByLabelText(RICH_BLOB_VIEWER_TITLE);
const findSimpleTextEditorBtn = () => wrapper.findByLabelText(SIMPLE_BLOB_VIEWER_TITLE);
const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
function createComponent({
async function createComponent({
blobProps = {},
options = {},
propsData = {},
mountFn = shallowMount,
} = {}) {
const userInfoMockResolver = jest.fn().mockResolvedValue({
data: { ...userInfoMock },
});
const applicationInfoMockResolver = jest.fn().mockResolvedValue({
data: { ...applicationInfoMock },
});
const fakeApollo = createMockApollo([
[userInfoQuery, userInfoMockResolver],
[applicationInfoQuery, applicationInfoMockResolver],
]);
wrapper = mountFn(BlobHeader, {
apolloProvider: fakeApollo,
provide: {
...defaultProvide,
},
@ -43,12 +66,40 @@ describe('Blob Header Default Actions', () => {
},
...options,
});
await waitForPromises();
}
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
describe('WebIdeLink component', () => {
it('renders the WebIdeLink component with the correct props', async () => {
const { ideEditPath, editBlobPath, gitpodBlobUrl, pipelineEditorPath } = Blob;
const showForkSuggestion = false;
await createComponent({ propsData: { showForkSuggestion } });
expect(findWebIdeLink().props()).toMatchObject({
showEditButton: true,
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
needsToFork: showForkSuggestion,
showPipelineEditorButton: Boolean(pipelineEditorPath),
pipelineEditorUrl: pipelineEditorPath,
gitpodUrl: gitpodBlobUrl,
showGitpodButton: applicationInfoMock.gitpodEnabled,
gitpodEnabled: userInfoMock.currentUser.gitpodEnabled,
userPreferencesGitpodPath: userInfoMock.currentUser.preferencesGitpodPath,
userProfileEnableGitpodPath: userInfoMock.currentUser.profileEnableGitpodPath,
});
});
it.each([[{ archived: true }], [{ editBlobPath: null }]])(
'does not render the WebIdeLink component when blob is archived or does not have an edit path',
(blobProps) => {
createComponent({ blobProps });
expect(findWebIdeLink().exists()).toBe(false);
},
);
});
describe('default render', () => {

View File

@ -30,6 +30,10 @@ export const Blob = {
richViewer: {
...RichViewerMock,
},
ideEditPath: 'ide/edit',
editBlobPath: 'edit/blob',
gitpodBlobUrl: 'gitpod/blob/url',
pipelineEditorPath: 'pipeline/editor/path',
};
export const BinaryBlob = {
@ -60,3 +64,14 @@ export const SimpleBlobContentMock = {
export const mockEnvironmentName = 'my.testing.environment';
export const mockEnvironmentPath = 'https://my.testing.environment';
export const userInfoMock = {
currentUser: {
id: '123',
gitpodEnabled: true,
preferencesGitpodPath: '/-/profile/preferences#user_gitpod_enabled',
profileEnableGitpodPath: '/-/profile?user%5Bgitpod_enabled%5D=true',
},
};
export const applicationInfoMock = { gitpodEnabled: true };

View File

@ -394,4 +394,20 @@ describe('Access Level Dropdown', () => {
expect(wrapper.emitted('hidden')[0][0]).toStrictEqual([{ access_level: 2 }]);
});
});
describe('when no license and accessLevel is MERGE', () => {
beforeEach(async () => {
createComponent({ hasLicense: false, accessLevel: ACCESS_LEVELS.MERGE });
await waitForPromises();
});
it('dropdown is single-select', () => {
const dropdownItems = findAllDropdownItems();
findDropdownItemWithText(dropdownItems, mockAccessLevelsData[0].text).trigger('click');
findDropdownItemWithText(dropdownItems, mockAccessLevelsData[1].text).trigger('click');
expect(wrapper.emitted('select')[1]).toHaveLength(1);
});
});
});

View File

@ -1,5 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import { ACCESS_LEVELS } from '~/protected_branches/constants';
import axios from '~/lib/utils/axios_utils';
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
const CODE_OWNER_TOGGLE_TESTID = 'code-owner-toggle';
@ -9,7 +12,12 @@ const IS_LOADING_CLASS = 'toggle-loading';
describe('ProtectedBranchCreate', () => {
beforeEach(() => {
jest.spyOn(ProtectedBranchCreate.prototype, 'buildDropdowns').mockImplementation();
// eslint-disable-next-line no-unused-vars
const mock = new MockAdapter(axios);
window.gon = {
merge_access_levels: { roles: [] },
push_access_levels: { roles: [] },
};
});
const findForcePushToggle = () =>
@ -34,6 +42,12 @@ describe('ProtectedBranchCreate', () => {
data-label="Toggle code owner approval"
data-is-checked="${codeOwnerToggleChecked}"
data-testid="${CODE_OWNER_TOGGLE_TESTID}"></span>
<div class="merge_access_levels-container">
<div class="js-allowed-to-merge"/>
</div>
<div class="push_access_levels-container">
<div class="js-allowed-to-push"/>
</div>
<input type="submit" />
</form>
`);
@ -85,14 +99,6 @@ describe('ProtectedBranchCreate', () => {
forcePushToggleChecked: false,
codeOwnerToggleChecked: true,
});
// Mock access levels. This should probably be improved in future iterations.
protectedBranchCreate.merge_access_levels_dropdown = {
getSelectedItems: () => [],
};
protectedBranchCreate.push_access_levels_dropdown = {
getSelectedItems: () => [],
};
});
afterEach(() => {
@ -116,4 +122,31 @@ describe('ProtectedBranchCreate', () => {
});
});
});
describe('access dropdown', () => {
let protectedBranchCreate;
beforeEach(() => {
protectedBranchCreate = create();
});
it('should be initialized', () => {
expect(protectedBranchCreate[`${ACCESS_LEVELS.MERGE}_dropdown`]).toBeDefined();
expect(protectedBranchCreate[`${ACCESS_LEVELS.PUSH}_dropdown`]).toBeDefined();
});
describe('`select` event is emitted', () => {
const selected = ['foo', 'bar'];
it('should update selected merged access items', () => {
protectedBranchCreate[`${ACCESS_LEVELS.MERGE}_dropdown`].$emit('select', selected);
expect(protectedBranchCreate.selectedItems[ACCESS_LEVELS.MERGE]).toEqual(selected);
});
it('should update selected push access items', () => {
protectedBranchCreate[`${ACCESS_LEVELS.PUSH}_dropdown`].$emit('select', selected);
expect(protectedBranchCreate.selectedItems[ACCESS_LEVELS.PUSH]).toEqual(selected);
});
});
});
});

View File

@ -8,11 +8,11 @@ import MockAdapter from 'axios-mock-adapter';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
@ -20,8 +20,6 @@ import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import userInfoQuery from '~/repository/queries/user_info.query.graphql';
import applicationInfoQuery from '~/repository/queries/application_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import * as urlUtility from '~/lib/utils/url_utility';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
@ -34,8 +32,6 @@ import {
simpleViewerMock,
richViewerMock,
projectMock,
userInfoMock,
applicationInfoMock,
userPermissionsMock,
propsMock,
refMock,
@ -46,12 +42,11 @@ jest.mock('~/repository/components/blob_viewers');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/lib/utils/common_utils');
jest.mock('~/blob/line_highlighter');
jest.mock('~/alert');
let wrapper;
let blobInfoMockResolver;
let userInfoMockResolver;
let projectInfoMockResolver;
let applicationInfoMockResolver;
Vue.use(Vuex);
@ -95,7 +90,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
const projectInfo = {
__typename: 'Project',
id: '123',
id: projectMock.id,
userPermissions: {
pushCode,
forkProject,
@ -121,19 +116,9 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
data: { isBinary, project: blobInfo },
});
userInfoMockResolver = jest.fn().mockResolvedValue({
data: { ...userInfoMock },
});
applicationInfoMockResolver = jest.fn().mockResolvedValue({
data: { ...applicationInfoMock },
});
const fakeApollo = createMockApollo([
[blobInfoQuery, blobInfoMockResolver],
[userInfoQuery, userInfoMockResolver],
[projectInfoQuery, projectInfoMockResolver],
[applicationInfoQuery, applicationInfoMockResolver],
]);
wrapper = extendedWrapper(
@ -167,7 +152,6 @@ const execImmediately = (callback) => {
describe('Blob content viewer component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
@ -197,9 +181,22 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
expect(findBlobHeader().props('showForkSuggestion')).toEqual(false);
expect(findBlobHeader().props('projectPath')).toEqual(propsMock.projectPath);
expect(findBlobHeader().props('projectId')).toEqual(projectMock.id);
expect(mockRouterPush).not.toHaveBeenCalled();
});
it('creates an alert when the BlobHeader component emits an error', async () => {
await createComponent();
findBlobHeader().vm.$emit('error');
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while loading the file. Please try again.',
});
});
it('copies blob text to clipboard', async () => {
jest.spyOn(navigator.clipboard, 'writeText');
await createComponent();
@ -401,45 +398,6 @@ describe('Blob content viewer component', () => {
});
describe('BlobHeader action slot', () => {
const { ideEditPath, editBlobPath } = simpleViewerMock;
it('renders WebIdeLink button in simple viewer', async () => {
await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount);
expect(findWebIdeLink().props()).toMatchObject({
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
showEditButton: true,
showGitpodButton: applicationInfoMock.gitpodEnabled,
gitpodEnabled: userInfoMock.currentUser.gitpodEnabled,
showPipelineEditorButton: true,
gitpodUrl: simpleViewerMock.gitpodBlobUrl,
pipelineEditorUrl: simpleViewerMock.pipelineEditorPath,
userPreferencesGitpodPath: userInfoMock.currentUser.preferencesGitpodPath,
userProfileEnableGitpodPath: userInfoMock.currentUser.profileEnableGitpodPath,
});
});
it('renders WebIdeLink button in rich viewer', async () => {
await createComponent({ blob: richViewerMock }, mount);
expect(findWebIdeLink().props()).toMatchObject({
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
showEditButton: true,
});
});
it('renders WebIdeLink button for binary files', async () => {
mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, axiosMockResponse);
await createComponent({}, mount);
expect(findWebIdeLink().props()).toMatchObject({
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
showEditButton: false,
});
});
describe('blob header binary file', () => {
it('passes the correct isBinary value when viewing a binary file', async () => {
mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, axiosMockResponse);
@ -465,7 +423,6 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
expect(findBlobHeader().props('isBinary')).toBe(true);
expect(findWebIdeLink().props('showEditButton')).toBe(false);
});
});
@ -538,12 +495,12 @@ describe('Blob content viewer component', () => {
beforeEach(() => createComponent({}, mount));
it('simple edit redirects to the simple editor', () => {
findWebIdeLink().vm.$emit('edit', 'simple');
findBlobHeader().vm.$emit('edit', 'simple');
expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath); // eslint-disable-line import/no-deprecated
});
it('IDE edit redirects to the IDE editor', () => {
findWebIdeLink().vm.$emit('edit', 'ide');
findBlobHeader().vm.$emit('edit', 'ide');
expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath); // eslint-disable-line import/no-deprecated
});
@ -572,7 +529,7 @@ describe('Blob content viewer component', () => {
mount,
);
findWebIdeLink().vm.$emit('edit', 'simple');
findBlobHeader().vm.$emit('edit', 'simple');
await nextTick();
expect(findForkSuggestion().exists()).toBe(showForkSuggestion);

View File

@ -73,17 +73,6 @@ export const projectMock = {
},
};
export const userInfoMock = {
currentUser: {
id: '123',
gitpodEnabled: true,
preferencesGitpodPath: '/-/profile/preferences#user_gitpod_enabled',
profileEnableGitpodPath: '/-/profile?user%5Bgitpod_enabled%5D=true',
},
};
export const applicationInfoMock = { gitpodEnabled: true };
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref';

View File

@ -236,9 +236,14 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo open') }
let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') }
let_it_be(:unarchived_project) { project }
let_it_be(:archived_project) { create(:project, :public, :archived) }
let_it_be(:unarchived_result) { create(:issue, project: unarchived_project, title: 'foo unarchived') }
let_it_be(:archived_result) { create(:issue, project: archived_project, title: 'foo archived') }
include_examples 'search results filtered by state'
include_examples 'search results filtered by confidential'
include_examples 'search results filtered by archived', 'search_issues_hide_archived_projects'
end
context 'ordering' do

View File

@ -135,10 +135,22 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:merge_request1) { create(:merge_request, :unique_branches, reviewers: [user1]) }
let_it_be(:merge_request2) { create(:merge_request, :unique_branches, reviewers: [user2]) }
let_it_be(:merge_request3) { create(:merge_request, :unique_branches, reviewers: []) }
let_it_be(:merge_request4) { create(:merge_request, :draft_merge_request) }
let_it_be(:merge_request1) do
create(:merge_request, :prepared, :unique_branches, reviewers: [user1], created_at:
2.days.ago)
end
let_it_be(:merge_request2) do
create(:merge_request, :unprepared, :unique_branches, reviewers: [user2], created_at:
3.hours.ago)
end
let_it_be(:merge_request3) do
create(:merge_request, :unprepared, :unique_branches, reviewers: [], created_at:
Time.current)
end
let_it_be(:merge_request4) { create(:merge_request, :prepared, :draft_merge_request) }
describe '.preload_target_project_with_namespace' do
subject(:mr) { described_class.preload_target_project_with_namespace.first }
@ -180,6 +192,14 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
describe '.recently_unprepared' do
it 'only returns the recently unprepared mrs' do
merge_request5 = create(:merge_request, :unprepared, :unique_branches, created_at: merge_request3.created_at)
expect(described_class.recently_unprepared).to eq([merge_request3, merge_request5])
end
end
describe '.by_sorted_source_branches' do
let(:fork_for_project) { fork_project(project) }

View File

@ -4,7 +4,13 @@
class ClickHouseTestRunner
def truncate_tables
ClickHouse::Client.configuration.databases.each_key do |db|
tables_for(db).each do |table|
# Select tables with at least one row
query = tables_for(db).map do |table|
"(SELECT '#{table}' AS table FROM #{table} LIMIT 1)"
end.join(' UNION ALL ')
tables_with_data = ClickHouse::Client.select(query, db).pluck('table')
tables_with_data.each do |table|
ClickHouse::Client.execute("TRUNCATE TABLE #{table}", db)
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MergeRequests::EnsurePreparedWorker, :sidekiq_inline, feature_category: :code_review_workflow do
subject(:worker) { described_class.new }
let_it_be(:merge_request_1, reload: true) { create(:merge_request, prepared_at: :nil) }
let_it_be(:merge_request_2, reload: true) { create(:merge_request, prepared_at: Time.current) }
let_it_be(:merge_request_3, reload: true) { create(:merge_request, prepared_at: :nil) }
describe '#perform' do
context 'when ensure_merge_requests_prepared is enabled' do
it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
worker.perform
expect(merge_request_1.reload.prepared_at).not_to eq(nil)
expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.reload.prepared_at).not_to eq(nil)
end
end
context 'when ensure_merge_requests_prepared is disabled' do
before do
stub_feature_flags(ensure_merge_requests_prepared: false)
end
it 'does not prepare any merge requests' do
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
worker.perform
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
end
end
end
it_behaves_like 'an idempotent worker' do
it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
subject
expect(merge_request_1.reload.prepared_at).not_to eq(nil)
expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.reload.prepared_at).not_to eq(nil)
end
end
end

View File

@ -88,33 +88,25 @@ RSpec.describe NewMergeRequestWorker, feature_category: :code_review_workflow do
worker.perform(merge_request.id, user.id)
end
context 'when add_prepared_state_to_mr feature flag is off' do
context 'when the merge request is prepared' do
before do
stub_feature_flags(add_prepared_state_to_mr: false)
merge_request.update!(prepared_at: Time.current)
end
it 'calls the create service' do
expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
expect(service).to receive(:execute).with(merge_request)
end
it 'does not call the create service' do
expect(MergeRequests::AfterCreateService).not_to receive(:new)
worker.perform(merge_request.id, user.id)
end
end
context 'when add_prepared_state_to_mr feature flag is on' do
before do
stub_feature_flags(add_prepared_state_to_mr: true)
end
context 'when the merge request is not prepared' do
it 'calls the create service' do
expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
expect(service).to receive(:execute).with(merge_request)
end
worker.perform(merge_request.id, user.id)
context 'when the merge request is not prepared' do
it 'calls the create service' do
expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
expect(service).to receive(:execute).with(merge_request).and_call_original
end
worker.perform(merge_request.id, user.id)
end
end
end