Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									73e7987ee3
								
							
						
					
					
						commit
						132b8909c8
					
				|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| --- | ||||
| # Cop supports --autocorrect. | ||||
| Gitlab/StrongMemoizeAttr: | ||||
|   Details: grace period | ||||
|   Exclude: | ||||
|     - 'app/components/pajamas/avatar_component.rb' | ||||
|     - 'app/controllers/application_controller.rb' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| --- | ||||
| RSpec/UselessDynamicDefinition: | ||||
|   Details: grace period | ||||
|   Exclude: | ||||
|     - 'ee/spec/factories/ci/builds.rb' | ||||
|     - 'ee/spec/factories/ci/job_artifacts.rb' | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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); | ||||
| } | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -116,6 +116,6 @@ export default class CreateItemDropdown { | |||
|   } | ||||
| 
 | ||||
|   close() { | ||||
|     this.$dropdown.data('deprecatedJQueryDropdown').close(); | ||||
|     this.$dropdown.data('deprecatedJQueryDropdown')?.close(); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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.
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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('*', '%')) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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'] ||= {} | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
| # | ||||
|  |  | |||
|  | @ -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. | ||||
| # | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -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) | ||||
|  | @ -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 | ||||
|  | @ -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). | ||||
|  |  | |||
|  | @ -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. | | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -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`. | ||||
|  |  | |||
|  | @ -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`. | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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 "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 } | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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> | ||||
| `; | ||||
|  | @ -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', () => { | ||||
|  |  | |||
|  | @ -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 }; | ||||
|  |  | |||
|  | @ -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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue