Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									28cff3de2b
								
							
						
					
					
						commit
						b17372c8e7
					
				|  | @ -233,7 +233,6 @@ Rails/Pluck: | ||||||
|     - 'spec/requests/groups/autocomplete_sources_spec.rb' |     - 'spec/requests/groups/autocomplete_sources_spec.rb' | ||||||
|     - 'spec/requests/groups/milestones_controller_spec.rb' |     - 'spec/requests/groups/milestones_controller_spec.rb' | ||||||
|     - 'spec/requests/lfs_http_spec.rb' |     - 'spec/requests/lfs_http_spec.rb' | ||||||
|     - 'spec/serializers/ci/dag_pipeline_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/pipeline_entity_spec.rb' |     - 'spec/serializers/ci/pipeline_entity_spec.rb' | ||||||
|     - 'spec/serializers/diff_file_entity_spec.rb' |     - 'spec/serializers/diff_file_entity_spec.rb' | ||||||
|     - 'spec/serializers/stage_entity_spec.rb' |     - 'spec/serializers/stage_entity_spec.rb' | ||||||
|  |  | ||||||
|  | @ -414,11 +414,6 @@ RSpec/FactoryBot/AvoidCreate: | ||||||
|     - 'spec/serializers/build_action_entity_spec.rb' |     - 'spec/serializers/build_action_entity_spec.rb' | ||||||
|     - 'spec/serializers/build_artifact_entity_spec.rb' |     - 'spec/serializers/build_artifact_entity_spec.rb' | ||||||
|     - 'spec/serializers/build_details_entity_spec.rb' |     - 'spec/serializers/build_details_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/dag_job_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_job_group_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_pipeline_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_pipeline_serializer_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_stage_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/downloadable_artifact_entity_spec.rb' |     - 'spec/serializers/ci/downloadable_artifact_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/downloadable_artifact_serializer_spec.rb' |     - 'spec/serializers/ci/downloadable_artifact_serializer_spec.rb' | ||||||
|     - 'spec/serializers/ci/job_entity_spec.rb' |     - 'spec/serializers/ci/job_entity_spec.rb' | ||||||
|  |  | ||||||
|  | @ -3863,11 +3863,6 @@ RSpec/FeatureCategory: | ||||||
|     - 'spec/serializers/build_details_entity_spec.rb' |     - 'spec/serializers/build_details_entity_spec.rb' | ||||||
|     - 'spec/serializers/build_trace_entity_spec.rb' |     - 'spec/serializers/build_trace_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb' |     - 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb' | ||||||
|     - 'spec/serializers/ci/dag_job_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_job_group_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_pipeline_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_pipeline_serializer_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_stage_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/daily_build_group_report_result_entity_spec.rb' |     - 'spec/serializers/ci/daily_build_group_report_result_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb' |     - 'spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb' | ||||||
|     - 'spec/serializers/ci/downloadable_artifact_entity_spec.rb' |     - 'spec/serializers/ci/downloadable_artifact_entity_spec.rb' | ||||||
|  |  | ||||||
|  | @ -2817,11 +2817,6 @@ RSpec/NamedSubject: | ||||||
|     - 'spec/serializers/build_details_entity_spec.rb' |     - 'spec/serializers/build_details_entity_spec.rb' | ||||||
|     - 'spec/serializers/build_trace_entity_spec.rb' |     - 'spec/serializers/build_trace_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb' |     - 'spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb' | ||||||
|     - 'spec/serializers/ci/dag_job_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_job_group_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_pipeline_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_pipeline_serializer_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_stage_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/downloadable_artifact_entity_spec.rb' |     - 'spec/serializers/ci/downloadable_artifact_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/downloadable_artifact_serializer_spec.rb' |     - 'spec/serializers/ci/downloadable_artifact_serializer_spec.rb' | ||||||
|     - 'spec/serializers/ci/group_variable_entity_spec.rb' |     - 'spec/serializers/ci/group_variable_entity_spec.rb' | ||||||
|  |  | ||||||
|  | @ -725,10 +725,6 @@ RSpec/VerifiedDoubles: | ||||||
|     - 'spec/serializers/build_action_entity_spec.rb' |     - 'spec/serializers/build_action_entity_spec.rb' | ||||||
|     - 'spec/serializers/build_details_entity_spec.rb' |     - 'spec/serializers/build_details_entity_spec.rb' | ||||||
|     - 'spec/serializers/build_trace_entity_spec.rb' |     - 'spec/serializers/build_trace_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/dag_job_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_job_group_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_pipeline_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/dag_stage_entity_spec.rb' |  | ||||||
|     - 'spec/serializers/ci/daily_build_group_report_result_entity_spec.rb' |     - 'spec/serializers/ci/daily_build_group_report_result_entity_spec.rb' | ||||||
|     - 'spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb' |     - 'spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb' | ||||||
|     - 'spec/serializers/ci/job_entity_spec.rb' |     - 'spec/serializers/ci/job_entity_spec.rb' | ||||||
|  |  | ||||||
|  | @ -489,9 +489,6 @@ Style/InlineDisableAnnotation: | ||||||
|     - 'app/serializers/analytics/cycle_analytics/value_stream_entity.rb' |     - 'app/serializers/analytics/cycle_analytics/value_stream_entity.rb' | ||||||
|     - 'app/serializers/analytics_build_entity.rb' |     - 'app/serializers/analytics_build_entity.rb' | ||||||
|     - 'app/serializers/analytics_issue_entity.rb' |     - 'app/serializers/analytics_issue_entity.rb' | ||||||
|     - 'app/serializers/ci/dag_job_entity.rb' |  | ||||||
|     - 'app/serializers/ci/dag_pipeline_entity.rb' |  | ||||||
|     - 'app/serializers/ci/job_entity.rb' |  | ||||||
|     - 'app/serializers/cluster_entity.rb' |     - 'app/serializers/cluster_entity.rb' | ||||||
|     - 'app/serializers/diffs_metadata_entity.rb' |     - 'app/serializers/diffs_metadata_entity.rb' | ||||||
|     - 'app/serializers/environment_serializer.rb' |     - 'app/serializers/environment_serializer.rb' | ||||||
|  |  | ||||||
|  | @ -1,2 +0,0 @@ | ||||||
| // /dag is an alias for show
 |  | ||||||
| import '../show/index'; |  | ||||||
|  | @ -24,11 +24,6 @@ export default { | ||||||
|       type: Object, |       type: Object, | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|     fadeDoneTodo: { |  | ||||||
|       type: Boolean, |  | ||||||
|       required: false, |  | ||||||
|       default: false, |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     isDone() { |     isDone() { | ||||||
|  | @ -40,9 +35,6 @@ export default { | ||||||
|     targetUrl() { |     targetUrl() { | ||||||
|       return this.todo.targetUrl; |       return this.todo.targetUrl; | ||||||
|     }, |     }, | ||||||
|     fadeTodo() { |  | ||||||
|       return this.fadeDoneTodo && this.isDone; |  | ||||||
|     }, |  | ||||||
|     trackingLabel() { |     trackingLabel() { | ||||||
|       return this.todo.targetType ?? 'UNKNOWN'; |       return this.todo.targetType ?? 'UNKNOWN'; | ||||||
|     }, |     }, | ||||||
|  | @ -53,7 +45,6 @@ export default { | ||||||
| <template> | <template> | ||||||
|   <li |   <li | ||||||
|     class="gl-border-t gl-border-b gl-relative -gl-mt-px gl-block gl-px-5 gl-py-3 hover:gl-z-1 hover:gl-cursor-pointer hover:gl-border-blue-200 hover:gl-bg-blue-50" |     class="gl-border-t gl-border-b gl-relative -gl-mt-px gl-block gl-px-5 gl-py-3 hover:gl-z-1 hover:gl-cursor-pointer hover:gl-border-blue-200 hover:gl-bg-blue-50" | ||||||
|     :class="{ 'gl-border-gray-50 gl-bg-gray-10': fadeTodo }" |  | ||||||
|   > |   > | ||||||
|     <gl-link |     <gl-link | ||||||
|       :href="targetUrl" |       :href="targetUrl" | ||||||
|  | @ -63,16 +54,18 @@ export default { | ||||||
|     > |     > | ||||||
|       <div |       <div | ||||||
|         class="gl-w-64 gl-flex-grow-2 gl-self-center gl-overflow-hidden gl-overflow-x-auto sm:gl-w-auto" |         class="gl-w-64 gl-flex-grow-2 gl-self-center gl-overflow-hidden gl-overflow-x-auto sm:gl-w-auto" | ||||||
|         :class="{ 'gl-opacity-5': fadeTodo }" |  | ||||||
|       > |       > | ||||||
|         <todo-item-title :todo="todo" /> |         <todo-item-title :todo="todo" /> | ||||||
|         <todo-item-body :todo="todo" :current-user-id="currentUserId" /> |         <todo-item-body :todo="todo" :current-user-id="currentUserId" /> | ||||||
|       </div> |       </div> | ||||||
|       <todo-item-actions :todo="todo" class="sm:gl-order-3" /> |       <todo-item-actions | ||||||
|  |         :todo="todo" | ||||||
|  |         class="sm:gl-order-3" | ||||||
|  |         @change="(id, markedAsDone) => $emit('change', id, markedAsDone)" | ||||||
|  |       /> | ||||||
|       <todo-item-timestamp |       <todo-item-timestamp | ||||||
|         :todo="todo" |         :todo="todo" | ||||||
|         class="gl-w-full gl-whitespace-nowrap gl-px-2 sm:gl-w-auto" |         class="gl-w-full gl-whitespace-nowrap gl-px-2 sm:gl-w-auto" | ||||||
|         :class="{ 'gl-opacity-5': fadeTodo }" |  | ||||||
|       /> |       /> | ||||||
|     </gl-link> |     </gl-link> | ||||||
|   </li> |   </li> | ||||||
|  |  | ||||||
|  | @ -3,7 +3,12 @@ import { GlButton, GlTooltipDirective } from '@gitlab/ui'; | ||||||
| import { reportToSentry } from '~/ci/utils'; | import { reportToSentry } from '~/ci/utils'; | ||||||
| import { s__ } from '~/locale'; | import { s__ } from '~/locale'; | ||||||
| import Tracking from '~/tracking'; | import Tracking from '~/tracking'; | ||||||
| import { INSTRUMENT_TODO_ITEM_CLICK, TODO_STATE_DONE, TODO_STATE_PENDING } from '../constants'; | import { | ||||||
|  |   INSTRUMENT_TODO_ITEM_CLICK, | ||||||
|  |   TAB_ALL, | ||||||
|  |   TODO_STATE_DONE, | ||||||
|  |   TODO_STATE_PENDING, | ||||||
|  | } from '../constants'; | ||||||
| import markAsDoneMutation from './mutations/mark_as_done.mutation.graphql'; | import markAsDoneMutation from './mutations/mark_as_done.mutation.graphql'; | ||||||
| import markAsPendingMutation from './mutations/mark_as_pending.mutation.graphql'; | import markAsPendingMutation from './mutations/mark_as_pending.mutation.graphql'; | ||||||
| 
 | 
 | ||||||
|  | @ -15,6 +20,7 @@ export default { | ||||||
|     GlTooltip: GlTooltipDirective, |     GlTooltip: GlTooltipDirective, | ||||||
|   }, |   }, | ||||||
|   mixins: [Tracking.mixin()], |   mixins: [Tracking.mixin()], | ||||||
|  |   inject: ['currentTab'], | ||||||
|   props: { |   props: { | ||||||
|     todo: { |     todo: { | ||||||
|       type: Object, |       type: Object, | ||||||
|  | @ -33,6 +39,14 @@ export default { | ||||||
|     isPending() { |     isPending() { | ||||||
|       return this.todo.state === TODO_STATE_PENDING; |       return this.todo.state === TODO_STATE_PENDING; | ||||||
|     }, |     }, | ||||||
|  |     tooltipTitle() { | ||||||
|  |       // Setting this to null while loading, combined with keeping the | ||||||
|  |       // loading state till the item gets removed, prevents the tooltip | ||||||
|  |       // text changing with the item state before the item gets removed. | ||||||
|  |       if (this.isLoading) return null; | ||||||
|  | 
 | ||||||
|  |       return this.isDone ? this.$options.i18n.markAsPending : this.$options.i18n.markAsDone; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     showMarkAsDoneError() { |     showMarkAsDoneError() { | ||||||
|  | @ -75,12 +89,22 @@ export default { | ||||||
|         if (data.errors?.length > 0) { |         if (data.errors?.length > 0) { | ||||||
|           reportToSentry(this.$options.name, new Error(data.errors.join(', '))); |           reportToSentry(this.$options.name, new Error(data.errors.join(', '))); | ||||||
|           showError(); |           showError(); | ||||||
|  |         } else { | ||||||
|  |           this.$emit('change', this.todo.id, this.isDone); | ||||||
|         } |         } | ||||||
|       } catch (failure) { |       } catch (failure) { | ||||||
|         reportToSentry(this.$options.name, failure); |         reportToSentry(this.$options.name, failure); | ||||||
|         showError(); |         showError(); | ||||||
|       } finally { |  | ||||||
|         this.isLoading = false; |         this.isLoading = false; | ||||||
|  |       } finally { | ||||||
|  |         // Only stop loading spinner when on "All" tab. | ||||||
|  |         // On the other tabs (Pending/Done) we want the loading to continue | ||||||
|  |         // until the todos query finished, removing this item from the list. | ||||||
|  |         // This way we hide the state change, which would otherwise update | ||||||
|  |         // the button's icon before it gets removed. | ||||||
|  |         if (this.currentTab === TAB_ALL) { | ||||||
|  |           this.isLoading = false; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  | @ -97,7 +121,7 @@ export default { | ||||||
|     :icon="isDone ? 'redo' : 'check'" |     :icon="isDone ? 'redo' : 'check'" | ||||||
|     :loading="isLoading" |     :loading="isLoading" | ||||||
|     :aria-label="isDone ? $options.i18n.markAsPending : $options.i18n.markAsDone" |     :aria-label="isDone ? $options.i18n.markAsPending : $options.i18n.markAsDone" | ||||||
|     :title="isDone ? $options.i18n.markAsPending : $options.i18n.markAsDone" |     :title="tooltipTitle" | ||||||
|     @click.prevent="toggleStatus" |     @click.prevent="toggleStatus" | ||||||
|   /> |   /> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| <script> | <script> | ||||||
|  | import { computed } from 'vue'; | ||||||
| import { GlLoadingIcon, GlKeysetPagination, GlLink, GlBadge, GlTab, GlTabs } from '@gitlab/ui'; | import { GlLoadingIcon, GlKeysetPagination, GlLink, GlBadge, GlTab, GlTabs } from '@gitlab/ui'; | ||||||
| import * as Sentry from '~/sentry/sentry_browser_wrapper'; | import * as Sentry from '~/sentry/sentry_browser_wrapper'; | ||||||
| import { createAlert } from '~/alert'; | import { createAlert } from '~/alert'; | ||||||
|  | @ -11,6 +12,8 @@ import { | ||||||
| } from '~/todos/constants'; | } from '~/todos/constants'; | ||||||
| import getTodosQuery from './queries/get_todos.query.graphql'; | import getTodosQuery from './queries/get_todos.query.graphql'; | ||||||
| import getPendingTodosCount from './queries/get_pending_todos_count.query.graphql'; | import getPendingTodosCount from './queries/get_pending_todos_count.query.graphql'; | ||||||
|  | import markAsDoneMutation from './mutations/mark_as_done.mutation.graphql'; | ||||||
|  | import markAsPendingMutation from './mutations/mark_as_pending.mutation.graphql'; | ||||||
| import TodoItem from './todo_item.vue'; | import TodoItem from './todo_item.vue'; | ||||||
| import TodosEmptyState from './todos_empty_state.vue'; | import TodosEmptyState from './todos_empty_state.vue'; | ||||||
| import TodosFilterBar, { SORT_OPTIONS } from './todos_filter_bar.vue'; | import TodosFilterBar, { SORT_OPTIONS } from './todos_filter_bar.vue'; | ||||||
|  | @ -32,6 +35,11 @@ export default { | ||||||
|     TodosMarkAllDoneButton, |     TodosMarkAllDoneButton, | ||||||
|   }, |   }, | ||||||
|   mixins: [Tracking.mixin()], |   mixins: [Tracking.mixin()], | ||||||
|  |   provide() { | ||||||
|  |     return { | ||||||
|  |       currentTab: computed(() => this.currentTab), | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       cursor: { |       cursor: { | ||||||
|  | @ -54,11 +62,13 @@ export default { | ||||||
|         sort: `${SORT_OPTIONS[0].value}_DESC`, |         sort: `${SORT_OPTIONS[0].value}_DESC`, | ||||||
|       }, |       }, | ||||||
|       alert: null, |       alert: null, | ||||||
|  |       showSpinnerWhileLoading: true, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   apollo: { |   apollo: { | ||||||
|     todos: { |     todos: { | ||||||
|       query: getTodosQuery, |       query: getTodosQuery, | ||||||
|  |       fetchPolicy: 'cache-and-network', | ||||||
|       variables() { |       variables() { | ||||||
|         return { |         return { | ||||||
|           state: this.statusByTab, |           state: this.statusByTab, | ||||||
|  | @ -107,9 +117,6 @@ export default { | ||||||
|     showMarkAllAsDone() { |     showMarkAllAsDone() { | ||||||
|       return this.currentTab === 0 && !this.showEmptyState; |       return this.currentTab === 0 && !this.showEmptyState; | ||||||
|     }, |     }, | ||||||
|     fadeDoneTodo() { |  | ||||||
|       return this.currentTab === 0; |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     nextPage(item) { |     nextPage(item) { | ||||||
|  | @ -144,9 +151,34 @@ export default { | ||||||
|       this.alert?.dismiss(); |       this.alert?.dismiss(); | ||||||
|       this.queryFilterValues = { ...data }; |       this.queryFilterValues = { ...data }; | ||||||
|     }, |     }, | ||||||
|  |     async handleItemChanged(id, markedAsDone) { | ||||||
|  |       await this.updateAllQueries(false); | ||||||
|  |       this.showUndoToast(id, markedAsDone); | ||||||
|  |     }, | ||||||
|  |     showUndoToast(todoId, markedAsDone) { | ||||||
|  |       const message = markedAsDone ? s__('Todos|Marked as done') : s__('Todos|Marked as undone'); | ||||||
|  |       const mutation = markedAsDone ? markAsPendingMutation : markAsDoneMutation; | ||||||
|  | 
 | ||||||
|  |       const { hide } = this.$toast.show(message, { | ||||||
|  |         action: { | ||||||
|  |           text: s__('Todos|Undo'), | ||||||
|  |           onClick: async () => { | ||||||
|  |             hide(); | ||||||
|  |             await this.$apollo.mutate({ mutation, variables: { todoId } }); | ||||||
|  |             this.updateAllQueries(false); | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|     updateCounts() { |     updateCounts() { | ||||||
|       this.$apollo.queries.pendingTodosCount.refetch(); |       this.$apollo.queries.pendingTodosCount.refetch(); | ||||||
|     }, |     }, | ||||||
|  |     async updateAllQueries(showLoading = true) { | ||||||
|  |       this.showSpinnerWhileLoading = showLoading; | ||||||
|  |       this.updateCounts(); | ||||||
|  |       await this.$apollo.queries.todos.refetch(); | ||||||
|  |       this.showSpinnerWhileLoading = true; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  | @ -184,15 +216,17 @@ export default { | ||||||
| 
 | 
 | ||||||
|     <div> |     <div> | ||||||
|       <div class="gl-flex gl-flex-col"> |       <div class="gl-flex gl-flex-col"> | ||||||
|         <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" /> |         <gl-loading-icon v-if="isLoading && showSpinnerWhileLoading" size="lg" class="gl-mt-5" /> | ||||||
|         <ul v-else class="gl-m-0 gl-border-collapse gl-list-none gl-p-0"> |         <ul v-else class="gl-m-0 gl-border-collapse gl-list-none gl-p-0"> | ||||||
|           <todo-item |           <transition-group name="todos"> | ||||||
|             v-for="todo in todos" |             <todo-item | ||||||
|             :key="todo.id" |               v-for="todo in todos" | ||||||
|             :todo="todo" |               :key="todo.id" | ||||||
|             :current-user-id="currentUserId" |               :todo="todo" | ||||||
|             :fade-done-todo="fadeDoneTodo" |               :current-user-id="currentUserId" | ||||||
|           /> |               @change="handleItemChanged" | ||||||
|  |             /> | ||||||
|  |           </transition-group> | ||||||
|         </ul> |         </ul> | ||||||
| 
 | 
 | ||||||
|         <todos-empty-state v-if="showEmptyState" :is-filtered="isFiltered" /> |         <todos-empty-state v-if="showEmptyState" :is-filtered="isFiltered" /> | ||||||
|  | @ -214,3 +248,17 @@ export default { | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .todos-leave-active { | ||||||
|  |   transition: transform 0.15s ease-out; | ||||||
|  |   position: absolute; | ||||||
|  | } | ||||||
|  | .todos-leave-to { | ||||||
|  |   opacity: 0; | ||||||
|  |   transform: translateY(-100px); | ||||||
|  | } | ||||||
|  | .todos-move { | ||||||
|  |   transition: transform 0.15s ease-out; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ export const TODO_EMPTY_TITLE_POOL = [ | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| export const STATUS_BY_TAB = [['pending'], ['done'], ['pending', 'done']]; | export const STATUS_BY_TAB = [['pending'], ['done'], ['pending', 'done']]; | ||||||
|  | export const TAB_ALL = 2; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Instrumentation |  * Instrumentation | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController | ||||||
|   before_action :authorize_read_group!, only: :index |   before_action :authorize_read_group!, only: :index | ||||||
|   before_action :find_todos, only: [:index, :destroy_all] |   before_action :find_todos, only: [:index, :destroy_all] | ||||||
| 
 | 
 | ||||||
|   feature_category :team_planning |   feature_category :notifications | ||||||
|   urgency :low |   urgency :low | ||||||
| 
 | 
 | ||||||
|   def index |   def index | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ class Projects::PipelinesController < Projects::ApplicationController | ||||||
| 
 | 
 | ||||||
|   urgency :low, [ |   urgency :low, [ | ||||||
|     :index, :new, :builds, :show, :failures, :create, |     :index, :new, :builds, :show, :failures, :create, | ||||||
|     :stage, :retry, :dag, :cancel, :test_report, |     :stage, :retry, :cancel, :test_report, | ||||||
|     :charts, :destroy, :status, :manual_variables |     :charts, :destroy, :status, :manual_variables | ||||||
|   ] |   ] | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +27,7 @@ class Projects::PipelinesController < Projects::ApplicationController | ||||||
|   before_action :authorize_cancel_pipeline!, only: [:cancel] |   before_action :authorize_cancel_pipeline!, only: [:cancel] | ||||||
|   before_action :ensure_pipeline, only: [:show, :downloadable_artifacts] |   before_action :ensure_pipeline, only: [:show, :downloadable_artifacts] | ||||||
|   before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy] |   before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy] | ||||||
|   before_action only: [:show, :dag, :builds, :failures, :test_report, :manual_variables] do |   before_action only: [:show, :builds, :failures, :test_report, :manual_variables] do | ||||||
|     push_frontend_feature_flag(:ci_show_manual_variables_in_pipeline, project) |     push_frontend_feature_flag(:ci_show_manual_variables_in_pipeline, project) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -54,7 +54,7 @@ class Projects::PipelinesController < Projects::ApplicationController | ||||||
| 
 | 
 | ||||||
|   feature_category :continuous_integration, [ |   feature_category :continuous_integration, [ | ||||||
|     :charts, :show, :stage, :cancel, :retry, |     :charts, :show, :stage, :cancel, :retry, | ||||||
|     :builds, :dag, :failures, :status, |     :builds, :failures, :status, | ||||||
|     :index, :new, :destroy, :manual_variables |     :index, :new, :destroy, :manual_variables | ||||||
|   ] |   ] | ||||||
|   feature_category :pipeline_composition, [:create] |   feature_category :pipeline_composition, [:create] | ||||||
|  | @ -144,19 +144,6 @@ class Projects::PipelinesController < Projects::ApplicationController | ||||||
|     render_show |     render_show | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def dag |  | ||||||
|     respond_to do |format| |  | ||||||
|       format.html do |  | ||||||
|         render_show |  | ||||||
|       end |  | ||||||
|       format.json do |  | ||||||
|         render json: Ci::DagPipelineSerializer |  | ||||||
|           .new(project: @project, current_user: @current_user) |  | ||||||
|           .represent(@pipeline) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def failures |   def failures | ||||||
|     if @pipeline.failed_builds.present? |     if @pipeline.failed_builds.present? | ||||||
|       render_show |       render_show | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ class Projects::TodosController < Projects::ApplicationController | ||||||
| 
 | 
 | ||||||
|   before_action :authenticate_user!, only: [:create] |   before_action :authenticate_user!, only: [:create] | ||||||
| 
 | 
 | ||||||
|   feature_category :team_planning |   feature_category :notifications | ||||||
|   urgency :low |   urgency :low | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  |  | ||||||
|  | @ -35,30 +35,21 @@ module Resolvers | ||||||
|         return unless runner.project_type? |         return unless runner.project_type? | ||||||
| 
 | 
 | ||||||
|         BatchLoader::GraphQL.for(runner.id).batch do |runner_ids, loader| |         BatchLoader::GraphQL.for(runner.id).batch do |runner_ids, loader| | ||||||
|           # rubocop: disable CodeReuse/ActiveRecord |           # rubocop: disable CodeReuse/ActiveRecord -- this runs on a limited number of records | ||||||
|           runner_and_projects_with_row_number = |           runner_id_to_owner_id = | ||||||
|             ::Ci::RunnerProject |             ::Ci::Runner.project_type.id_in(runner_ids) | ||||||
|               .where(runner_id: runner_ids) |               .pluck(:id, :sharding_key_id) | ||||||
|               .select('id, runner_id, project_id, ROW_NUMBER() OVER (PARTITION BY runner_id ORDER BY id ASC)') |               .to_h | ||||||
|           runner_and_owner_projects = |           # rubocop: enable CodeReuse/ActiveRecord | ||||||
|             ::Ci::RunnerProject |  | ||||||
|               .select(:id, :runner_id, :project_id) |  | ||||||
|               .from("(#{runner_and_projects_with_row_number.to_sql}) temp WHERE row_number = 1") |  | ||||||
|           owner_project_id_by_runner_id = |  | ||||||
|             runner_and_owner_projects |  | ||||||
|               .group_by(&:runner_id) |  | ||||||
|               .transform_values { |runner_projects| runner_projects.first.project_id } |  | ||||||
|           project_ids = owner_project_id_by_runner_id.values.uniq |  | ||||||
| 
 | 
 | ||||||
|           projects = apply_lookahead(Project.id_in(project_ids)) |           projects = apply_lookahead(Project.id_in(runner_id_to_owner_id.values)) | ||||||
|           Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute |           Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute | ||||||
|           projects_by_id = projects.index_by(&:id) |           projects_by_id = projects.index_by(&:id) | ||||||
| 
 | 
 | ||||||
|           runner_ids.each do |runner_id| |           runner_ids.each do |runner_id| | ||||||
|             owner_project_id = owner_project_id_by_runner_id[runner_id] |             owner_project_id = runner_id_to_owner_id[runner_id] | ||||||
|             loader.call(runner_id, projects_by_id[owner_project_id]) |             loader.call(runner_id, projects_by_id[owner_project_id]) | ||||||
|           end |           end | ||||||
|           # rubocop: enable CodeReuse/ActiveRecord |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -48,15 +48,10 @@ module Ci | ||||||
|           request |           request | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def pipeline_creating_for_merge_request?(merge_request, delete_if_all_complete: false) |         def pipeline_creating_for_merge_request?(merge_request) | ||||||
|           key = merge_request_key(merge_request) |           key = merge_request_key(merge_request) | ||||||
| 
 | 
 | ||||||
|           requests, _del_result = Gitlab::Redis::SharedState.with do |redis| |           requests = Gitlab::Redis::SharedState.with { |redis| redis.hvals(key) } | ||||||
|             redis.multi do |transaction| |  | ||||||
|               transaction.hvals(key) |  | ||||||
|               transaction.del(key) if delete_if_all_complete |  | ||||||
|             end |  | ||||||
|           end |  | ||||||
| 
 | 
 | ||||||
|           return false unless requests.present? |           return false unless requests.present? | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -331,19 +331,18 @@ module Ci | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def runner_matcher |     def runner_matcher | ||||||
|       strong_memoize(:runner_matcher) do |       Gitlab::Ci::Matching::RunnerMatcher.new({ | ||||||
|         Gitlab::Ci::Matching::RunnerMatcher.new({ |         runner_ids: [id], | ||||||
|           runner_ids: [id], |         runner_type: runner_type, | ||||||
|           runner_type: runner_type, |         public_projects_minutes_cost_factor: public_projects_minutes_cost_factor, | ||||||
|           public_projects_minutes_cost_factor: public_projects_minutes_cost_factor, |         private_projects_minutes_cost_factor: private_projects_minutes_cost_factor, | ||||||
|           private_projects_minutes_cost_factor: private_projects_minutes_cost_factor, |         run_untagged: run_untagged, | ||||||
|           run_untagged: run_untagged, |         access_level: access_level, | ||||||
|           access_level: access_level, |         tag_list: tag_list, | ||||||
|           tag_list: tag_list, |         allowed_plan_ids: allowed_plan_ids | ||||||
|           allowed_plan_ids: allowed_plan_ids |       }) | ||||||
|         }) |  | ||||||
|       end |  | ||||||
|     end |     end | ||||||
|  |     strong_memoize_attr :runner_matcher | ||||||
| 
 | 
 | ||||||
|     def assign_to(project, current_user = nil) |     def assign_to(project, current_user = nil) | ||||||
|       if instance_type? |       if instance_type? | ||||||
|  | @ -354,7 +353,11 @@ module Ci | ||||||
| 
 | 
 | ||||||
|       begin |       begin | ||||||
|         transaction do |         transaction do | ||||||
|           self.sharding_key_id = project.id if self.runner_projects.empty? |           if self.runner_projects.empty? | ||||||
|  |             self.sharding_key_id = project.id | ||||||
|  |             self.clear_memoization(:owner) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|           self.runner_projects << ::Ci::RunnerProject.new(project: project, runner: self) |           self.runner_projects << ::Ci::RunnerProject.new(project: project, runner: self) | ||||||
|           self.save! |           self.save! | ||||||
|         end |         end | ||||||
|  | @ -384,14 +387,20 @@ module Ci | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def owner_project |     def owner | ||||||
|       return unless project_type? |       case runner_type | ||||||
| 
 |       when 'instance_type' | ||||||
|       runner_projects.order(:id).first&.project |         ::User.find_by_id(creator_id) | ||||||
|  |       when 'group_type' | ||||||
|  |         ::Group.find_by_id(sharding_key_id) | ||||||
|  |       when 'project_type' | ||||||
|  |         ::Project.find_by_id(sharding_key_id) | ||||||
|  |       end | ||||||
|     end |     end | ||||||
|  |     strong_memoize_attr :owner | ||||||
| 
 | 
 | ||||||
|     def belongs_to_one_project? |     def belongs_to_one_project? | ||||||
|       runner_projects.count == 1 |       runner_projects.limit(2).count(:all) == 1 | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def belongs_to_more_than_one_project? |     def belongs_to_more_than_one_project? | ||||||
|  | @ -497,10 +506,9 @@ module Ci | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def namespace_ids |     def namespace_ids | ||||||
|       strong_memoize(:namespace_ids) do |       runner_namespaces.pluck(:namespace_id).compact | ||||||
|         runner_namespaces.pluck(:namespace_id).compact |  | ||||||
|       end |  | ||||||
|     end |     end | ||||||
|  |     strong_memoize_attr :namespace_ids | ||||||
| 
 | 
 | ||||||
|     def compute_token_expiration |     def compute_token_expiration | ||||||
|       case runner_type |       case runner_type | ||||||
|  |  | ||||||
|  | @ -2440,7 +2440,7 @@ class MergeRequest < ApplicationRecord | ||||||
|   def pipeline_creating? |   def pipeline_creating? | ||||||
|     return false unless Feature.enabled?(:ci_redis_pipeline_creations, project) |     return false unless Feature.enabled?(:ci_redis_pipeline_creations, project) | ||||||
| 
 | 
 | ||||||
|     Ci::PipelineCreation::Requests.pipeline_creating_for_merge_request?(self, delete_if_all_complete: true) |     Ci::PipelineCreation::Requests.pipeline_creating_for_merge_request?(self) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def merge_base_pipelines |   def merge_base_pipelines | ||||||
|  |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Ci |  | ||||||
|   class DagJobEntity < Grape::Entity |  | ||||||
|     expose :name |  | ||||||
|     expose :scheduling_type |  | ||||||
| 
 |  | ||||||
|     expose :needs, if: ->(job, _) { job.scheduling_type_dag? } do |job| |  | ||||||
|       job.needs.pluck(:name) # rubocop: disable CodeReuse/ActiveRecord |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Ci |  | ||||||
|   class DagJobGroupEntity < Grape::Entity |  | ||||||
|     expose :name |  | ||||||
|     expose :size |  | ||||||
|     expose :jobs, with: Ci::DagJobEntity |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,20 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Ci |  | ||||||
|   class DagPipelineEntity < Grape::Entity |  | ||||||
|     expose :stages_with_preloads, as: :stages, using: Ci::DagStageEntity |  | ||||||
| 
 |  | ||||||
|     private |  | ||||||
| 
 |  | ||||||
|     def stages_with_preloads |  | ||||||
|       object.stages.preload(preloaded_relations) # rubocop: disable CodeReuse/ActiveRecord |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def preloaded_relations |  | ||||||
|       [ |  | ||||||
|         :project, |  | ||||||
|         { latest_statuses: :needs } |  | ||||||
|       ] |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,7 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Ci |  | ||||||
|   class DagPipelineSerializer < BaseSerializer |  | ||||||
|     entity Ci::DagPipelineEntity |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Ci |  | ||||||
|   class DagStageEntity < Grape::Entity |  | ||||||
|     expose :name |  | ||||||
| 
 |  | ||||||
|     expose :groups, with: Ci::DagJobGroupEntity |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -73,7 +73,9 @@ module Ci | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def path_to(route, job, params = {}) |     def path_to(route, job, params = {}) | ||||||
|       send("#{route}_path", job.project.namespace, job.project, job, params) # rubocop:disable GitlabSecurity/PublicSend |       # rubocop:disable GitlabSecurity/PublicSend -- needs send | ||||||
|  |       send("#{route}_path", job.project.namespace, job.project, job, params) | ||||||
|  |       # rubocop:enable GitlabSecurity/PublicSend | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def job_path(job) |     def job_path(job) | ||||||
|  |  | ||||||
|  | @ -45,7 +45,7 @@ module Ci | ||||||
|             reason: :not_authorized_to_add_runner_in_project) |             reason: :not_authorized_to_add_runner_in_project) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         if runner.owner_project && project.organization_id != runner.owner_project.organization_id |         if runner.owner && project.organization_id != runner.owner.organization_id | ||||||
|           return ServiceResponse.error(message: _('runner can only be assigned to projects in the same organization'), |           return ServiceResponse.error(message: _('runner can only be assigned to projects in the same organization'), | ||||||
|             reason: :project_not_in_same_organization) |             reason: :project_not_in_same_organization) | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -26,13 +26,11 @@ module Ci | ||||||
|       private |       private | ||||||
| 
 | 
 | ||||||
|       def set_associated_projects |       def set_associated_projects | ||||||
|         new_project_ids = [runner.owner_project&.id].compact + project_ids |         new_project_ids = [runner.owner&.id].compact + project_ids | ||||||
| 
 | 
 | ||||||
|         response = ServiceResponse.success |         response = ServiceResponse.success | ||||||
|         runner.transaction do |         runner.transaction do | ||||||
|           # rubocop:disable CodeReuse/ActiveRecord |           current_project_ids = runner.project_ids # rubocop:disable CodeReuse/ActiveRecord -- reasonable use | ||||||
|           current_project_ids = runner.projects.ids |  | ||||||
|           # rubocop:enable CodeReuse/ActiveRecord |  | ||||||
| 
 | 
 | ||||||
|           response = associate_new_projects(new_project_ids, current_project_ids) |           response = associate_new_projects(new_project_ids, current_project_ids) | ||||||
|           response = disassociate_old_projects(new_project_ids, current_project_ids) if response.success? |           response = disassociate_old_projects(new_project_ids, current_project_ids) if response.success? | ||||||
|  |  | ||||||
|  | @ -33,9 +33,9 @@ module Ci | ||||||
|         kwargs = { user: current_user } |         kwargs = { user: current_user } | ||||||
|         case runner.runner_type |         case runner.runner_type | ||||||
|         when 'group_type' |         when 'group_type' | ||||||
|           kwargs[:namespace] = runner.groups.first |           kwargs[:namespace] = runner.owner | ||||||
|         when 'project_type' |         when 'project_type' | ||||||
|           kwargs[:project] = runner.owner_project |           kwargs[:project] = runner.owner | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         track_internal_event( |         track_internal_event( | ||||||
|  |  | ||||||
|  | @ -26,6 +26,8 @@ module MergeRequests | ||||||
|         invalidate_cache_counts(merge_request, users: merge_request.assignees) |         invalidate_cache_counts(merge_request, users: merge_request.assignees) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       invalidate_cache_counts(merge_request, users: old_assignees) | ||||||
|  | 
 | ||||||
|       execute_assignees_hooks(merge_request, old_assignees) if options['execute_hooks'] |       execute_assignees_hooks(merge_request, old_assignees) if options['execute_hooks'] | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2318,7 +2318,7 @@ | ||||||
|   :tags: [] |   :tags: [] | ||||||
| - :name: todos_destroyer:todos_destroyer_confidential_issue | - :name: todos_destroyer:todos_destroyer_confidential_issue | ||||||
|   :worker_name: TodosDestroyer::ConfidentialIssueWorker |   :worker_name: TodosDestroyer::ConfidentialIssueWorker | ||||||
|   :feature_category: :team_planning |   :feature_category: :notifications | ||||||
|   :has_external_dependencies: false |   :has_external_dependencies: false | ||||||
|   :urgency: :low |   :urgency: :low | ||||||
|   :resource_boundary: :unknown |   :resource_boundary: :unknown | ||||||
|  | @ -2327,7 +2327,7 @@ | ||||||
|   :tags: [] |   :tags: [] | ||||||
| - :name: todos_destroyer:todos_destroyer_destroyed_designs | - :name: todos_destroyer:todos_destroyer_destroyed_designs | ||||||
|   :worker_name: TodosDestroyer::DestroyedDesignsWorker |   :worker_name: TodosDestroyer::DestroyedDesignsWorker | ||||||
|   :feature_category: :team_planning |   :feature_category: :notifications | ||||||
|   :has_external_dependencies: false |   :has_external_dependencies: false | ||||||
|   :urgency: :low |   :urgency: :low | ||||||
|   :resource_boundary: :unknown |   :resource_boundary: :unknown | ||||||
|  | @ -2336,7 +2336,7 @@ | ||||||
|   :tags: [] |   :tags: [] | ||||||
| - :name: todos_destroyer:todos_destroyer_destroyed_issuable | - :name: todos_destroyer:todos_destroyer_destroyed_issuable | ||||||
|   :worker_name: TodosDestroyer::DestroyedIssuableWorker |   :worker_name: TodosDestroyer::DestroyedIssuableWorker | ||||||
|   :feature_category: :team_planning |   :feature_category: :notifications | ||||||
|   :has_external_dependencies: false |   :has_external_dependencies: false | ||||||
|   :urgency: :low |   :urgency: :low | ||||||
|   :resource_boundary: :unknown |   :resource_boundary: :unknown | ||||||
|  | @ -2345,7 +2345,7 @@ | ||||||
|   :tags: [] |   :tags: [] | ||||||
| - :name: todos_destroyer:todos_destroyer_entity_leave | - :name: todos_destroyer:todos_destroyer_entity_leave | ||||||
|   :worker_name: TodosDestroyer::EntityLeaveWorker |   :worker_name: TodosDestroyer::EntityLeaveWorker | ||||||
|   :feature_category: :team_planning |   :feature_category: :notifications | ||||||
|   :has_external_dependencies: false |   :has_external_dependencies: false | ||||||
|   :urgency: :low |   :urgency: :low | ||||||
|   :resource_boundary: :unknown |   :resource_boundary: :unknown | ||||||
|  | @ -2354,7 +2354,7 @@ | ||||||
|   :tags: [] |   :tags: [] | ||||||
| - :name: todos_destroyer:todos_destroyer_group_private | - :name: todos_destroyer:todos_destroyer_group_private | ||||||
|   :worker_name: TodosDestroyer::GroupPrivateWorker |   :worker_name: TodosDestroyer::GroupPrivateWorker | ||||||
|   :feature_category: :team_planning |   :feature_category: :notifications | ||||||
|   :has_external_dependencies: false |   :has_external_dependencies: false | ||||||
|   :urgency: :low |   :urgency: :low | ||||||
|   :resource_boundary: :unknown |   :resource_boundary: :unknown | ||||||
|  | @ -2363,7 +2363,7 @@ | ||||||
|   :tags: [] |   :tags: [] | ||||||
| - :name: todos_destroyer:todos_destroyer_private_features | - :name: todos_destroyer:todos_destroyer_private_features | ||||||
|   :worker_name: TodosDestroyer::PrivateFeaturesWorker |   :worker_name: TodosDestroyer::PrivateFeaturesWorker | ||||||
|   :feature_category: :team_planning |   :feature_category: :notifications | ||||||
|   :has_external_dependencies: false |   :has_external_dependencies: false | ||||||
|   :urgency: :low |   :urgency: :low | ||||||
|   :resource_boundary: :unknown |   :resource_boundary: :unknown | ||||||
|  | @ -2372,7 +2372,7 @@ | ||||||
|   :tags: [] |   :tags: [] | ||||||
| - :name: todos_destroyer:todos_destroyer_project_private | - :name: todos_destroyer:todos_destroyer_project_private | ||||||
|   :worker_name: TodosDestroyer::ProjectPrivateWorker |   :worker_name: TodosDestroyer::ProjectPrivateWorker | ||||||
|   :feature_category: :team_planning |   :feature_category: :notifications | ||||||
|   :has_external_dependencies: false |   :has_external_dependencies: false | ||||||
|   :urgency: :low |   :urgency: :low | ||||||
|   :resource_boundary: :unknown |   :resource_boundary: :unknown | ||||||
|  |  | ||||||
|  | @ -8,6 +8,6 @@ module TodosDestroyerQueue | ||||||
| 
 | 
 | ||||||
|   included do |   included do | ||||||
|     queue_namespace :todos_destroyer |     queue_namespace :todos_destroyer | ||||||
|     feature_category :team_planning |     feature_category :notifications | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ column: default_project_creation | ||||||
| db_type: integer | db_type: integer | ||||||
| default: '2' | default: '2' | ||||||
| description: 'Default project creation protection. Can take: `0` _(No one)_, `1` _(Maintainers)_, | description: 'Default project creation protection. Can take: `0` _(No one)_, `1` _(Maintainers)_, | ||||||
|   `2` _(Developers + Maintainers)_ or `3` _(Administrators)_' |   `2` _(Developers + Maintainers)_, or `3` _(Administrators)_.' | ||||||
| encrypted: false | encrypted: false | ||||||
| gitlab_com_different_than_default: false | gitlab_com_different_than_default: false | ||||||
| jihu: false | jihu: false | ||||||
|  |  | ||||||
|  | @ -5,7 +5,8 @@ clusterwide: false | ||||||
| column: elasticsearch_retry_on_failure | column: elasticsearch_retry_on_failure | ||||||
| db_type: integer | db_type: integer | ||||||
| default: '0' | default: '0' | ||||||
| description: Maximum number of possible retries for Elasticsearch search requests. Premium and Ultimate only. | description: Maximum number of possible retries for Elasticsearch search requests. | ||||||
|  |   Premium and Ultimate only. | ||||||
| encrypted: false | encrypted: false | ||||||
| gitlab_com_different_than_default: false | gitlab_com_different_than_default: false | ||||||
| jihu: false | jihu: false | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ clusterwide: true | ||||||
| column: identity_verification_settings | column: identity_verification_settings | ||||||
| db_type: jsonb | db_type: jsonb | ||||||
| default: "'{}'::jsonb" | default: "'{}'::jsonb" | ||||||
| description: 'Configuration settings related to identity verification' | description: | ||||||
| encrypted: false | encrypted: false | ||||||
| gitlab_com_different_than_default: true | gitlab_com_different_than_default: true | ||||||
| jihu: false | jihu: false | ||||||
|  |  | ||||||
|  | @ -8,7 +8,9 @@ default: | ||||||
| description: Maximum allowable lifetime for access tokens in days. When left blank, | description: Maximum allowable lifetime for access tokens in days. When left blank, | ||||||
|   default value of 365 is applied. When set, value must be 365 or less. When changed, |   default value of 365 is applied. When set, value must be 365 or less. When changed, | ||||||
|   existing access tokens with an expiration date beyond the maximum allowable lifetime |   existing access tokens with an expiration date beyond the maximum allowable lifetime | ||||||
|   are revoked. Self-managed, Ultimate only. |   are revoked. Self-managed, Ultimate only. In GitLab 17.6 or later, the maximum lifetime | ||||||
|  |   limit can be [extended to 400 days](https://gitlab.com/gitlab-org/gitlab/-/issues/461901) | ||||||
|  |   by enabling a [feature flag](../administration/feature_flags.md) named `buffered_token_expiration_limit`. | ||||||
| encrypted: false | encrypted: false | ||||||
| gitlab_com_different_than_default: false | gitlab_com_different_than_default: false | ||||||
| jihu: false | jihu: false | ||||||
|  |  | ||||||
|  | @ -6,7 +6,9 @@ column: max_ssh_key_lifetime | ||||||
| db_type: integer | db_type: integer | ||||||
| default: | default: | ||||||
| description: Maximum allowable lifetime for SSH keys in days. Self-managed, Ultimate | description: Maximum allowable lifetime for SSH keys in days. Self-managed, Ultimate | ||||||
|   only. |   only. In GitLab 17.6 or later, the maximum lifetime limit can be [extended to 400 | ||||||
|  |   days](https://gitlab.com/gitlab-org/gitlab/-/issues/461901) by enabling a [feature | ||||||
|  |   flag](../administration/feature_flags.md) named `buffered_token_expiration_limit`. | ||||||
| encrypted: false | encrypted: false | ||||||
| gitlab_com_different_than_default: false | gitlab_com_different_than_default: false | ||||||
| jihu: false | jihu: false | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ clusterwide: false | ||||||
| column: sign_in_restrictions | column: sign_in_restrictions | ||||||
| db_type: jsonb | db_type: jsonb | ||||||
| default: "'{}'::jsonb" | default: "'{}'::jsonb" | ||||||
| description: "Settings related to sign-in restrictions" | description: | ||||||
| encrypted: false | encrypted: false | ||||||
| gitlab_com_different_than_default: true | gitlab_com_different_than_default: true | ||||||
| jihu: false | jihu: false | ||||||
|  |  | ||||||
|  | @ -5,10 +5,7 @@ clusterwide: false | ||||||
| column: transactional_emails | column: transactional_emails | ||||||
| db_type: jsonb | db_type: jsonb | ||||||
| default: "'{}'::jsonb" | default: "'{}'::jsonb" | ||||||
| description: > | description: | ||||||
|   Settings for transactional emails. |  | ||||||
|   [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/168245) in |  | ||||||
|   GitLab 17.6 to support options for group and project access token expiry. |  | ||||||
| encrypted: false | encrypted: false | ||||||
| gitlab_com_different_than_default: false | gitlab_com_different_than_default: false | ||||||
| jihu: false | jihu: false | ||||||
|  |  | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | --- | ||||||
|  | name: expand_nested_variables_in_job_rules_exists_and_changes | ||||||
|  | feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327780 | ||||||
|  | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166779 | ||||||
|  | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/483115 | ||||||
|  | milestone: '17.6' | ||||||
|  | group: group::pipeline authoring | ||||||
|  | type: gitlab_com_derisk | ||||||
|  | default_enabled: false | ||||||
|  | @ -14,7 +14,6 @@ resources :pipelines, only: [:index, :new, :create, :show, :destroy] do | ||||||
|     post :cancel |     post :cancel | ||||||
|     post :retry |     post :retry | ||||||
|     get :builds |     get :builds | ||||||
|     get :dag |  | ||||||
|     get :failures |     get :failures | ||||||
|     get :status |     get :status | ||||||
|     get :test_report |     get :test_report | ||||||
|  |  | ||||||
|  | @ -10,3 +10,8 @@ | ||||||
|     [GitLab Runner documentation](https://docs.gitlab.com/runner/) |     [GitLab Runner documentation](https://docs.gitlab.com/runner/) | ||||||
|   stage: verify |   stage: verify | ||||||
|   issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387937 |   issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/387937 | ||||||
|  |   window: "1" | ||||||
|  |   impact: low | ||||||
|  |   scope: [instance, group, project] | ||||||
|  |   resolution_role: admin | ||||||
|  |   manual_task: false | ||||||
|  |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| - title: "List container registry repository tags API endpoint pagination" |  | ||||||
|   announcement_milestone: "16.10" |  | ||||||
|   removal_milestone: "18.0" |  | ||||||
|   breaking_change: true |  | ||||||
|   reporter: trizzi |  | ||||||
|   stage: Package |  | ||||||
|   issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432470 |  | ||||||
|   body: | |  | ||||||
|     You can use the container registry REST API to [get a list of registry repository tags](https://docs.gitlab.com/ee/api/container_registry.html#list-registry-repository-tags). We plan to improve this endpoint, adding more metadata and new features like improved sorting and filtering. |  | ||||||
| 
 |  | ||||||
|     While offset-based pagination was already available for this endpoint, keyset-based pagination was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/432470) in GitLab 16.10 for GitLab.com only. This is now the preferred pagination method. |  | ||||||
| 
 |  | ||||||
|     Offset-based pagination for the [List registry repository tags](https://docs.gitlab.com/ee/api/container_registry.html#list-registry-repository-tags) endpoint is deprecated in GitLab 16.10 and will be removed in 18.0. Instead, use the keyset-based pagination. |  | ||||||
|  | @ -3,7 +3,7 @@ table_name: todos | ||||||
| classes: | classes: | ||||||
| - Todo | - Todo | ||||||
| feature_categories: | feature_categories: | ||||||
| - team_planning | - notifications | ||||||
| description: >- | description: >- | ||||||
|   An action required or notification of action taken for a user on a target object, generated by various actions within the |   An action required or notification of action taken for a user on a target object, generated by various actions within the | ||||||
|   GitLab application |   GitLab application | ||||||
|  |  | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddMetadataToZoektIndices < Gitlab::Database::Migration[2.2] | ||||||
|  |   enable_lock_retries! | ||||||
|  |   milestone '17.6' | ||||||
|  | 
 | ||||||
|  |   def change | ||||||
|  |     add_column :zoekt_indices, :metadata, :jsonb, default: {}, null: false | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 7bc71fb040af525f9592177bfe35225a44d84b1a85d8c5bc669664c00303bafc | ||||||
|  | @ -22005,7 +22005,8 @@ CREATE TABLE zoekt_indices ( | ||||||
|     zoekt_replica_id bigint, |     zoekt_replica_id bigint, | ||||||
|     reserved_storage_bytes bigint DEFAULT '10737418240'::bigint, |     reserved_storage_bytes bigint DEFAULT '10737418240'::bigint, | ||||||
|     used_storage_bytes bigint DEFAULT 0 NOT NULL, |     used_storage_bytes bigint DEFAULT 0 NOT NULL, | ||||||
|     watermark_level smallint DEFAULT 0 NOT NULL |     watermark_level smallint DEFAULT 0 NOT NULL, | ||||||
|  |     metadata jsonb DEFAULT '{}'::jsonb NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE SEQUENCE zoekt_indices_id_seq | CREATE SEQUENCE zoekt_indices_id_seq | ||||||
|  |  | ||||||
|  | @ -47,14 +47,18 @@ supporting custom domains a secondary IP is not needed. | ||||||
| 
 | 
 | ||||||
| ## Prerequisites | ## Prerequisites | ||||||
| 
 | 
 | ||||||
| Before proceeding with the Pages configuration, you must: | This section describes the prerequisites for configuring GitLab Pages. | ||||||
|  | 
 | ||||||
|  | ### Wildcard domains | ||||||
|  | 
 | ||||||
|  | Before configuring Pages for wildcard domains, you must: | ||||||
| 
 | 
 | ||||||
| 1. Have a domain for Pages that is not a subdomain of your GitLab instance domain. | 1. Have a domain for Pages that is not a subdomain of your GitLab instance domain. | ||||||
| 
 | 
 | ||||||
|    | GitLab domain | Pages domain | Does it work? | |    | GitLab domain        | Pages domain        | Does it work? | | ||||||
|    | :---: | :---: | :---: | |    | -------------------- | ------------------- | ------------- | | ||||||
|    | `example.com` | `example.io` | **{check-circle}** Yes | |    | `example.com`        | `example.io`        | **{check-circle}** Yes | | ||||||
|    | `example.com` | `pages.example.com` | **{dotted-circle}** No | |    | `example.com`        | `pages.example.com` | **{dotted-circle}** No | | ||||||
|    | `gitlab.example.com` | `pages.example.com` | **{check-circle}** Yes | |    | `gitlab.example.com` | `pages.example.com` | **{check-circle}** Yes | | ||||||
| 
 | 
 | ||||||
| 1. Configure a **wildcard DNS record**. | 1. Configure a **wildcard DNS record**. | ||||||
|  | @ -64,6 +68,24 @@ Before proceeding with the Pages configuration, you must: | ||||||
|    so that your users don't have to bring their own. |    so that your users don't have to bring their own. | ||||||
| 1. For custom domains, have a **secondary IP**. | 1. For custom domains, have a **secondary IP**. | ||||||
| 
 | 
 | ||||||
|  | ### Single-domain sites | ||||||
|  | 
 | ||||||
|  | Before configuring Pages for single-domain sites, you must: | ||||||
|  | 
 | ||||||
|  | 1. Have a domain for Pages that is not a subdomain of your GitLab instance domain. | ||||||
|  | 
 | ||||||
|  |    | GitLab domain        | Pages domain        | Supported | | ||||||
|  |    | -------------------- | ------------------- | ------------- | | ||||||
|  |    | `example.com`        | `example.io`        | **{check-circle}** Yes | | ||||||
|  |    | `example.com`        | `pages.example.com` | **{dotted-circle}** No | | ||||||
|  |    | `gitlab.example.com` | `pages.example.com` | **{check-circle}** Yes | | ||||||
|  | 
 | ||||||
|  | 1. Configure a **DNS record**. | ||||||
|  | 1. Optional. If you decide to serve Pages under HTTPS, have a **TLS certificate** for that domain. | ||||||
|  | 1. Optional but recommended. Enable [instance runners](../../ci/runners/index.md) | ||||||
|  |    so that your users don't have to bring their own. | ||||||
|  | 1. For custom domains, have a **secondary IP**. | ||||||
|  | 
 | ||||||
| NOTE: | NOTE: | ||||||
| If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites are only accessible to devices/users that have access to the private network. | If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites are only accessible to devices/users that have access to the private network. | ||||||
| 
 | 
 | ||||||
|  | @ -77,8 +99,8 @@ Suffix List prevents browsers from accepting | ||||||
| [supercookies](https://en.wikipedia.org/wiki/HTTP_cookie#Supercookie), | [supercookies](https://en.wikipedia.org/wiki/HTTP_cookie#Supercookie), | ||||||
| among other things. | among other things. | ||||||
| 
 | 
 | ||||||
| Follow [these instructions](https://publicsuffix.org/submit/) to submit your | To submit your GitLab Pages subdomain, follow [Submit amendments to the Public Suffix List](https://publicsuffix.org/submit/). | ||||||
| GitLab Pages subdomain. For instance, if your domain is `example.io`, you should | For example, if your domain is `example.io`, you should | ||||||
| request that `example.io` is added to the Public Suffix List. GitLab.com | request that `example.io` is added to the Public Suffix List. GitLab.com | ||||||
| added `gitlab.io` [in 2016](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/230). | added `gitlab.io` [in 2016](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/230). | ||||||
| 
 | 
 | ||||||
|  | @ -97,14 +119,14 @@ Where `example.io` is the domain GitLab Pages is served from, | ||||||
| `192.0.2.1` is the IPv4 address of your GitLab instance, and `2001:db8::1` is the | `192.0.2.1` is the IPv4 address of your GitLab instance, and `2001:db8::1` is the | ||||||
| IPv6 address. If you don't have IPv6, you can omit the `AAAA` record. | IPv6 address. If you don't have IPv6, you can omit the `AAAA` record. | ||||||
| 
 | 
 | ||||||
| #### For namespace in URL path, without wildcard DNS | #### DNS configuration for single-domain sites | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) as an [experiment](../../policy/experiment-beta-support.md) in GitLab 16.7. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) as an [experiment](../../policy/experiment-beta-support.md) in GitLab 16.7. | ||||||
| > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148621) to [beta](../../policy/experiment-beta-support.md) in GitLab 16.11. | > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148621) to [beta](../../policy/experiment-beta-support.md) in GitLab 16.11. | ||||||
| > - [Changed](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/1111) implementation from NGINX to the GitLab Pages codebase in GitLab 17.2. | > - [Changed](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/1111) implementation from NGINX to the GitLab Pages codebase in GitLab 17.2. | ||||||
| > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/483365) in GitLab 17.4. | > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/483365) in GitLab 17.4. | ||||||
| 
 | 
 | ||||||
| If you need support for namespace in the URL path to remove the requirement for wildcard DNS: | To configure GitLab Pages DNS for single-domain sites without wildcard DNS: | ||||||
| 
 | 
 | ||||||
| 1. Enable the GitLab Pages flag for this feature by adding | 1. Enable the GitLab Pages flag for this feature by adding | ||||||
|    `gitlab_pages["namespace_in_path"] = true` to `/etc/gitlab/gitlab.rb`. |    `gitlab_pages["namespace_in_path"] = true` to `/etc/gitlab/gitlab.rb`. | ||||||
|  | @ -160,17 +182,18 @@ advanced one. | ||||||
| 
 | 
 | ||||||
| ### Wildcard domains | ### Wildcard domains | ||||||
| 
 | 
 | ||||||
|  | The following configuration is the minimum setup to use GitLab Pages. | ||||||
|  | It is the foundation for all other setups described here. | ||||||
|  | In this configuration: | ||||||
|  | 
 | ||||||
|  | - NGINX proxies all requests to the GitLab Pages daemon. | ||||||
|  | - The GitLab Pages daemon doesn't listen directly to the public internet. | ||||||
|  | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
| 
 | 
 | ||||||
| - [Wildcard DNS setup](#dns-configuration) | - [Wildcard DNS setup](#dns-configuration) | ||||||
| 
 | 
 | ||||||
| --- | To configure GitLab Pages to use wildcard domains: | ||||||
| 
 |  | ||||||
| URL scheme: `http://<namespace>.example.io/<project_slug>` |  | ||||||
| 
 |  | ||||||
| The following is the minimum setup that you can use Pages with. It is the base for all |  | ||||||
| other setups as described below. NGINX proxies all requests to the daemon. |  | ||||||
| The Pages daemon doesn't listen to the outside world. |  | ||||||
| 
 | 
 | ||||||
| 1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: | 1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: | ||||||
| 
 | 
 | ||||||
|  | @ -181,30 +204,38 @@ The Pages daemon doesn't listen to the outside world. | ||||||
| 
 | 
 | ||||||
| 1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). | 1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). | ||||||
| 
 | 
 | ||||||
| Watch the [video tutorial](https://youtu.be/dD8c7WNcc6s) for this configuration. | The resulting URL scheme is `http://<namespace>.example.io/<project_slug>`. | ||||||
| 
 | 
 | ||||||
| ### Pages domain without wildcard DNS | <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> | ||||||
|  | For an overview, see [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s). | ||||||
|  | <!-- Video published on 2017-02-22 --> | ||||||
|  | 
 | ||||||
|  | ### Single-domain sites | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) as an [experiment](../../policy/experiment-beta-support.md) in GitLab 16.7. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) as an [experiment](../../policy/experiment-beta-support.md) in GitLab 16.7. | ||||||
| > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148621) to [beta](../../policy/experiment-beta-support.md) in GitLab 16.11. | > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148621) to [beta](../../policy/experiment-beta-support.md) in GitLab 16.11. | ||||||
| > - [Changed](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/1111) implementation from NGINX to the GitLab Pages codebase in GitLab 17.2. | > - [Changed](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/1111) implementation from NGINX to the GitLab Pages codebase in GitLab 17.2. | ||||||
| > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/483365) in GitLab 17.4. | > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/483365) in GitLab 17.4. | ||||||
| 
 | 
 | ||||||
| This configuration is the minimum setup for GitLab Pages. It is the base for all | The following configuration is the minimum setup to use GitLab Pages. | ||||||
| other configurations. In this configuration, NGINX proxies all requests to the daemon, | It is the foundation for all other setups described here. | ||||||
| because the GitLab Pages daemon doesn't listen to the outside world. | In this configuration: | ||||||
|  | 
 | ||||||
|  | - NGINX proxies all requests to the GitLab Pages daemon. | ||||||
|  | - The GitLab Pages daemon doesn't listen directly to the public internet. | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
| 
 | 
 | ||||||
| - You have configured DNS setup | - You have configured DNS for | ||||||
|   [without a wildcard](#for-namespace-in-url-path-without-wildcard-dns). |   [single-domain sites](#dns-configuration-for-single-domain-sites). | ||||||
|  | 
 | ||||||
|  | To configure GitLab Pages to use single-domain sites: | ||||||
| 
 | 
 | ||||||
| 1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable the feature: | 1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable the feature: | ||||||
| 
 | 
 | ||||||
|    ```ruby |    ```ruby | ||||||
|    # External_url here is only for reference |    external_url "http://example.com" # Swap out this URL for your own | ||||||
|    external_url "http://example.com" |    pages_external_url 'http://example.io' # Important: not a subdomain of external_url, so cannot be http://pages.example.com | ||||||
|    pages_external_url 'http://example.io' |  | ||||||
| 
 | 
 | ||||||
|    # Set this flag to enable this feature |    # Set this flag to enable this feature | ||||||
|    gitlab_pages["namespace_in_path"] = true |    gitlab_pages["namespace_in_path"] = true | ||||||
|  | @ -216,9 +247,9 @@ The resulting URL scheme is `http://example.io/<namespace>/<project_slug>`. | ||||||
| 
 | 
 | ||||||
| WARNING: | WARNING: | ||||||
| GitLab Pages supports only one URL scheme at a time: | GitLab Pages supports only one URL scheme at a time: | ||||||
| with wildcard DNS, or without wildcard DNS. | wildcard domains or single-domain sites. | ||||||
| If you enable `namespace_in_path`, existing GitLab Pages websites | If you enable `namespace_in_path`, existing GitLab Pages websites | ||||||
| are accessible only on domains without wildcard DNS. | are accessible only on single-domain. | ||||||
| 
 | 
 | ||||||
| ### Wildcard domains with TLS support | ### Wildcard domains with TLS support | ||||||
| 
 | 
 | ||||||
|  | @ -227,12 +258,8 @@ Prerequisites: | ||||||
| - [Wildcard DNS setup](#dns-configuration) | - [Wildcard DNS setup](#dns-configuration) | ||||||
| - TLS certificate. Can be either Wildcard, or any other type meeting the [requirements](../../user/project/pages/custom_domains_ssl_tls_certification/index.md#manual-addition-of-ssltls-certificates). | - TLS certificate. Can be either Wildcard, or any other type meeting the [requirements](../../user/project/pages/custom_domains_ssl_tls_certification/index.md#manual-addition-of-ssltls-certificates). | ||||||
| 
 | 
 | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| URL scheme: `https://<namespace>.example.io/<project_slug>` |  | ||||||
| 
 |  | ||||||
| NGINX proxies all requests to the daemon. Pages daemon doesn't listen to the | NGINX proxies all requests to the daemon. Pages daemon doesn't listen to the | ||||||
| outside world. | public internet. | ||||||
| 
 | 
 | ||||||
| 1. Place the wildcard TLS certificate for `*.example.io` and the key inside `/etc/gitlab/ssl`. | 1. Place the wildcard TLS certificate for `*.example.io` and the key inside `/etc/gitlab/ssl`. | ||||||
| 1. In `/etc/gitlab/gitlab.rb` specify the following configuration: | 1. In `/etc/gitlab/gitlab.rb` specify the following configuration: | ||||||
|  | @ -257,6 +284,8 @@ outside world. | ||||||
|    [System OAuth application](../../integration/oauth_provider.md#create-an-instance-wide-application) |    [System OAuth application](../../integration/oauth_provider.md#create-an-instance-wide-application) | ||||||
|    to use the HTTPS protocol. |    to use the HTTPS protocol. | ||||||
| 
 | 
 | ||||||
|  | The resulting URL scheme is `https://<namespace>.example.io/<project_slug>`. | ||||||
|  | 
 | ||||||
| WARNING: | WARNING: | ||||||
| Multiple wildcards for one instance is not supported. Only one wildcard per instance can be assigned. | Multiple wildcards for one instance is not supported. Only one wildcard per instance can be assigned. | ||||||
| 
 | 
 | ||||||
|  | @ -266,7 +295,7 @@ Before you reconfigure, remove the `gitlab_pages` section from `/etc/gitlab/gitl | ||||||
| then run `gitlab-ctl reconfigure`. For more information, read | then run `gitlab-ctl reconfigure`. For more information, read | ||||||
| [GitLab Pages does not regenerate OAuth](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3947). | [GitLab Pages does not regenerate OAuth](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3947). | ||||||
| 
 | 
 | ||||||
| ### Pages domain with TLS support, without wildcard DNS | ### Single-domain sites with TLS support | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) as an [experiment](../../policy/experiment-beta-support.md) in GitLab 16.7. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) as an [experiment](../../policy/experiment-beta-support.md) in GitLab 16.7. | ||||||
| > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148621) to [beta](../../policy/experiment-beta-support.md) in GitLab 16.11. | > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148621) to [beta](../../policy/experiment-beta-support.md) in GitLab 16.11. | ||||||
|  | @ -275,20 +304,19 @@ then run `gitlab-ctl reconfigure`. For more information, read | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
| 
 | 
 | ||||||
| - You have configured DNS setup | - You have configured DNS for | ||||||
|   [without a wildcard](#for-namespace-in-url-path-without-wildcard-dns). |   [single-domain sites](#dns-configuration-for-single-domain-sites). | ||||||
| - You have a TLS certificate that covers your domain (like `example.io`). | - You have a TLS certificate that covers your domain (like `example.io`). | ||||||
| 
 | 
 | ||||||
| In this configuration, NGINX proxies all requests to the daemon. The GitLab Pages | In this configuration, NGINX proxies all requests to the daemon. The GitLab Pages | ||||||
| daemon doesn't listen to the outside world: | daemon doesn't listen to the public internet: | ||||||
| 
 | 
 | ||||||
| 1. Add your TLS certificate and key as mentioned in the prerequisites into `/etc/gitlab/ssl`. | 1. Add your TLS certificate and key as mentioned in the prerequisites into `/etc/gitlab/ssl`. | ||||||
| 1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable the feature: | 1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable the feature: | ||||||
| 
 | 
 | ||||||
|    ```ruby |    ```ruby | ||||||
|    # The external_url field is here only for reference. |    external_url "https://example.com" # Swap out this URL for your own | ||||||
|    external_url "https://example.com" |    pages_external_url 'https://example.io' # Important: not a subdomain of external_url, so cannot be https://pages.example.com | ||||||
|    pages_external_url 'https://example.io' |  | ||||||
| 
 | 
 | ||||||
|    pages_nginx['redirect_http_to_https'] = true |    pages_nginx['redirect_http_to_https'] = true | ||||||
| 
 | 
 | ||||||
|  | @ -322,9 +350,9 @@ The resulting URL scheme is `https://example.io/<namespace>/<project_slug>`. | ||||||
| 
 | 
 | ||||||
| WARNING: | WARNING: | ||||||
| GitLab Pages supports only one URL scheme at a time: | GitLab Pages supports only one URL scheme at a time: | ||||||
| with wildcard DNS, or without wildcard DNS. | wildcard domains or single-domain sites. | ||||||
| If you enable `namespace_in_path`, existing GitLab Pages websites | If you enable `namespace_in_path`, existing GitLab Pages websites | ||||||
| are accessible only on domains without wildcard DNS. | are accessible only as single-domain sites. | ||||||
| 
 | 
 | ||||||
| ### Wildcard domains with TLS-terminating Load Balancer | ### Wildcard domains with TLS-terminating Load Balancer | ||||||
| 
 | 
 | ||||||
|  | @ -333,10 +361,6 @@ Prerequisites: | ||||||
| - [Wildcard DNS setup](#dns-configuration) | - [Wildcard DNS setup](#dns-configuration) | ||||||
| - [TLS-terminating load balancer](../../install/aws/index.md#load-balancer) | - [TLS-terminating load balancer](../../install/aws/index.md#load-balancer) | ||||||
| 
 | 
 | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| URL scheme: `https://<namespace>.example.io/<project_slug>` |  | ||||||
| 
 |  | ||||||
| This setup is primarily intended to be used when [installing a GitLab POC on Amazon Web Services](../../install/aws/index.md). This includes a TLS-terminating [classic load balancer](../../install/aws/index.md#load-balancer) that listens for HTTPS connections, manages TLS certificates, and forwards HTTP traffic to the instance. | This setup is primarily intended to be used when [installing a GitLab POC on Amazon Web Services](../../install/aws/index.md). This includes a TLS-terminating [classic load balancer](../../install/aws/index.md#load-balancer) that listens for HTTPS connections, manages TLS certificates, and forwards HTTP traffic to the instance. | ||||||
| 
 | 
 | ||||||
| 1. In `/etc/gitlab/gitlab.rb` specify the following configuration: | 1. In `/etc/gitlab/gitlab.rb` specify the following configuration: | ||||||
|  | @ -353,6 +377,8 @@ This setup is primarily intended to be used when [installing a GitLab POC on Ama | ||||||
| 
 | 
 | ||||||
| 1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). | 1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). | ||||||
| 
 | 
 | ||||||
|  | The resulting URL scheme is `https://<namespace>.example.io/<project_slug>`. | ||||||
|  | 
 | ||||||
| ### Global settings | ### Global settings | ||||||
| 
 | 
 | ||||||
| Below is a table of all configuration settings known to Pages in a Linux package installation, | Below is a table of all configuration settings known to Pages in a Linux package installation, | ||||||
|  | @ -403,7 +429,7 @@ control over how the Pages daemon runs and serves content in your environment. | ||||||
| | `log_directory`                         | Absolute path to a log directory.                                                                                                                                                                                                                                                                          | | | `log_directory`                         | Absolute path to a log directory.                                                                                                                                                                                                                                                                          | | ||||||
| | `log_format`                            | The log output format: `text` or `json`.                                                                                                                                                                                                                                                                   | | | `log_format`                            | The log output format: `text` or `json`.                                                                                                                                                                                                                                                                   | | ||||||
| | `log_verbose`                           | Verbose logging, true/false.                                                                                                                                                                                                                                                                               | | | `log_verbose`                           | Verbose logging, true/false.                                                                                                                                                                                                                                                                               | | ||||||
| | `namespace_in_path`                     | (Beta) Enable or disable namespace in the URL path to support [without wildcard DNS setup](#for-namespace-in-url-path-without-wildcard-dns). Default: `false`.                                                                                                                                             | | | `namespace_in_path`                     | Enable or disable namespace in the URL path to support [single-domain sites DNS setup](#dns-configuration-for-single-domain-sites). Default: `false`.                                                                                                                                             | | ||||||
| | `propagate_correlation_id`              | Set to true (false by default) to re-use existing Correlation ID from the incoming request header `X-Request-ID` if present. If a reverse proxy sets this header, the value is propagated in the request chain.                                                                                            | | | `propagate_correlation_id`              | Set to true (false by default) to re-use existing Correlation ID from the incoming request header `X-Request-ID` if present. If a reverse proxy sets this header, the value is propagated in the request chain.                                                                                            | | ||||||
| | `max_connections`                       | Limit on the number of concurrent connections to the HTTP, HTTPS or proxy listeners.                                                                                                                                                                                                                       | | | `max_connections`                       | Limit on the number of concurrent connections to the HTTP, HTTPS or proxy listeners.                                                                                                                                                                                                                       | | ||||||
| | `max_uri_length`                        | The maximum length of URIs accepted by GitLab Pages. Set to 0 for unlimited length.                                                                                                                                                                                                                        | | | `max_uri_length`                        | The maximum length of URIs accepted by GitLab Pages. Set to 0 for unlimited length.                                                                                                                                                                                                                        | | ||||||
|  | @ -459,10 +485,6 @@ Prerequisites: | ||||||
| - [Wildcard DNS setup](#dns-configuration) | - [Wildcard DNS setup](#dns-configuration) | ||||||
| - Secondary IP | - Secondary IP | ||||||
| 
 | 
 | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| URL scheme: `http://<namespace>.example.io/<project_slug>` and `http://custom-domain.com` |  | ||||||
| 
 |  | ||||||
| In that case, the Pages daemon is running, NGINX still proxies requests to | In that case, the Pages daemon is running, NGINX still proxies requests to | ||||||
| the daemon but the daemon is also able to receive requests from the outside | the daemon but the daemon is also able to receive requests from the outside | ||||||
| world. Custom domains are supported, but no TLS. | world. Custom domains are supported, but no TLS. | ||||||
|  | @ -481,6 +503,8 @@ world. Custom domains are supported, but no TLS. | ||||||
| 
 | 
 | ||||||
| 1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). | 1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). | ||||||
| 
 | 
 | ||||||
|  | The resulting URL schemes are `http://<namespace>.example.io/<project_slug>` and `http://custom-domain.com`. | ||||||
|  | 
 | ||||||
| ### Custom domains with TLS support | ### Custom domains with TLS support | ||||||
| 
 | 
 | ||||||
| Prerequisites: | Prerequisites: | ||||||
|  | @ -489,10 +513,6 @@ Prerequisites: | ||||||
| - TLS certificate. Can be either Wildcard, or any other type meeting the [requirements](../../user/project/pages/custom_domains_ssl_tls_certification/index.md#manual-addition-of-ssltls-certificates). | - TLS certificate. Can be either Wildcard, or any other type meeting the [requirements](../../user/project/pages/custom_domains_ssl_tls_certification/index.md#manual-addition-of-ssltls-certificates). | ||||||
| - Secondary IP | - Secondary IP | ||||||
| 
 | 
 | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| URL scheme: `https://<namespace>.example.io/<project_slug>` and `https://custom-domain.com` |  | ||||||
| 
 |  | ||||||
| In that case, the Pages daemon is running, NGINX still proxies requests to | In that case, the Pages daemon is running, NGINX still proxies requests to | ||||||
| the daemon but the daemon is also able to receive requests from the outside | the daemon but the daemon is also able to receive requests from the outside | ||||||
| world. Custom domains and TLS are supported. | world. Custom domains and TLS are supported. | ||||||
|  | @ -526,6 +546,8 @@ world. Custom domains and TLS are supported. | ||||||
|    [System OAuth application](../../integration/oauth_provider.md#create-an-instance-wide-application) |    [System OAuth application](../../integration/oauth_provider.md#create-an-instance-wide-application) | ||||||
|    to use the HTTPS protocol. |    to use the HTTPS protocol. | ||||||
| 
 | 
 | ||||||
|  | The resulting URL schemes are `https://<namespace>.example.io/<project_slug>` and `https://custom-domain.com`. | ||||||
|  | 
 | ||||||
| ### Custom domain verification | ### Custom domain verification | ||||||
| 
 | 
 | ||||||
| To prevent malicious users from hijacking domains that don't belong to them, | To prevent malicious users from hijacking domains that don't belong to them, | ||||||
|  |  | ||||||
|  | @ -39,7 +39,7 @@ Before setting up a self-hosted model infrastructure, you must have: | ||||||
| - A [supported model](supported_models_and_hardware_requirements.md) (either cloud-based or on-premises). | - A [supported model](supported_models_and_hardware_requirements.md) (either cloud-based or on-premises). | ||||||
| - A [supported serving platform](supported_llm_serving_platforms.md) (either cloud-based or on-premises). | - A [supported serving platform](supported_llm_serving_platforms.md) (either cloud-based or on-premises). | ||||||
| - A locally hosted or GitLab.com AI Gateway. | - A locally hosted or GitLab.com AI Gateway. | ||||||
| - GitLab [Enterprise Edition license](../../administration/license.md). | - GitLab Ultimate + [Duo Enterprise license](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?toggle=gitlab-duo-pro). | ||||||
| 
 | 
 | ||||||
| ## Choose a configuration type | ## Choose a configuration type | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ DETAILS: | ||||||
| > - [Enabled on self-managed](https://gitlab.com/groups/gitlab-org/-/epics/15176) in GitLab 17.6. | > - [Enabled on self-managed](https://gitlab.com/groups/gitlab-org/-/epics/15176) in GitLab 17.6. | ||||||
| > - Changed to require GitLab Duo add-on in GitLab 17.6 and later. | > - Changed to require GitLab Duo add-on in GitLab 17.6 and later. | ||||||
| 
 | 
 | ||||||
| To deploy self-hosted AI models, you need **GitLab Enterprise Edition**. For more information about licensing, refer to the [licensing documentation](../../administration/license.md). | To deploy self-hosted AI models, you need GitLab Ultimate and Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial). | ||||||
| 
 | 
 | ||||||
| ## Offerings | ## Offerings | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -231,6 +231,9 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \ | ||||||
| 
 | 
 | ||||||
| Get a list of tags for given registry repository. | Get a list of tags for given registry repository. | ||||||
| 
 | 
 | ||||||
|  | NOTE: | ||||||
|  | Offset pagination is deprecated and keyset pagination is now the preferred pagination method. | ||||||
|  | 
 | ||||||
| ```plaintext | ```plaintext | ||||||
| GET /projects/:id/registry/repositories/:repository_id/tags | GET /projects/:id/registry/repositories/:repository_id/tags | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | @ -12441,6 +12441,30 @@ The edge type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage | ||||||
| | <a id="ciminutesprojectmonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | | <a id="ciminutesprojectmonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | ||||||
| | <a id="ciminutesprojectmonthlyusageedgenode"></a>`node` | [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage) | The item at the end of the edge. | | | <a id="ciminutesprojectmonthlyusageedgenode"></a>`node` | [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage) | The item at the end of the edge. | | ||||||
| 
 | 
 | ||||||
|  | #### `CiProjectSubscriptionConnection` | ||||||
|  | 
 | ||||||
|  | The connection type for [`CiProjectSubscription`](#ciprojectsubscription). | ||||||
|  | 
 | ||||||
|  | ##### Fields | ||||||
|  | 
 | ||||||
|  | | Name | Type | Description | | ||||||
|  | | ---- | ---- | ----------- | | ||||||
|  | | <a id="ciprojectsubscriptionconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. | | ||||||
|  | | <a id="ciprojectsubscriptionconnectionedges"></a>`edges` | [`[CiProjectSubscriptionEdge]`](#ciprojectsubscriptionedge) | A list of edges. | | ||||||
|  | | <a id="ciprojectsubscriptionconnectionnodes"></a>`nodes` | [`[CiProjectSubscription]`](#ciprojectsubscription) | A list of nodes. | | ||||||
|  | | <a id="ciprojectsubscriptionconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | | ||||||
|  | 
 | ||||||
|  | #### `CiProjectSubscriptionEdge` | ||||||
|  | 
 | ||||||
|  | The edge type for [`CiProjectSubscription`](#ciprojectsubscription). | ||||||
|  | 
 | ||||||
|  | ##### Fields | ||||||
|  | 
 | ||||||
|  | | Name | Type | Description | | ||||||
|  | | ---- | ---- | ----------- | | ||||||
|  | | <a id="ciprojectsubscriptionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | ||||||
|  | | <a id="ciprojectsubscriptionedgenode"></a>`node` | [`CiProjectSubscription`](#ciprojectsubscription) | The item at the end of the edge. | | ||||||
|  | 
 | ||||||
| #### `CiProjectVariableConnection` | #### `CiProjectVariableConnection` | ||||||
| 
 | 
 | ||||||
| The connection type for [`CiProjectVariable`](#ciprojectvariable). | The connection type for [`CiProjectVariable`](#ciprojectvariable). | ||||||
|  | @ -19990,6 +20014,17 @@ CI/CD variables given to a manual job. | ||||||
| | <a id="ciminutesprojectmonthlyusageproject"></a>`project` | [`Project`](#project) | Project having the recorded usage. | | | <a id="ciminutesprojectmonthlyusageproject"></a>`project` | [`Project`](#project) | Project having the recorded usage. | | ||||||
| | <a id="ciminutesprojectmonthlyusagesharedrunnersduration"></a>`sharedRunnersDuration` | [`Int`](#int) | Total duration (in seconds) of shared runners use by the project for the month. | | | <a id="ciminutesprojectmonthlyusagesharedrunnersduration"></a>`sharedRunnersDuration` | [`Int`](#int) | Total duration (in seconds) of shared runners use by the project for the month. | | ||||||
| 
 | 
 | ||||||
|  | ### `CiProjectSubscription` | ||||||
|  | 
 | ||||||
|  | #### Fields | ||||||
|  | 
 | ||||||
|  | | Name | Type | Description | | ||||||
|  | | ---- | ---- | ----------- | | ||||||
|  | | <a id="ciprojectsubscriptionauthor"></a>`author` | [`UserCore`](#usercore) | Author of the subscription. | | ||||||
|  | | <a id="ciprojectsubscriptiondownstreamproject"></a>`downstreamProject` | [`CiSubscriptionsProjectDetails`](#cisubscriptionsprojectdetails) | Downstream project of the subscription.When an upstream project's pipeline completes, a pipeline is triggered in the downstream project. | | ||||||
|  | | <a id="ciprojectsubscriptionid"></a>`id` | [`CiSubscriptionsProjectID`](#cisubscriptionsprojectid) | Global ID of the subscription. | | ||||||
|  | | <a id="ciprojectsubscriptionupstreamproject"></a>`upstreamProject` | [`CiSubscriptionsProjectDetails`](#cisubscriptionsprojectdetails) | Upstream project of the subscription.When an upstream project's pipeline completes, a pipeline is triggered in the downstream project. | | ||||||
|  | 
 | ||||||
| ### `CiProjectVariable` | ### `CiProjectVariable` | ||||||
| 
 | 
 | ||||||
| CI/CD variables for a project. | CI/CD variables for a project. | ||||||
|  | @ -20290,6 +20325,26 @@ Represents the Geo replication and verification state of a ci_secure_file. | ||||||
| | <a id="cisubscriptionsprojectid"></a>`id` | [`CiSubscriptionsProjectID`](#cisubscriptionsprojectid) | Global ID of the subscription. | | | <a id="cisubscriptionsprojectid"></a>`id` | [`CiSubscriptionsProjectID`](#cisubscriptionsprojectid) | Global ID of the subscription. | | ||||||
| | <a id="cisubscriptionsprojectupstreamproject"></a>`upstreamProject` | [`Project`](#project) | Upstream project of the subscription. | | | <a id="cisubscriptionsprojectupstreamproject"></a>`upstreamProject` | [`Project`](#project) | Upstream project of the subscription. | | ||||||
| 
 | 
 | ||||||
|  | ### `CiSubscriptionsProjectDetails` | ||||||
|  | 
 | ||||||
|  | #### Fields | ||||||
|  | 
 | ||||||
|  | | Name | Type | Description | | ||||||
|  | | ---- | ---- | ----------- | | ||||||
|  | | <a id="cisubscriptionsprojectdetailsid"></a>`id` | [`ID!`](#id) | ID of the project. | | ||||||
|  | | <a id="cisubscriptionsprojectdetailsname"></a>`name` | [`ID!`](#id) | Full path of the project. | | ||||||
|  | | <a id="cisubscriptionsprojectdetailsnamespace"></a>`namespace` | [`CiSubscriptionsProjectNamespaceDetails!`](#cisubscriptionsprojectnamespacedetails) | Namespace of the project. | | ||||||
|  | | <a id="cisubscriptionsprojectdetailsweburl"></a>`webUrl` | [`String`](#string) | Web URL of the project. | | ||||||
|  | 
 | ||||||
|  | ### `CiSubscriptionsProjectNamespaceDetails` | ||||||
|  | 
 | ||||||
|  | #### Fields | ||||||
|  | 
 | ||||||
|  | | Name | Type | Description | | ||||||
|  | | ---- | ---- | ----------- | | ||||||
|  | | <a id="cisubscriptionsprojectnamespacedetailsid"></a>`id` | [`ID!`](#id) | ID of the project. | | ||||||
|  | | <a id="cisubscriptionsprojectnamespacedetailsname"></a>`name` | [`ID!`](#id) | Full path of the project. | | ||||||
|  | 
 | ||||||
| ### `CiTemplate` | ### `CiTemplate` | ||||||
| 
 | 
 | ||||||
| GitLab CI/CD configuration template. | GitLab CI/CD configuration template. | ||||||
|  | @ -30567,10 +30622,12 @@ Project-level settings for product analytics provider. | ||||||
| | <a id="projectciaccessauthorizedagents"></a>`ciAccessAuthorizedAgents` | [`ClusterAgentAuthorizationCiAccessConnection`](#clusteragentauthorizationciaccessconnection) | Authorized cluster agents for the project through ci_access keyword. (see [Connections](#connections)) | | | <a id="projectciaccessauthorizedagents"></a>`ciAccessAuthorizedAgents` | [`ClusterAgentAuthorizationCiAccessConnection`](#clusteragentauthorizationciaccessconnection) | Authorized cluster agents for the project through ci_access keyword. (see [Connections](#connections)) | | ||||||
| | <a id="projectcicdsettings"></a>`ciCdSettings` | [`ProjectCiCdSetting`](#projectcicdsetting) | CI/CD settings for the project. | | | <a id="projectcicdsettings"></a>`ciCdSettings` | [`ProjectCiCdSetting`](#projectcicdsetting) | CI/CD settings for the project. | | ||||||
| | <a id="projectciconfigpathordefault"></a>`ciConfigPathOrDefault` | [`String!`](#string) | Path of the CI configuration file. | | | <a id="projectciconfigpathordefault"></a>`ciConfigPathOrDefault` | [`String!`](#string) | Path of the CI configuration file. | | ||||||
|  | | <a id="projectcidownstreamprojectsubscriptions"></a>`ciDownstreamProjectSubscriptions` **{warning-solid}** | [`CiProjectSubscriptionConnection`](#ciprojectsubscriptionconnection) | **Introduced** in GitLab 17.6. **Status**: Experiment. Pipeline subscriptions where this project is the upstream project.When this project's pipeline completes, a pipeline is triggered in the downstream project. | | ||||||
| | <a id="projectcijobtokenauthlogs"></a>`ciJobTokenAuthLogs` **{warning-solid}** | [`CiJobTokenAuthLogConnection`](#cijobtokenauthlogconnection) | **Introduced** in GitLab 17.6. **Status**: Experiment. The CI Job Tokens authorization logs. | | | <a id="projectcijobtokenauthlogs"></a>`ciJobTokenAuthLogs` **{warning-solid}** | [`CiJobTokenAuthLogConnection`](#cijobtokenauthlogconnection) | **Introduced** in GitLab 17.6. **Status**: Experiment. The CI Job Tokens authorization logs. | | ||||||
| | <a id="projectcijobtokenscope"></a>`ciJobTokenScope` | [`CiJobTokenScopeType`](#cijobtokenscopetype) | The CI Job Tokens scope of access. | | | <a id="projectcijobtokenscope"></a>`ciJobTokenScope` | [`CiJobTokenScopeType`](#cijobtokenscopetype) | The CI Job Tokens scope of access. | | ||||||
| | <a id="projectcisubscribedprojects"></a>`ciSubscribedProjects` | [`CiSubscriptionsProjectConnection`](#cisubscriptionsprojectconnection) | Pipeline subscriptions for projects subscribed to the project. (see [Connections](#connections)) | | | <a id="projectcisubscribedprojects"></a>`ciSubscribedProjects` **{warning-solid}** | [`CiSubscriptionsProjectConnection`](#cisubscriptionsprojectconnection) | **Deprecated** in GitLab 17.6. Use `ciDownstreamProjectSubscriptions`. | | ||||||
| | <a id="projectcisubscriptionsprojects"></a>`ciSubscriptionsProjects` | [`CiSubscriptionsProjectConnection`](#cisubscriptionsprojectconnection) | Pipeline subscriptions for the project. (see [Connections](#connections)) | | | <a id="projectcisubscriptionsprojects"></a>`ciSubscriptionsProjects` **{warning-solid}** | [`CiSubscriptionsProjectConnection`](#cisubscriptionsprojectconnection) | **Deprecated** in GitLab 17.6. Use `ciUpstreamProjectSubscriptions`. | | ||||||
|  | | <a id="projectciupstreamprojectsubscriptions"></a>`ciUpstreamProjectSubscriptions` **{warning-solid}** | [`CiProjectSubscriptionConnection`](#ciprojectsubscriptionconnection) | **Introduced** in GitLab 17.6. **Status**: Experiment. Pipeline subscriptions where this project is the downstream project.When an upstream project's pipeline completes, a pipeline is triggered in the downstream project (this project). | | ||||||
| | <a id="projectcodecoveragesummary"></a>`codeCoverageSummary` | [`CodeCoverageSummary`](#codecoveragesummary) | Code coverage summary associated with the project. | | | <a id="projectcodecoveragesummary"></a>`codeCoverageSummary` | [`CodeCoverageSummary`](#codecoveragesummary) | Code coverage summary associated with the project. | | ||||||
| | <a id="projectcomplianceframeworks"></a>`complianceFrameworks` | [`ComplianceFrameworkConnection`](#complianceframeworkconnection) | Compliance frameworks associated with the project. (see [Connections](#connections)) | | | <a id="projectcomplianceframeworks"></a>`complianceFrameworks` | [`ComplianceFrameworkConnection`](#complianceframeworkconnection) | Compliance frameworks associated with the project. (see [Connections](#connections)) | | ||||||
| | <a id="projectcontainerexpirationpolicy"></a>`containerExpirationPolicy` **{warning-solid}** | [`ContainerExpirationPolicy`](#containerexpirationpolicy) | **Deprecated** in GitLab 17.5. Use `container_tags_expiration_policy`. | | | <a id="projectcontainerexpirationpolicy"></a>`containerExpirationPolicy` **{warning-solid}** | [`ContainerExpirationPolicy`](#containerexpirationpolicy) | **Deprecated** in GitLab 17.5. Use `container_tags_expiration_policy`. | | ||||||
|  |  | ||||||
|  | @ -7,12 +7,12 @@ info: Analysis of Application Settings for Cells 1.0. | ||||||
| 
 | 
 | ||||||
| ## Statistics | ## Statistics | ||||||
| 
 | 
 | ||||||
| - Number of attributes: 499 | - Number of attributes: 503 | ||||||
| - Number of encrypted attributes: 43 (9.0%) | - Number of encrypted attributes: 43 (9.0%) | ||||||
| - Number of attributes documented: 309 (62.0%) | - Number of attributes documented: 310 (62.0%) | ||||||
| - Number of attributes on GitLab.com different from the defaults: 218 (44.0%) | - Number of attributes on GitLab.com different from the defaults: 220 (44.0%) | ||||||
| - Number of attributes with `clusterwide` set: 498 (100.0%) | - Number of attributes with `clusterwide` set: 503 (100.0%) | ||||||
| - Number of attributes with `clusterwide: true` set: 120 (24.0%) | - Number of attributes with `clusterwide: true` set: 121 (24.0%) | ||||||
| 
 | 
 | ||||||
| ## Individual columns | ## Individual columns | ||||||
| 
 | 
 | ||||||
|  | @ -33,7 +33,7 @@ info: Analysis of Application Settings for Cells 1.0. | ||||||
| | `allow_possible_spam` | `false` | `boolean` | `` | `true` | `false` | `false` | `false`| `false` | | | `allow_possible_spam` | `false` | `boolean` | `` | `true` | `false` | `false` | `false`| `false` | | ||||||
| | `allow_project_creation_for_guest_and_below` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | | `allow_project_creation_for_guest_and_below` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | ||||||
| | `allow_runner_registration_token` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | | `allow_runner_registration_token` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | ||||||
| | `allow_top_level_group_owners_to_create_service_accounts` | `false` | `boolean` | `` | `true` | `false` | `false` | `???`| `false` | | | `allow_top_level_group_owners_to_create_service_accounts` | `false` | `boolean` | `` | `true` | `false` | `false` | `false`| `false` | | ||||||
| | `anthropic_api_key` | `true` | `bytea` | `` | `false` | `null` | `false` | `false`| `false` | | | `anthropic_api_key` | `true` | `bytea` | `` | `false` | `null` | `false` | `false`| `false` | | ||||||
| | `archive_builds_in_seconds` | `false` | `integer` | `` | `false` | `null` | `false` | `false`| `false` | | | `archive_builds_in_seconds` | `false` | `integer` | `` | `false` | `null` | `false` | `false`| `false` | | ||||||
| | `arkose_labs_client_secret` | `true` | `bytea` | `` | `false` | `null` | `true` | `true`| `false` | | | `arkose_labs_client_secret` | `true` | `bytea` | `` | `false` | `null` | `true` | `true`| `false` | | ||||||
|  | @ -240,6 +240,7 @@ info: Analysis of Application Settings for Cells 1.0. | ||||||
| | `housekeeping_incremental_repack_period` | `false` | `integer` | `integer` | `true` | `10` | `false` | `false`| `true` | | | `housekeeping_incremental_repack_period` | `false` | `integer` | `integer` | `true` | `10` | `false` | `false`| `true` | | ||||||
| | `html_emails_enabled` | `false` | `boolean` | `boolean` | `false` | `true` | `false` | `false`| `true` | | | `html_emails_enabled` | `false` | `boolean` | `boolean` | `false` | `true` | `false` | `false`| `true` | | ||||||
| | `id` | `false` | `bigint` | `` | `true` | `???` | `false` | `false`| `false` | | | `id` | `false` | `bigint` | `` | `true` | `???` | `false` | `false`| `false` | | ||||||
|  | | `identity_verification_settings` | `false` | `jsonb` | `` | `true` | `'{}'::jsonb` | `true` | `true`| `false` | | ||||||
| | `import_sources` | `false` | `text` | `array of strings` | `false` | `null` | `true` | `true`| `true` | | | `import_sources` | `false` | `text` | `array of strings` | `false` | `null` | `true` | `true`| `true` | | ||||||
| | `importers` | `false` | `jsonb` | `` | `true` | `'{}'::jsonb` | `true` | `true`| `false` | | | `importers` | `false` | `jsonb` | `` | `true` | `'{}'::jsonb` | `true` | `true`| `false` | | ||||||
| | `inactive_projects_delete_after_months` | `false` | `integer` | `` | `true` | `2` | `false` | `false`| `false` | | | `inactive_projects_delete_after_months` | `false` | `integer` | `` | `true` | `2` | `false` | `false`| `false` | | ||||||
|  | @ -420,6 +421,7 @@ info: Analysis of Application Settings for Cells 1.0. | ||||||
| | `sidekiq_job_limiter_compression_threshold_bytes` | `false` | `integer` | `integer` | `true` | `100000` | `false` | `false`| `true` | | | `sidekiq_job_limiter_compression_threshold_bytes` | `false` | `integer` | `integer` | `true` | `100000` | `false` | `false`| `true` | | ||||||
| | `sidekiq_job_limiter_limit_bytes` | `false` | `integer` | `integer` | `true` | `0` | `true` | `false`| `true` | | | `sidekiq_job_limiter_limit_bytes` | `false` | `integer` | `integer` | `true` | `0` | `true` | `false`| `true` | | ||||||
| | `sidekiq_job_limiter_mode` | `false` | `smallint` | `string` | `true` | `1` | `false` | `false`| `true` | | | `sidekiq_job_limiter_mode` | `false` | `smallint` | `string` | `true` | `1` | `false` | `false`| `true` | | ||||||
|  | | `sign_in_restrictions` | `false` | `jsonb` | `` | `true` | `'{}'::jsonb` | `true` | `false`| `false` | | ||||||
| | `signup_enabled` | `false` | `boolean` | `boolean` | `false` | `null` | `true` | `false`| `true` | | | `signup_enabled` | `false` | `boolean` | `boolean` | `false` | `null` | `true` | `false`| `true` | | ||||||
| | `silent_mode_enabled` | `false` | `boolean` | `boolean` | `true` | `false` | `false` | `false`| `true` | | | `silent_mode_enabled` | `false` | `boolean` | `boolean` | `true` | `false` | `false` | `false`| `true` | | ||||||
| | `slack_app_enabled` | `false` | `boolean` | `boolean` | `false` | `false` | `true` | `false`| `true` | | | `slack_app_enabled` | `false` | `boolean` | `boolean` | `false` | `false` | `true` | `false`| `true` | | ||||||
|  | @ -486,6 +488,7 @@ info: Analysis of Application Settings for Cells 1.0. | ||||||
| | `throttle_unauthenticated_period_in_seconds` | `false` | `integer` | `integer` | `true` | `3600` | `true` | `false`| `true` | | | `throttle_unauthenticated_period_in_seconds` | `false` | `integer` | `integer` | `true` | `3600` | `true` | `false`| `true` | | ||||||
| | `throttle_unauthenticated_requests_per_period` | `false` | `integer` | `integer` | `true` | `3600` | `true` | `false`| `true` | | | `throttle_unauthenticated_requests_per_period` | `false` | `integer` | `integer` | `true` | `3600` | `true` | `false`| `true` | | ||||||
| | `time_tracking_limit_to_hours` | `false` | `boolean` | `boolean` | `true` | `false` | `true` | `false`| `true` | | | `time_tracking_limit_to_hours` | `false` | `boolean` | `boolean` | `true` | `false` | `true` | `false`| `true` | | ||||||
|  | | `transactional_emails` | `false` | `jsonb` | `` | `true` | `'{}'::jsonb` | `false` | `false`| `false` | | ||||||
| | `two_factor_grace_period` | `false` | `integer` | `integer` | `false` | `48` | `false` | `false`| `true` | | | `two_factor_grace_period` | `false` | `integer` | `integer` | `false` | `48` | `false` | `false`| `true` | | ||||||
| | `unconfirmed_users_delete_after_days` | `false` | `integer` | `integer` | `true` | `7` | `true` | `true`| `true` | | | `unconfirmed_users_delete_after_days` | `false` | `integer` | `integer` | `true` | `7` | `true` | `true`| `true` | | ||||||
| | `unique_ips_limit_enabled` | `false` | `boolean` | `boolean` | `true` | `false` | `false` | `false`| `true` | | | `unique_ips_limit_enabled` | `false` | `boolean` | `boolean` | `true` | `false` | `false` | `false`| `true` | | ||||||
|  |  | ||||||
|  | @ -2073,7 +2073,7 @@ Searching is different from [filtering](#filter). | ||||||
| 
 | 
 | ||||||
| When referring to the subscription billing model: | When referring to the subscription billing model: | ||||||
| 
 | 
 | ||||||
| - For GitLab SaaS, use **seats**. Customers purchase seats. Users occupy seats when they are invited | - For GitLab.com, use **seats**. Customers purchase seats. Users occupy seats when they are invited | ||||||
|   to a group, with some [exceptions](../../../subscriptions/gitlab_com/index.md#how-seat-usage-is-determined). |   to a group, with some [exceptions](../../../subscriptions/gitlab_com/index.md#how-seat-usage-is-determined). | ||||||
| - For GitLab self-managed, use **users**. Customers purchase subscriptions for a specified number of **users**. | - For GitLab self-managed, use **users**. Customers purchase subscriptions for a specified number of **users**. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,327 @@ | ||||||
|  | --- | ||||||
|  | stage: Create | ||||||
|  | group: Source Code | ||||||
|  | description: Common commands and workflows. | ||||||
|  | info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments" | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | # File management | ||||||
|  | 
 | ||||||
|  | Git provides file management capabilities that help you to track changes, | ||||||
|  | collaborate with others, and manage large files efficiently. | ||||||
|  | 
 | ||||||
|  | ## File history | ||||||
|  | 
 | ||||||
|  | Use `git log` to view a file's complete history and understand how it has changed over time. | ||||||
|  | The file history shows you: | ||||||
|  | 
 | ||||||
|  | - The author of each change. | ||||||
|  | - The date and time of each modification. | ||||||
|  | - The specific changes made in each commit. | ||||||
|  | 
 | ||||||
|  | For example, to view `history` information about the `CONTRIBUTING.md` file in the root | ||||||
|  | of the `gitlab` repository, run: | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | git log CONTRIBUTING.md | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Example output: | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | commit b350bf041666964c27834885e4590d90ad0bfe90 | ||||||
|  | Author: Nick Malcolm <nmalcolm@gitlab.com> | ||||||
|  | Date:   Fri Dec 8 13:43:07 2023 +1300 | ||||||
|  | 
 | ||||||
|  |     Update security contact and vulnerability disclosure info | ||||||
|  | 
 | ||||||
|  | commit 8e4c7f26317ff4689610bf9d031b4931aef54086 | ||||||
|  | Author: Brett Walker <bwalker@gitlab.com> | ||||||
|  | Date:   Fri Oct 20 17:53:25 2023 +0000 | ||||||
|  | 
 | ||||||
|  |     Fix link to Code of Conduct | ||||||
|  | 
 | ||||||
|  |     and condense some of the verbiage | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Check previous changes to a file | ||||||
|  | 
 | ||||||
|  | Use `git blame` to see who made the last change to a file and when. | ||||||
|  | This helps to understand the context of a file's content, | ||||||
|  | resolve conflicts, and identify the person responsible for a specific change. | ||||||
|  | 
 | ||||||
|  | If you want to find `blame` information about a `README.md` file in the local directory: | ||||||
|  | 
 | ||||||
|  | 1. Open a terminal or command prompt. | ||||||
|  | 1. Go to your Git repository. | ||||||
|  | 1. Run the following command: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git blame README.md | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. To navigate the results page, press <kbd>Space</kbd>. | ||||||
|  | 1. To exit out of the results, press <kbd>Q</kbd>. | ||||||
|  | 
 | ||||||
|  | This output displays the file content with annotations showing the commit SHA, author, | ||||||
|  | and date for each line. For example: | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | 58233c4f1054c (Dan Rhodes           2022-05-13 07:02:20 +0000  1) ## Contributor License Agreement | ||||||
|  | b87768f435185 (Jamie Hurewitz       2017-10-31 18:09:23 +0000  2) | ||||||
|  | 8e4c7f26317ff (Brett Walker         2023-10-20 17:53:25 +0000  3) Contributions to this repository are subject to the | ||||||
|  | 58233c4f1054c (Dan Rhodes           2022-05-13 07:02:20 +0000  4) | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Git LFS | ||||||
|  | 
 | ||||||
|  | Git Large File Storage (LFS) is an extension that helps you manage large files in Git repositories. | ||||||
|  | It replaces large files with text pointers in Git, and stores the file contents on a remote server. | ||||||
|  | 
 | ||||||
|  | Prerequisites: | ||||||
|  | 
 | ||||||
|  | - Download and install the appropriate version of the [CLI extension for Git LFS](https://git-lfs.com) for your operating system. | ||||||
|  | - [Configure your project to use Git LFS](lfs/index.md). | ||||||
|  | - Install the Git LFS pre-push hook. To do this, run `git lfs install` in the root directory of your repository. | ||||||
|  | 
 | ||||||
|  | ### Add and track files | ||||||
|  | 
 | ||||||
|  | To add a large file into your Git repository and track it with Git LFS: | ||||||
|  | 
 | ||||||
|  | 1. Configure tracking for all files of a certain type. Replace `iso` with your desired file type: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs track "*.iso" | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    This command creates a `.gitattributes` file with instructions to handle all | ||||||
|  |    ISO files with Git LFS. The following line is added to your `.gitattributes` file: | ||||||
|  | 
 | ||||||
|  |    ```plaintext | ||||||
|  |    *.iso filter=lfs -text | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Add a file of that type, `.iso`, to your repository. | ||||||
|  | 1. Track the changes to both the `.gitattributes` file and the `.iso` file: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git add . | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Ensure you've added both files: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git status | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    The `.gitattributes` file must be included in your commit. | ||||||
|  |    It if isn't included, Git does not track the ISO file with Git LFS. | ||||||
|  | 
 | ||||||
|  |    NOTE: | ||||||
|  |    Ensure the files you're changing are not listed in a `.gitignore` file. | ||||||
|  |    If they are, Git commits the change locally but doesn't push it to your upstream repository. | ||||||
|  | 
 | ||||||
|  | 1. Commit both files to your local copy of the repository: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git commit -m "Add an ISO file and .gitattributes" | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Push your changes upstream. Replace `main` with the name of your branch: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git push origin main | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Create a merge request. | ||||||
|  | 
 | ||||||
|  | NOTE: | ||||||
|  | When you add a new file type to Git LFS tracking, existing files of this type | ||||||
|  | are not converted to Git LFS. Only files of this type, added after you begin tracking, are added to Git LFS. Use `git lfs migrate` to convert existing files to use Git LFS. | ||||||
|  | 
 | ||||||
|  | ### Stop tracking a file | ||||||
|  | 
 | ||||||
|  | When you stop tracking a file with Git LFS, the file remains on disk because it's still | ||||||
|  | part of your repository's history. | ||||||
|  | 
 | ||||||
|  | To stop tracking a file with Git LFS: | ||||||
|  | 
 | ||||||
|  | 1. Run the `git lfs untrack` command and provide the path to the file: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs untrack doc/example.iso | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Use the `touch` command to convert it back to a standard file: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    touch doc/example.iso | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Track the changes to the file: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git add . | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Commit and push your changes. | ||||||
|  | 1. Create a merge request and request a review. | ||||||
|  | 1. Merge the request into the target branch. | ||||||
|  | 
 | ||||||
|  | NOTE: | ||||||
|  | If you delete an object tracked by Git LFS, without tracking it with `git lfs untrack`, | ||||||
|  | the object shows as `modified` in `git status`. | ||||||
|  | 
 | ||||||
|  | ### Stop tracking all files of a single type | ||||||
|  | 
 | ||||||
|  | To stop tracking all files of a particular type in Git LFS: | ||||||
|  | 
 | ||||||
|  | 1. Run the `git lfs untrack` command and provide the file type to stop tracking: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs untrack "*.iso" | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Use the `touch` command to convert the files back to standard files: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    touch *.iso | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Track the changes to the files: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git add . | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Commit and push your changes. | ||||||
|  | 1. Create a merge request and request a review. | ||||||
|  | 1. Merge the request into the target branch. | ||||||
|  | 
 | ||||||
|  | ## File locks | ||||||
|  | 
 | ||||||
|  | File locks help prevent conflicts and ensure that only one person can edit a file at a time. | ||||||
|  | It's a good option for: | ||||||
|  | 
 | ||||||
|  | - Binary files that can't be merged. For example, design files and videos. | ||||||
|  | - Files that require exclusive access during editing. | ||||||
|  | 
 | ||||||
|  | Prerequisites: | ||||||
|  | 
 | ||||||
|  | - You must have [Git LFS installed](../../topics/git/lfs/index.md). | ||||||
|  | - You must have the Maintainer role for the project. | ||||||
|  | 
 | ||||||
|  | ### Configure file locks | ||||||
|  | 
 | ||||||
|  | To configure file locks for a specific file type: | ||||||
|  | 
 | ||||||
|  | 1. Use the `git lfs track` command with the `--lockable` option. For example, to configure PNG files: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs track "*.png" --lockable | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    This command creates or updates your `.gitattributes` file with the following content: | ||||||
|  | 
 | ||||||
|  |     ```plaintext | ||||||
|  |     *.png filter=lfs diff=lfs merge=lfs -text lockable | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | 1. Push the `.gitattributes` file to the remote repository for the changes to take effect. | ||||||
|  | 
 | ||||||
|  |    NOTE: | ||||||
|  |    After a file type is registered as lockable, it is automatically marked as read-only. | ||||||
|  | 
 | ||||||
|  | #### Configure file locks without LFS | ||||||
|  | 
 | ||||||
|  | To register a file type as lockable without using Git LFS: | ||||||
|  | 
 | ||||||
|  | 1. Edit the `.gitattributes` file manually: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    *.pdf lockable | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Push the `.gitattributes` file to the remote repository. | ||||||
|  | 
 | ||||||
|  | ### Lock and unlock files | ||||||
|  | 
 | ||||||
|  | To lock or unlock a file with exclusive file locking: | ||||||
|  | 
 | ||||||
|  | 1. Open a terminal window in your repository directory. | ||||||
|  | 1. Run one of the following commands: | ||||||
|  | 
 | ||||||
|  |    ::Tabs | ||||||
|  | 
 | ||||||
|  |    :::TabTitle Lock a file | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs lock path/to/file.png | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    :::TabTitle Unlock a file | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs unlock path/to/file.png | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    :::TabTitle Unlock a file by ID | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs unlock --id=123 | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    :::TabTitle Force unlock a file | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs unlock --id=123 --force | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    ::EndTabs | ||||||
|  | 
 | ||||||
|  | ### View locked files | ||||||
|  | 
 | ||||||
|  | To view locked files: | ||||||
|  | 
 | ||||||
|  | 1. Open a terminal window in your repository. | ||||||
|  | 1. Run the following command: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs locks | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  |    The output lists the locked files, the users who locked them, and the file IDs. | ||||||
|  | 
 | ||||||
|  | In the GitLab UI: | ||||||
|  | 
 | ||||||
|  | - The repository file tree displays an LFS badge for files tracked by Git LFS. | ||||||
|  | - Exclusively-locked files show a padlock icon. | ||||||
|  | LFS-Locked files | ||||||
|  | 
 | ||||||
|  | You can also [view and remove existing locks](../../user/project/file_lock.md) from the GitLab UI. | ||||||
|  | 
 | ||||||
|  | NOTE: | ||||||
|  | When you rename an exclusively-locked file, the lock is lost. You must lock it again to keep it locked. | ||||||
|  | 
 | ||||||
|  | ### Lock and edit a file | ||||||
|  | 
 | ||||||
|  | To lock a file, edit it, and optionally unlock it: | ||||||
|  | 
 | ||||||
|  | 1. Lock the file: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs lock <file_path> | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | 1. Edit the file. | ||||||
|  | 1. Optional. Unlock the file when you're done: | ||||||
|  | 
 | ||||||
|  |    ```shell | ||||||
|  |    git lfs unlock <file_path> | ||||||
|  |    ``` | ||||||
|  | 
 | ||||||
|  | ## Related topics | ||||||
|  | 
 | ||||||
|  | - [File management with the GitLab UI](../../user/project/repository/files/index.md) | ||||||
|  | - [Git Large File Storage (LFS) documentation](lfs/index.md) | ||||||
|  | - [File locking](../../user/project/file_lock.md) | ||||||
|  | @ -81,171 +81,6 @@ It offers both server settings and project-specific settings. | ||||||
|        handling for individual files and file types. |        handling for individual files and file types. | ||||||
|   1. Add the files and file types you want to track with Git LFS. |   1. Add the files and file types you want to track with Git LFS. | ||||||
| 
 | 
 | ||||||
| ## Add a file with Git LFS |  | ||||||
| 
 |  | ||||||
| Prerequisites: |  | ||||||
| 
 |  | ||||||
| - You have downloaded and installed the appropriate version of the |  | ||||||
|   [CLI extension for Git LFS](https://git-lfs.com) for your operating system. |  | ||||||
| - Your project is [configured to use Git LFS](#configure-git-lfs-for-a-project). |  | ||||||
| 
 |  | ||||||
| To add a large file into your Git repository and immediately track it with Git LFS: |  | ||||||
| 
 |  | ||||||
| 1. To track all files of a certain type with Git LFS, rather than a single file, |  | ||||||
|    run this command, replacing `iso` with your desired file type: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git lfs track "*.iso" |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
|    This command creates a `.gitattributes` file with instructions to handle all |  | ||||||
|    ISO files with Git LFS. The line in your `.gitattributes` file looks like this: |  | ||||||
| 
 |  | ||||||
|    ```plaintext |  | ||||||
|    *.iso filter=lfs -text |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Add a file of that type (`.iso`) to your repository. |  | ||||||
| 1. Tell Git to track the changes to both the `.gitattributes` file and the `.iso` file: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git add . |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. To ensure you've added both files, run `git status`. If the `.gitattributes` file |  | ||||||
|    isn't included in your commit, users who clone your repository don't get the |  | ||||||
|    files they need. |  | ||||||
| 1. Commit both files to your local copy of your repository: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git commit -am "Add an ISO file and .gitattributes" |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Push your changes back upstream, replacing `main` with the name of your branch: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git push origin main |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
|    Make sure the files you are changing aren't listed in a `.gitignore` file. |  | ||||||
|    If this file (or file type) is in your `.gitignore` file, Git commits |  | ||||||
|    the change locally, but does not push it to your upstream repository. |  | ||||||
| 
 |  | ||||||
| 1. Create your merge request. |  | ||||||
| 
 |  | ||||||
| ### Add a file type to Git LFS |  | ||||||
| 
 |  | ||||||
| When you add a new file type into Git LFS tracking, existing files of this type |  | ||||||
| are _not_ converted to Git LFS. Files of this type added _after_ you begin |  | ||||||
| tracking are added to Git LFS. To convert existing files of that type to |  | ||||||
| use Git LFS, use `git lfs migrate`. |  | ||||||
| 
 |  | ||||||
| Prerequisites: |  | ||||||
| 
 |  | ||||||
| - You have downloaded and installed the appropriate version of the |  | ||||||
|   [CLI extension for Git LFS](https://git-lfs.com) for your operating system. |  | ||||||
| - Your project is [configured to use Git LFS](#configure-git-lfs-for-a-project). |  | ||||||
| 
 |  | ||||||
| To start tracking a file type in Git LFS: |  | ||||||
| 
 |  | ||||||
| 1. Make sure this file type isn't listed in your project's `.gitignore` file. |  | ||||||
|    If this file type is in your `.gitignore` file, Git commits your changes |  | ||||||
|    locally, but does not push it to your upstream repository. |  | ||||||
| 1. Decide what file types to track with Git LFS. For each file type, run this |  | ||||||
|    command, replacing `iso` with your desired file type: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git lfs track "*.iso" |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Tell Git to track the changes to the `.gitattributes` file. Commit the |  | ||||||
|    file to your local copy of your repository, replacing `iso` with your desired file type: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git add . |  | ||||||
|    git commit -am "Use Git LFS for files of type .iso" |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Push your changes back upstream, replacing `filetype` with the name of your branch: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git push origin filetype |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| ## Stop tracking a file with Git LFS |  | ||||||
| 
 |  | ||||||
| When you stop tracking a file with Git LFS, the file remains on disk because it remains part of your repository's |  | ||||||
| history. To understand why, see [Delete a Git LFS file from repository history](#delete-a-git-lfs-file-from-repository-history). |  | ||||||
| 
 |  | ||||||
| Prerequisites: |  | ||||||
| 
 |  | ||||||
| - You have downloaded and installed the appropriate version of the |  | ||||||
|   [CLI extension for Git LFS](https://git-lfs.com) for your operating system. |  | ||||||
| - You have installed the Git LFS pre-push hook by running `git lfs install` |  | ||||||
|   in the root directory of your repository. |  | ||||||
| 
 |  | ||||||
| To stop tracking a file with Git LFS: |  | ||||||
| 
 |  | ||||||
| 1. Run the [`git lfs untrack`](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-untrack.adoc) |  | ||||||
|    command and provide the path to the file: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git lfs untrack doc/example.iso |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Use the `touch` command to convert it back to a standard file: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    touch doc/example.iso |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Tell Git to track the changes to the file: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git add . |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Commit and push your changes. |  | ||||||
| 1. Create a merge request and request a review. |  | ||||||
| 1. After you get the required approvals, merge the request into the target branch. |  | ||||||
| 
 |  | ||||||
| If you delete an object (`example.iso`) tracked by Git LFS, but don't use |  | ||||||
| the `git lfs untrack` command, `example.iso` shows as `modified` in `git status`. |  | ||||||
| 
 |  | ||||||
| ### Stop tracking all files of a single type |  | ||||||
| 
 |  | ||||||
| Prerequisites: |  | ||||||
| 
 |  | ||||||
| - You have downloaded and installed the appropriate version of the |  | ||||||
|   [CLI extension for Git LFS](https://git-lfs.com) for your operating system. |  | ||||||
| - You have installed the Git LFS pre-push hook by running `git lfs install` |  | ||||||
|   in the root directory of your repository. |  | ||||||
| 
 |  | ||||||
| To stop tracking all files of a particular type in Git LFS: |  | ||||||
| 
 |  | ||||||
| 1. Run the [`git lfs untrack`](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-untrack.adoc) |  | ||||||
|    command and provide the file type to stop tracking: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git lfs untrack "*.iso" |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Use the `touch` command to convert the files back to standard files: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    touch *.iso |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Tell Git to track the changes to the files: |  | ||||||
| 
 |  | ||||||
|    ```shell |  | ||||||
|    git add . |  | ||||||
|    ``` |  | ||||||
| 
 |  | ||||||
| 1. Commit and push your changes. |  | ||||||
| 1. Create a merge request and request a review. |  | ||||||
| 1. After you get the required approvals, merge the request into the target branch. |  | ||||||
| 
 |  | ||||||
| ## Enable or disable Git LFS for a project | ## Enable or disable Git LFS for a project | ||||||
| 
 | 
 | ||||||
| Git LFS is enabled by default for both self-managed instances and GitLab.com. | Git LFS is enabled by default for both self-managed instances and GitLab.com. | ||||||
|  | @ -262,6 +97,12 @@ To enable or disable Git LFS at the project level: | ||||||
| 1. Select the **Git Large File Storage (LFS)** toggle. | 1. Select the **Git Large File Storage (LFS)** toggle. | ||||||
| 1. Select **Save changes**. | 1. Select **Save changes**. | ||||||
| 
 | 
 | ||||||
|  | ## Add and track files | ||||||
|  | 
 | ||||||
|  | You can add large files to Git LFS. This helps you manage files in Git repositories. | ||||||
|  | When you track files with Git LFS, they are replaced with text pointers in Git, | ||||||
|  | and stored on a remote server. For more information, see [Git LFS](../../../topics/git/file_management.md#git-lfs). | ||||||
|  | 
 | ||||||
| ## Clone a repository that uses Git LFS | ## Clone a repository that uses Git LFS | ||||||
| 
 | 
 | ||||||
| When you clone a repository that uses Git LFS, Git detects the LFS-tracked files | When you clone a repository that uses Git LFS, Git detects the LFS-tracked files | ||||||
|  | @ -308,8 +149,9 @@ the total size of your repository, see | ||||||
| 
 | 
 | ||||||
| ## Related topics | ## Related topics | ||||||
| 
 | 
 | ||||||
| - Use Git LFS to set up [exclusive file locks](../../../user/project/file_lock.md#exclusive-file-locks). | - Use Git LFS to set up [exclusive file locks](../file_management.md#configure-file-locks). | ||||||
| - Blog post: [Getting started with Git LFS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/) | - Blog post: [Getting started with Git LFS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/) | ||||||
|  | - [Git LFS with Git](../../../topics/git/file_management.md#git-lfs) | ||||||
| - [Git LFS developer information](../../../development/lfs.md) | - [Git LFS developer information](../../../development/lfs.md) | ||||||
| - [GitLab Git Large File Storage (LFS) Administration](../../../administration/lfs/index.md) for self-managed instances | - [GitLab Git Large File Storage (LFS) Administration](../../../administration/lfs/index.md) for self-managed instances | ||||||
| - [Troubleshooting Git LFS](troubleshooting.md) | - [Troubleshooting Git LFS](troubleshooting.md) | ||||||
|  |  | ||||||
|  | @ -500,26 +500,6 @@ You can configure a custom limit on self-managed instances with the `scan_execut | ||||||
| 
 | 
 | ||||||
| <div class="deprecation breaking-change" data-milestone="18.0"> | <div class="deprecation breaking-change" data-milestone="18.0"> | ||||||
| 
 | 
 | ||||||
| ### List container registry repository tags API endpoint pagination |  | ||||||
| 
 |  | ||||||
| <div class="deprecation-notes"> |  | ||||||
| 
 |  | ||||||
| - Announced in GitLab <span class="milestone">16.10</span> |  | ||||||
| - Removal in GitLab <span class="milestone">18.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/432470). |  | ||||||
| 
 |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| You can use the container registry REST API to [get a list of registry repository tags](https://docs.gitlab.com/ee/api/container_registry.html#list-registry-repository-tags). We plan to improve this endpoint, adding more metadata and new features like improved sorting and filtering. |  | ||||||
| 
 |  | ||||||
| While offset-based pagination was already available for this endpoint, keyset-based pagination was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/432470) in GitLab 16.10 for GitLab.com only. This is now the preferred pagination method. |  | ||||||
| 
 |  | ||||||
| Offset-based pagination for the [List registry repository tags](https://docs.gitlab.com/ee/api/container_registry.html#list-registry-repository-tags) endpoint is deprecated in GitLab 16.10 and will be removed in 18.0. Instead, use the keyset-based pagination. |  | ||||||
| 
 |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <div class="deprecation breaking-change" data-milestone="18.0"> |  | ||||||
| 
 |  | ||||||
| ### OpenTofu CI/CD template | ### OpenTofu CI/CD template | ||||||
| 
 | 
 | ||||||
| <div class="deprecation-notes"> | <div class="deprecation-notes"> | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ said to have "released the lock". | ||||||
| 
 | 
 | ||||||
| GitLab supports two different modes of file locking: | GitLab supports two different modes of file locking: | ||||||
| 
 | 
 | ||||||
| - [Exclusive file locks](#exclusive-file-locks) for binary files: done | - [Exclusive file locks](../../topics/git/file_management.md#file-locks) for binary files: done | ||||||
|   **through the command line** with Git LFS and `.gitattributes`, it prevents locked |   **through the command line** with Git LFS and `.gitattributes`, it prevents locked | ||||||
|   files from being modified on any branch. |   files from being modified on any branch. | ||||||
| - [Default branch locks](#default-branch-file-and-directory-locks): done | - [Default branch locks](#default-branch-file-and-directory-locks): done | ||||||
|  | @ -44,156 +44,6 @@ users are prevented from modifying locked files by pushing, merging, | ||||||
| or any other means, and are shown an error like: | or any other means, and are shown an error like: | ||||||
| `'.gitignore' is locked by @Administrator`. | `'.gitignore' is locked by @Administrator`. | ||||||
| 
 | 
 | ||||||
| ## Exclusive file locks |  | ||||||
| 
 |  | ||||||
| This process allows you to lock single files or file extensions and it is |  | ||||||
| done through the command line. It doesn't require GitLab paid subscriptions. |  | ||||||
| 
 |  | ||||||
| Git LFS is well known for tracking files to reduce the storage of |  | ||||||
| Git repositories, but it can also be used for [locking files](https://github.com/git-lfs/git-lfs/wiki/File-Locking). |  | ||||||
| This is the method used for Exclusive File Locks. |  | ||||||
| 
 |  | ||||||
| ### Install Git LFS |  | ||||||
| 
 |  | ||||||
| Before getting started, make sure you have [Git LFS installed](../../topics/git/lfs/index.md) in your computer. Open a terminal window and run: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git-lfs --version |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| If it doesn't recognize this command, you must install it. There are |  | ||||||
| several [installation methods](https://git-lfs.com/) that you can |  | ||||||
| choose according to your OS. To install it with Homebrew: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| brew install git-lfs |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| Once installed, **open your local repository in a terminal window** and |  | ||||||
| install Git LFS in your repository. If you're sure that LFS is already installed, |  | ||||||
| you can skip this step. If you're unsure, re-installing it does no harm: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git lfs install |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| For more information, see [Git Large File Storage (LFS)](../../topics/git/lfs/index.md). |  | ||||||
| 
 |  | ||||||
| ### Configure Exclusive File Locks |  | ||||||
| 
 |  | ||||||
| You need the Maintainer role |  | ||||||
| Exclusive File Locks for your project through the command line. |  | ||||||
| 
 |  | ||||||
| The first thing to do before using File Locking is to tell Git LFS which |  | ||||||
| kind of files are lockable. The following command stores PNG files |  | ||||||
| in LFS and flag them as lockable: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git lfs track "*.png" --lockable |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| After executing the above command a file named `.gitattributes` is |  | ||||||
| created or updated with the following content: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| *.png filter=lfs diff=lfs merge=lfs -text lockable |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| You can also register a file type as lockable without using LFS (to be able, for example, |  | ||||||
| to lock/unlock a file you need in a remote server that |  | ||||||
| implements the LFS File Locking API). To do that you can edit the |  | ||||||
| `.gitattributes` file manually: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| *.pdf lockable |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| The `.gitattributes` file is key to the process and **must** |  | ||||||
| be pushed to the remote repository for the changes to take effect. |  | ||||||
| 
 |  | ||||||
| After a file type has been registered as lockable, Git LFS makes |  | ||||||
| them read-only on the file system automatically. This means you |  | ||||||
| must **lock the file** before [editing it](#edit-lockable-files). |  | ||||||
| 
 |  | ||||||
| ### Lock files |  | ||||||
| 
 |  | ||||||
| By locking a file, you verify that no one else is editing it, and |  | ||||||
| prevent anyone else from editing the file until you're done. On the other |  | ||||||
| hand, when you unlock a file, you communicate that you've finished editing |  | ||||||
| and allow other people to edit it. |  | ||||||
| 
 |  | ||||||
| To lock or unlock a file with Exclusive File Locking, open a terminal window |  | ||||||
| in your repository directory and run the commands as described below. |  | ||||||
| 
 |  | ||||||
| To **lock** a file: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git lfs lock path/to/file.png |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| To **unlock** a file: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git lfs unlock path/to/file.png |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| You can also unlock by file ID (given by LFS when you [view locked files](#view-exclusively-locked-files)): |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git lfs unlock --id=123 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| If for some reason you need to unlock a file that was not locked by |  | ||||||
| yourself, you can use the `--force` flag as long as you have **Maintainer** |  | ||||||
| permissions to the project: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git lfs unlock --id=123 --force |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| You can push files to GitLab whether they're locked or unlocked. |  | ||||||
| 
 |  | ||||||
| NOTE: |  | ||||||
| Although multi-branch file locks can be created and managed through the Git LFS |  | ||||||
| command-line interface, file locks can be created for any file. |  | ||||||
| 
 |  | ||||||
| ### View exclusively-locked files |  | ||||||
| 
 |  | ||||||
| To list all the files locked with LFS locally, open a terminal window in your |  | ||||||
| repository and run: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| git lfs locks |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| The output lists the locked files followed by the user who locked each of them |  | ||||||
| and the files' IDs. |  | ||||||
| 
 |  | ||||||
| On the repository file tree, GitLab displays an LFS badge for files |  | ||||||
| tracked by Git LFS plus a padlock icon on exclusively-locked files: |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
| 
 |  | ||||||
| You can also [view and remove existing locks](#view-and-remove-existing-locks) from the GitLab UI. |  | ||||||
| 
 |  | ||||||
| NOTE: |  | ||||||
| When you rename an exclusively-locked file, the lock is lost. You must |  | ||||||
| lock it again to keep it locked. |  | ||||||
| 
 |  | ||||||
| ### Edit lockable files |  | ||||||
| 
 |  | ||||||
| After the file is [configured as lockable](#configure-exclusive-file-locks), it is set to read-only. |  | ||||||
| Therefore, you need to lock it before editing it. |  | ||||||
| 
 |  | ||||||
| Suggested workflow for shared projects: |  | ||||||
| 
 |  | ||||||
| 1. Lock the file. |  | ||||||
| 1. Edit the file. |  | ||||||
| 1. Commit your changes. |  | ||||||
| 1. Push to the repository. |  | ||||||
| 1. Get your changes reviewed, approved, and merged. |  | ||||||
| 1. Unlock the file. |  | ||||||
| 
 |  | ||||||
| ## Default branch file and directory locks | ## Default branch file and directory locks | ||||||
| 
 | 
 | ||||||
| DETAILS: | DETAILS: | ||||||
|  | @ -234,3 +84,8 @@ This list shows all the files locked either through LFS or GitLab UI. | ||||||
| 
 | 
 | ||||||
| Locks can be removed by their author, or any user | Locks can be removed by their author, or any user | ||||||
| with at least the Maintainer role. | with at least the Maintainer role. | ||||||
|  | 
 | ||||||
|  | ## Related topics | ||||||
|  | 
 | ||||||
|  | - [File management with Git](../../topics/git/file_management.md) | ||||||
|  | - [File locks](../../topics/git/file_management.md#file-locks) | ||||||
|  |  | ||||||
|  | @ -55,35 +55,8 @@ To see earlier revisions of a specific line: | ||||||
| 1. Select **View blame prior to this change** (**{doc-versions}**) | 1. Select **View blame prior to this change** (**{doc-versions}**) | ||||||
|    until you've found the changes you're interested in viewing. |    until you've found the changes you're interested in viewing. | ||||||
| 
 | 
 | ||||||
| ## Associated `git` command |  | ||||||
| 
 |  | ||||||
| If you're running `git` from the command line, the equivalent command is |  | ||||||
| `git blame <filename>`. For example, if you want to find `blame` information |  | ||||||
| about a `README.md` file in the local directory: |  | ||||||
| 
 |  | ||||||
| 1. Run this command `git blame README.md`. |  | ||||||
| 1. If the line you want to see is not in the first page of results, press <kbd>Space</kbd> |  | ||||||
|    until you find the line you want. |  | ||||||
| 1. To exit out of the results, press <kbd>Q</kbd>. |  | ||||||
| 
 |  | ||||||
| The `git blame` output in the CLI looks like this: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| 58233c4f1054c (Dan Rhodes           2022-05-13 07:02:20 +0000  1) ## Contributor License Agreement |  | ||||||
| b87768f435185 (Jamie Hurewitz       2017-10-31 18:09:23 +0000  2) |  | ||||||
| 8e4c7f26317ff (Brett Walker         2023-10-20 17:53:25 +0000  3) Contributions to this repository are subject to the |  | ||||||
| 58233c4f1054c (Dan Rhodes           2022-05-13 07:02:20 +0000  4) |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| The output includes: |  | ||||||
| 
 |  | ||||||
| - The SHA of the commit. |  | ||||||
| - The name of the committer. |  | ||||||
| - The date and time in UTC format. |  | ||||||
| - The line number. |  | ||||||
| - The contents of the line. |  | ||||||
| 
 |  | ||||||
| ## Related topics | ## Related topics | ||||||
| 
 | 
 | ||||||
| - [Git file blame REST API](../../../../api/repository_files.md#get-file-blame-from-repository) | - [Git file blame REST API](../../../../api/repository_files.md#get-file-blame-from-repository) | ||||||
| - [Common Git commands](../../../../topics/git/commands.md) | - [Common Git commands](../../../../topics/git/commands.md) | ||||||
|  | - [File management with Git](../../../../topics/git/file_management.md) | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ GitLab retrieves the user name and email information from the | ||||||
| [Git configuration](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) | [Git configuration](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) | ||||||
| of the contributor when the user creates a commit. | of the contributor when the user creates a commit. | ||||||
| 
 | 
 | ||||||
| ## View a file's Git history in the UI | ## View a file's Git history | ||||||
| 
 | 
 | ||||||
| To see a file's Git history in the UI: | To see a file's Git history in the UI: | ||||||
| 
 | 
 | ||||||
|  | @ -40,34 +40,11 @@ To see a file's Git history in the UI: | ||||||
| 1. Go to your desired file in the repository. | 1. Go to your desired file in the repository. | ||||||
| 1. In the upper-right corner, select **History**. | 1. In the upper-right corner, select **History**. | ||||||
| 
 | 
 | ||||||
| ## In the CLI |  | ||||||
| 
 |  | ||||||
| To see the history of a file from the command line, use the `git log <filename>` command. |  | ||||||
| For example, to see `history` information about the `CONTRIBUTING.md` file in the root |  | ||||||
| of the `gitlab` repository, run this command: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| $ git log CONTRIBUTING.md |  | ||||||
| 
 |  | ||||||
| commit b350bf041666964c27834885e4590d90ad0bfe90 |  | ||||||
| Author: Nick Malcolm <nmalcolm@gitlab.com> |  | ||||||
| Date:   Fri Dec 8 13:43:07 2023 +1300 |  | ||||||
| 
 |  | ||||||
|     Update security contact and vulnerability disclosure info |  | ||||||
| 
 |  | ||||||
| commit 8e4c7f26317ff4689610bf9d031b4931aef54086 |  | ||||||
| Author: Brett Walker <bwalker@gitlab.com> |  | ||||||
| Date:   Fri Oct 20 17:53:25 2023 +0000 |  | ||||||
| 
 |  | ||||||
|     Fix link to Code of Conduct |  | ||||||
| 
 |  | ||||||
|     and condense some of the verbiage |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## Related topics | ## Related topics | ||||||
| 
 | 
 | ||||||
| - [Git blame](git_blame.md) for line-by-line information about a file | - [Git blame](git_blame.md) for line-by-line information about a file | ||||||
| - [Common Git commands](../../../../topics/git/commands.md) | - [Common Git commands](../../../../topics/git/commands.md) | ||||||
|  | - [File management with Git](../../../../topics/git/file_management.md) | ||||||
| 
 | 
 | ||||||
| ## Troubleshooting | ## Troubleshooting | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -130,6 +130,7 @@ To change the default handling of a file or file type, create a | ||||||
| ## Related topics | ## Related topics | ||||||
| 
 | 
 | ||||||
| - [Repository files API](../../../../api/repository_files.md) | - [Repository files API](../../../../api/repository_files.md) | ||||||
|  | - [File management with Git](../../../../topics/git/file_management.md) | ||||||
| 
 | 
 | ||||||
| ## Troubleshooting | ## Troubleshooting | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,8 +30,9 @@ const extendConfigs = [ | ||||||
| // rewrite.
 | // rewrite.
 | ||||||
| let jhConfigs = []; | let jhConfigs = []; | ||||||
| if (existsSync(path.resolve(dirname, 'jh'))) { | if (existsSync(path.resolve(dirname, 'jh'))) { | ||||||
|   // eslint-disable-next-line import/no-unresolved, import/extensions
 |   const pathToJhConfig = path.resolve(dirname, 'jh/eslint.config.js') | ||||||
|   jhConfigs = (await import('jh/eslint.config.js')).default; |   // eslint-disable-next-line import/no-dynamic-require, no-unsanitized/method
 | ||||||
|  |   jhConfigs = (await import(pathToJhConfig)).default; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const jestConfig = { | const jestConfig = { | ||||||
|  |  | ||||||
|  | @ -11,6 +11,17 @@ module Keeps | ||||||
|         @rewriter = Parser::Source::TreeRewriter.new(@source.buffer) |         @rewriter = Parser::Source::TreeRewriter.new(@source.buffer) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       class << self | ||||||
|  |         # Define a node matcher method in the +RuboCop::AST::Node+, which all other node types inherits from. | ||||||
|  |         def def_node_matcher(method_name, pattern) | ||||||
|  |           RuboCop::AST::NodePattern.new(pattern).def_node_matcher(RuboCop::AST::Node, method_name) | ||||||
|  | 
 | ||||||
|  |           define_method method_name do | ||||||
|  |             source.ast.public_send(method_name) # rubocop:disable GitlabSecurity/PublicSend -- it's used to evaluate the node matcher at instance level | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       def replace_method_content(method_name, content, strip_comments_from_file: false) |       def replace_method_content(method_name, content, strip_comments_from_file: false) | ||||||
|         method = source.ast.each_node(:class).first.each_node(:def).find do |child| |         method = source.ast.each_node(:class).first.each_node(:def).find do |child| | ||||||
|           child.method_name == method_name.to_sym |           child.method_name == method_name.to_sym | ||||||
|  | @ -25,6 +36,12 @@ module Keeps | ||||||
|         process |         process | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       def replace_as_string(node, content) | ||||||
|  |         rewriter.replace(node.loc.expression, content) | ||||||
|  | 
 | ||||||
|  |         process | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       private |       private | ||||||
| 
 | 
 | ||||||
|       attr_reader :file, :source, :rewriter, :corrector |       attr_reader :file, :source, :rewriter, :corrector | ||||||
|  | @ -52,7 +69,7 @@ module Keeps | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def process |       def process | ||||||
|         @process ||= rewriter.process.lstrip.gsub(/\n{3,}/, "\n\n") |         rewriter.process.lstrip.gsub(/\n{3,}/, "\n\n") | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,135 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require_relative '../rubocop/cop_todo' | ||||||
|  | 
 | ||||||
|  | module Keeps | ||||||
|  |   # This is an implementation of ::Gitlab::Housekeeper::Keep. | ||||||
|  |   # This changes workers which have `data_consistency: :always` to `:sticky`. | ||||||
|  |   # | ||||||
|  |   # You can run it individually with: | ||||||
|  |   # | ||||||
|  |   # ``` | ||||||
|  |   # bundle exec gitlab-housekeeper -d -k Keeps::UpdateWorkersDataConsistency | ||||||
|  |   # ``` | ||||||
|  |   class UpdateWorkersDataConsistency < ::Gitlab::Housekeeper::Keep | ||||||
|  |     WORKER_REGEX = %r{app/workers/(.+).rb} | ||||||
|  |     WORKERS_DATA_CONSISTENCY_PATH = '.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml' | ||||||
|  |     FALLBACK_FEATURE_CATEGORY = 'database' | ||||||
|  |     LIMIT_TO = 5 | ||||||
|  | 
 | ||||||
|  |     def initialize(...) | ||||||
|  |       ::Keeps::Helpers::FileHelper.def_node_matcher :data_consistency_node, <<~PATTERN | ||||||
|  |           `(send nil? :data_consistency $(sym _) ...) | ||||||
|  |       PATTERN | ||||||
|  | 
 | ||||||
|  |       super | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def each_change | ||||||
|  |       workers_by_feature_category.deep_dup.each do |feature_category, workers| | ||||||
|  |         remove_workers_from_list(workers.pluck(:path)) # rubocop:disable CodeReuse/ActiveRecord -- small dataset | ||||||
|  | 
 | ||||||
|  |         workers.each do |worker| | ||||||
|  |           file_helper = ::Keeps::Helpers::FileHelper.new(worker[:path]) | ||||||
|  |           node = file_helper.data_consistency_node | ||||||
|  |           File.write(worker[:path], file_helper.replace_as_string(node, ':sticky')) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         yield(build_change(feature_category, workers)) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     private | ||||||
|  | 
 | ||||||
|  |     def workers_by_feature_category | ||||||
|  |       worker_paths.each_with_object(Hash.new { |h, k| h[k] = [] }) do |worker_path, group| | ||||||
|  |         next unless File.read(worker_path, mode: 'rb').include?('data_consistency :always') | ||||||
|  | 
 | ||||||
|  |         worker_name = worker_path.match(WORKER_REGEX)[1].camelize | ||||||
|  | 
 | ||||||
|  |         feature_category = worker_feature_category(worker_name) | ||||||
|  | 
 | ||||||
|  |         next if group[feature_category].size >= LIMIT_TO | ||||||
|  | 
 | ||||||
|  |         group[feature_category] << { path: worker_path, name: worker_name } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def build_change(feature_category, workers) | ||||||
|  |       change = ::Gitlab::Housekeeper::Change.new | ||||||
|  |       change.title = "Change data consistency for workers maintained by #{feature_category}".truncate(70, omission: '') | ||||||
|  |       change.identifiers = workers.map { |worker| worker[:name].to_s }.prepend(feature_category) | ||||||
|  |       change.labels = labels(feature_category) | ||||||
|  |       change.reviewers = pick_reviewers(feature_category, change.identifiers) | ||||||
|  |       change.changed_files = workers.pluck(:path).prepend(WORKERS_DATA_CONSISTENCY_PATH) # rubocop:disable CodeReuse/ActiveRecord -- small dataset | ||||||
|  | 
 | ||||||
|  |       change.description = <<~MARKDOWN.chomp | ||||||
|  |         ## What does this MR | ||||||
|  | 
 | ||||||
|  |         It updates workers data consistency from `:always` to `:sticky` for workers maintained by `#{feature_category}`, | ||||||
|  |         as a way to reduce database reads on the primary DB. Check https://gitlab.com/gitlab-org/gitlab/-/issues/462611. | ||||||
|  | 
 | ||||||
|  |         To reduce resource saturation on the primary node, all workers should be changed to `sticky`, at minimum. | ||||||
|  | 
 | ||||||
|  |         Since jobs are now enqueued along with the current database LSN, the replica (for `:sticky` or `:delayed`) | ||||||
|  |         is guaranteed to be caught up to that point, or the job will be retried, or use the primary. Consider updating | ||||||
|  |         the worker(s) to `delayed`, if it's applicable. | ||||||
|  | 
 | ||||||
|  |         You can read more about the Sidekiq Workers `data_consistency` in | ||||||
|  |         https://docs.gitlab.com/ee/development/sidekiq/worker_attributes.html#job-data-consistency-strategies. | ||||||
|  | 
 | ||||||
|  |         You can use this [dashboard](https://log.gprd.gitlab.net/app/r/s/iyIUV) to monitor the worker query activity on | ||||||
|  |         primary vs. replicas. | ||||||
|  | 
 | ||||||
|  |         Currently, the `gitlab-housekeeper` is not always capable of updating all references, so you must check the diff | ||||||
|  |         and pipeline failures to confirm if there are any issues. | ||||||
|  |       MARKDOWN | ||||||
|  | 
 | ||||||
|  |       change | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def labels(feature_category) | ||||||
|  |       group_labels = groups_helper.labels_for_feature_category(feature_category) | ||||||
|  | 
 | ||||||
|  |       group_labels + %w[maintenance::scalability type::maintenance severity::3 priority::1] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def pick_reviewers(feature_category, identifiers) | ||||||
|  |       groups_helper.pick_reviewer_for_feature_category( | ||||||
|  |         feature_category, | ||||||
|  |         identifiers, | ||||||
|  |         fallback_feature_category: 'database' | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def worker_feature_category(worker_name) | ||||||
|  |       feature_category = workers_meta.find { |entry| entry[:worker_name].to_s == worker_name.to_s } || {} | ||||||
|  | 
 | ||||||
|  |       feature_category.fetch(:feature_category, FALLBACK_FEATURE_CATEGORY).to_s | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def workers_meta | ||||||
|  |       @workers_meta ||= Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS.flat_map do |yaml_file| | ||||||
|  |         YAML.safe_load_file(yaml_file, permitted_classes: [Symbol]) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def remove_workers_from_list(paths_to_remove) | ||||||
|  |       todo_helper = RuboCop::CopTodo.new('SidekiqLoadBalancing/WorkerDataConsistency') | ||||||
|  |       todo_helper.add_files(worker_paths - paths_to_remove) | ||||||
|  | 
 | ||||||
|  |       File.write(WORKERS_DATA_CONSISTENCY_PATH, todo_helper.to_yaml) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def worker_paths | ||||||
|  |       @worker_paths ||= YAML.safe_load_file(WORKERS_DATA_CONSISTENCY_PATH).dig( | ||||||
|  |         'SidekiqLoadBalancing/WorkerDataConsistency', | ||||||
|  |         'Exclude' | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def groups_helper | ||||||
|  |       @groups_helper ||= ::Keeps::Helpers::Groups.new | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -6,7 +6,7 @@ module API | ||||||
| 
 | 
 | ||||||
|     before { authenticate! } |     before { authenticate! } | ||||||
| 
 | 
 | ||||||
|     feature_category :team_planning |     feature_category :notifications | ||||||
|     urgency :low |     urgency :low | ||||||
| 
 | 
 | ||||||
|     ISSUABLE_TYPES = { |     ISSUABLE_TYPES = { | ||||||
|  |  | ||||||
|  | @ -23,6 +23,12 @@ module Gitlab | ||||||
|             end |             end | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|  |           def variables_hash_expanded | ||||||
|  |             strong_memoize(:variables_hash_expanded) do | ||||||
|  |               variables.sort_and_expand_all.to_hash | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|           def project |           def project | ||||||
|             pipeline.project |             pipeline.project | ||||||
|           end |           end | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ module Gitlab | ||||||
|           return true unless modified_paths |           return true unless modified_paths | ||||||
|           return false if modified_paths.empty? |           return false if modified_paths.empty? | ||||||
| 
 | 
 | ||||||
|           expanded_globs = expand_globs(context).uniq |           expanded_globs = expand_globs(context, pipeline).uniq | ||||||
|           return false if expanded_globs.empty? |           return false if expanded_globs.empty? | ||||||
| 
 | 
 | ||||||
|           cache_key = [ |           cache_key = [ | ||||||
|  | @ -43,11 +43,17 @@ module Gitlab | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def expand_globs(context) |         def expand_globs(context, pipeline) | ||||||
|           return paths unless context |           return paths unless context | ||||||
| 
 | 
 | ||||||
|           paths.map do |glob| |           if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project) | ||||||
|             ExpandVariables.expand_existing(glob, -> { context.variables_hash }) |             paths.map do |glob| | ||||||
|  |               expand_value_nested(glob, context) | ||||||
|  |             end | ||||||
|  |           else | ||||||
|  |             paths.map do |glob| | ||||||
|  |               expand_value(glob, context) | ||||||
|  |             end | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -70,12 +76,25 @@ module Gitlab | ||||||
|         def find_compare_to_sha(pipeline, context) |         def find_compare_to_sha(pipeline, context) | ||||||
|           return unless @globs.include?(:compare_to) |           return unless @globs.include?(:compare_to) | ||||||
| 
 | 
 | ||||||
|           compare_to = ExpandVariables.expand(@globs[:compare_to], -> { context.variables_hash }) |           compare_to = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project) | ||||||
|  |                          expand_value_nested(@globs[:compare_to], context) | ||||||
|  |                        else | ||||||
|  |                          expand_value(@globs[:compare_to], context) | ||||||
|  |                        end | ||||||
|  | 
 | ||||||
|           commit = pipeline.project.commit(compare_to) |           commit = pipeline.project.commit(compare_to) | ||||||
|           raise Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' unless commit |           raise Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' unless commit | ||||||
| 
 | 
 | ||||||
|           commit.sha |           commit.sha | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         def expand_value(value, context) | ||||||
|  |           ExpandVariables.expand_existing(value, -> { context.variables_hash }) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         def expand_value_nested(value, context) | ||||||
|  |           ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded }) | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -18,13 +18,13 @@ module Gitlab | ||||||
|           @ref = clause[:ref] |           @ref = clause[:ref] | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def satisfied_by?(_pipeline, context) |         def satisfied_by?(pipeline, context) | ||||||
|           # Return early to avoid redundant Gitaly calls |           # Return early to avoid redundant Gitaly calls | ||||||
|           return false unless @globs.any? |           return false unless @globs.any? | ||||||
| 
 | 
 | ||||||
|           context = change_context(context) if @project_path |           context = change_context(context, pipeline) if @project_path | ||||||
| 
 | 
 | ||||||
|           expanded_globs = expand_globs(context) |           expanded_globs = expand_globs(context, pipeline) | ||||||
|           top_level_only = expanded_globs.all?(&method(:top_level_glob?)) |           top_level_only = expanded_globs.all?(&method(:top_level_glob?)) | ||||||
| 
 | 
 | ||||||
|           paths = worktree_paths(context, top_level_only) |           paths = worktree_paths(context, top_level_only) | ||||||
|  | @ -42,9 +42,15 @@ module Gitlab | ||||||
|           grouped.values_at(:exact, :extension, :pattern).map { |globs| Array(globs) } |           grouped.values_at(:exact, :extension, :pattern).map { |globs| Array(globs) } | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def expand_globs(context) |         def expand_globs(context, pipeline) | ||||||
|           @globs.map do |glob| |           if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline&.project) | ||||||
|             expand_value(glob, context) |             @globs.map do |glob| | ||||||
|  |               expand_value_nested(glob, context) | ||||||
|  |             end | ||||||
|  |           else | ||||||
|  |             @globs.map do |glob| | ||||||
|  |               expand_value(glob, context) | ||||||
|  |             end | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -121,10 +127,10 @@ module Gitlab | ||||||
|           glob.delete_prefix(WILDCARD_NESTED_PATTERN) |           glob.delete_prefix(WILDCARD_NESTED_PATTERN) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def change_context(old_context) |         def change_context(old_context, pipeline) | ||||||
|           user = find_context_user(old_context) |           user = find_context_user(old_context) | ||||||
|           new_project = find_context_project(user, old_context) |           new_project = find_context_project(user, old_context, pipeline) | ||||||
|           new_sha = find_context_sha(new_project, old_context) |           new_sha = find_context_sha(new_project, old_context, pipeline) | ||||||
| 
 | 
 | ||||||
|           Gitlab::Ci::Config::External::Context.new( |           Gitlab::Ci::Config::External::Context.new( | ||||||
|             project: new_project, |             project: new_project, | ||||||
|  | @ -138,8 +144,13 @@ module Gitlab | ||||||
|           context.is_a?(Gitlab::Ci::Config::External::Context) ? context.user : context.pipeline.user |           context.is_a?(Gitlab::Ci::Config::External::Context) ? context.user : context.pipeline.user | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def find_context_project(user, context) |         def find_context_project(user, context, pipeline) | ||||||
|           full_path = expand_value(@project_path, context) |           full_path = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project) | ||||||
|  |                         expand_value_nested(@project_path, context) | ||||||
|  |                       else | ||||||
|  |                         expand_value(@project_path, context) | ||||||
|  |                       end | ||||||
|  | 
 | ||||||
|           project = Project.find_by_full_path(full_path) |           project = Project.find_by_full_path(full_path) | ||||||
| 
 | 
 | ||||||
|           unless project |           unless project | ||||||
|  | @ -156,10 +167,16 @@ module Gitlab | ||||||
|           project |           project | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def find_context_sha(project, context) |         def find_context_sha(project, context, pipeline) | ||||||
|           return project.commit&.sha unless @ref |           return project.commit&.sha unless @ref | ||||||
| 
 | 
 | ||||||
|           ref = expand_value(@ref, context) |           ref = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, | ||||||
|  |             pipeline.project) | ||||||
|  |                   expand_value_nested(@ref, context) | ||||||
|  |                 else | ||||||
|  |                   expand_value(@ref, context) | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|           commit = project.commit(ref) |           commit = project.commit(ref) | ||||||
| 
 | 
 | ||||||
|           unless commit |           unless commit | ||||||
|  | @ -184,6 +201,10 @@ module Gitlab | ||||||
|         def expand_value(value, context) |         def expand_value(value, context) | ||||||
|           ExpandVariables.expand_existing(value, -> { context.variables_hash }) |           ExpandVariables.expand_existing(value, -> { context.variables_hash }) | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         def expand_value_nested(value, context) | ||||||
|  |           ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded }) | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -61,6 +61,12 @@ module Gitlab | ||||||
|             end |             end | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|  |           def variables_hash_expanded | ||||||
|  |             strong_memoize(:variables_hash_expanded) do | ||||||
|  |               variables.sort_and_expand_all.to_hash | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|           def mutate(attrs = {}) |           def mutate(attrs = {}) | ||||||
|             self.class.new(**attrs) do |ctx| |             self.class.new(**attrs) do |ctx| | ||||||
|               ctx.pipeline = pipeline |               ctx.pipeline = pipeline | ||||||
|  |  | ||||||
|  | @ -57513,6 +57513,12 @@ msgid_plural "Todos|Marked %d to-dos as done" | ||||||
| msgstr[0] "" | msgstr[0] "" | ||||||
| msgstr[1] "" | msgstr[1] "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Todos|Marked as done" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "Todos|Marked as undone" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Todos|Member access request" | msgid "Todos|Member access request" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -145,6 +145,7 @@ class ApplicationSettingsAnalysis | ||||||
|       help_page_support_url |       help_page_support_url | ||||||
|       help_page_text |       help_page_text | ||||||
|       home_page_url |       home_page_url | ||||||
|  |       identity_verification_settings | ||||||
|       import_sources |       import_sources | ||||||
|       importers |       importers | ||||||
|       invisible_captcha_enabled |       invisible_captcha_enabled | ||||||
|  | @ -217,6 +218,7 @@ class ApplicationSettingsAnalysis | ||||||
|       shared_runners_minutes |       shared_runners_minutes | ||||||
|       shared_runners_text |       shared_runners_text | ||||||
|       sidekiq_job_limiter_limit_bytes |       sidekiq_job_limiter_limit_bytes | ||||||
|  |       sign_in_restrictions | ||||||
|       signup_enabled |       signup_enabled | ||||||
|       silent_admin_exports_enabled |       silent_admin_exports_enabled | ||||||
|       slack_app_enabled |       slack_app_enabled | ||||||
|  |  | ||||||
|  | @ -612,47 +612,6 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe 'GET dag' do |  | ||||||
|     let(:pipeline) { create(:ci_pipeline, project: project) } |  | ||||||
| 
 |  | ||||||
|     it_behaves_like 'the show page', 'dag' |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe 'GET dag.json' do |  | ||||||
|     let(:pipeline) { create(:ci_pipeline, project: project) } |  | ||||||
|     let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) } |  | ||||||
|     let(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|     before do |  | ||||||
|       create_build(build_stage, 1, 'build') |  | ||||||
|       create_build(test_stage, 2, 'test', scheduling_type: 'dag').tap do |job| |  | ||||||
|         create(:ci_build_need, build: job, name: 'build') |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'returns the pipeline with DAG serialization' do |  | ||||||
|       get :dag, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json |  | ||||||
| 
 |  | ||||||
|       expect(response).to have_gitlab_http_status(:ok) |  | ||||||
| 
 |  | ||||||
|       expect(json_response.fetch('stages')).not_to be_empty |  | ||||||
| 
 |  | ||||||
|       build_stage = json_response['stages'].first |  | ||||||
|       expect(build_stage.fetch('name')).to eq 'build' |  | ||||||
|       expect(build_stage.fetch('groups').first.fetch('jobs')) |  | ||||||
|         .to eq [{ 'name' => 'build', 'scheduling_type' => 'stage' }] |  | ||||||
| 
 |  | ||||||
|       test_stage = json_response['stages'].last |  | ||||||
|       expect(test_stage.fetch('name')).to eq 'test' |  | ||||||
|       expect(test_stage.fetch('groups').first.fetch('jobs')) |  | ||||||
|         .to eq [{ 'name' => 'test', 'scheduling_type' => 'dag', 'needs' => ['build'] }] |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def create_build(stage, stage_idx, name, params = {}) |  | ||||||
|       create(:ci_build, pipeline: pipeline, ci_stage: stage, stage_idx: stage_idx, name: name, **params) |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe 'GET builds' do |   describe 'GET builds' do | ||||||
|     let(:pipeline) { create(:ci_pipeline, project: project) } |     let(:pipeline) { create(:ci_pipeline, project: project) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'Dashboard > User filters todos', :js, feature_category: :team_planning do | RSpec.describe 'Dashboard > User filters todos', :js, feature_category: :notifications do | ||||||
|   let(:user_1)    { create(:user, username: 'user_1', name: 'user_1') } |   let(:user_1)    { create(:user, username: 'user_1', name: 'user_1') } | ||||||
|   let(:user_2)    { create(:user, username: 'user_2', name: 'user_2') } |   let(:user_2)    { create(:user, username: 'user_2', name: 'user_2') } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'Dashboard > User sorts todos', feature_category: :team_planning do | RSpec.describe 'Dashboard > User sorts todos', feature_category: :notifications do | ||||||
|   let(:user)    { create(:user) } |   let(:user)    { create(:user) } | ||||||
|   let(:project) { create(:project) } |   let(:project) { create(:project) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do | RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do | ||||||
|   include DesignManagementTestHelpers |   include DesignManagementTestHelpers | ||||||
| 
 | 
 | ||||||
|   let_it_be(:user) { create(:user, username: 'john') } |   let_it_be(:user) { create(:user, username: 'john') } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'Manually create a todo item from issue', :js, feature_category: :team_planning do | RSpec.describe 'Manually create a todo item from issue', :js, feature_category: :notifications do | ||||||
|   let!(:project) { create(:project) } |   let!(:project) { create(:project) } | ||||||
|   let!(:issue)   { create(:issue, project: project) } |   let!(:issue)   { create(:issue, project: project) } | ||||||
|   let!(:user)    { create(:user) } |   let!(:user)    { create(:user) } | ||||||
|  |  | ||||||
|  | @ -168,15 +168,6 @@ RSpec.describe 'Project active tab', :js, feature_category: :groups_and_projects | ||||||
|         it_behaves_like 'page has active sub tab', _('Pipelines') |         it_behaves_like 'page has active sub tab', _('Pipelines') | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'Needs tab' do |  | ||||||
|         before do |  | ||||||
|           visit dag_project_pipeline_path(project, pipeline) |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it_behaves_like 'page has active tab', _('Build') |  | ||||||
|         it_behaves_like 'page has active sub tab', _('Pipelines') |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'Builds tab' do |       context 'Builds tab' do | ||||||
|         before do |         before do | ||||||
|           visit builds_project_pipeline_path(project, pipeline) |           visit builds_project_pipeline_path(project, pipeline) | ||||||
|  |  | ||||||
|  | @ -1220,25 +1220,6 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe 'GET /:project/-/pipelines/:id/dag' do |  | ||||||
|     include_context 'pipeline builds' |  | ||||||
| 
 |  | ||||||
|     let_it_be(:project) { create(:project, :repository) } |  | ||||||
| 
 |  | ||||||
|     let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } |  | ||||||
| 
 |  | ||||||
|     before do |  | ||||||
|       visit dag_project_pipeline_path(project, pipeline) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'page tabs' do |  | ||||||
|       it 'shows Pipeline and Jobs tabs with link' do |  | ||||||
|         expect(page).to have_link('Pipeline') |  | ||||||
|         expect(page).to have_link('Jobs') |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   context 'when user sees pipeline flags in a pipeline detail page' do |   context 'when user sees pipeline flags in a pipeline detail page' do | ||||||
|     let_it_be(:project) { create(:project, :repository) } |     let_it_be(:project) { create(:project, :repository) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe TodosFinder, feature_category: :team_planning do | RSpec.describe TodosFinder, feature_category: :notifications do | ||||||
|   describe '#execute' do |   describe '#execute' do | ||||||
|     let_it_be(:user) { create(:user) } |     let_it_be(:user) { create(:user) } | ||||||
|     let_it_be(:group) { create(:group) } |     let_it_be(:group) { create(:group) } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | import { shallowMount } from '@vue/test-utils'; | ||||||
|  | import { GlButton } from '@gitlab/ui'; | ||||||
|  | import TodoItemActions from '~/todos/components/todo_item_actions.vue'; | ||||||
|  | import { TODO_STATE_DONE, TODO_STATE_PENDING } from '~/todos/constants'; | ||||||
|  | 
 | ||||||
|  | describe('TodoItemActions', () => { | ||||||
|  |   let wrapper; | ||||||
|  |   const mockTodo = { | ||||||
|  |     id: 'gid://gitlab/Todo/1', | ||||||
|  |     state: TODO_STATE_PENDING, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const createComponent = (props = {}) => { | ||||||
|  |     wrapper = shallowMount(TodoItemActions, { | ||||||
|  |       propsData: { | ||||||
|  |         todo: mockTodo, | ||||||
|  |         ...props, | ||||||
|  |       }, | ||||||
|  |       provide: { | ||||||
|  |         currentTab: 0, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   it('sets correct icon for pending todo action button', () => { | ||||||
|  |     createComponent(); | ||||||
|  |     expect(wrapper.findComponent(GlButton).props('icon')).toBe('check'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('sets correct icon for done todo action button', () => { | ||||||
|  |     createComponent({ todo: { ...mockTodo, state: TODO_STATE_DONE } }); | ||||||
|  |     expect(wrapper.findComponent(GlButton).props('icon')).toBe('redo'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('sets correct aria-label for pending todo', () => { | ||||||
|  |     createComponent(); | ||||||
|  |     expect(wrapper.findComponent(GlButton).attributes('aria-label')).toBe('Mark as done'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('sets correct aria-label for done todo', () => { | ||||||
|  |     createComponent({ todo: { ...mockTodo, state: TODO_STATE_DONE } }); | ||||||
|  |     expect(wrapper.findComponent(GlButton).attributes('aria-label')).toBe('Undo'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('tooltipTitle', () => { | ||||||
|  |     it('returns null when isLoading is true', () => { | ||||||
|  |       createComponent(); | ||||||
|  |       // eslint-disable-next-line no-restricted-syntax
 | ||||||
|  |       wrapper.setData({ isLoading: true }); | ||||||
|  |       expect(wrapper.vm.tooltipTitle).toBeNull(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('returns "Mark as done" for pending todo', () => { | ||||||
|  |       createComponent(); | ||||||
|  |       expect(wrapper.vm.tooltipTitle).toBe('Mark as done'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('returns "Undo" for done todo', () => { | ||||||
|  |       createComponent({ todo: { ...mockTodo, state: TODO_STATE_DONE } }); | ||||||
|  |       expect(wrapper.vm.tooltipTitle).toBe('Undo'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| // write jest specs in this file, for the component in the todo_item_body.vue file
 |  | ||||||
| import { shallowMount } from '@vue/test-utils'; | import { shallowMount } from '@vue/test-utils'; | ||||||
| import { GlLink, GlAvatar, GlAvatarLink } from '@gitlab/ui'; | import { GlLink, GlAvatar, GlAvatarLink } from '@gitlab/ui'; | ||||||
| import TodoItemBody from '~/todos/components/todo_item_body.vue'; | import TodoItemBody from '~/todos/components/todo_item_body.vue'; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,70 @@ | ||||||
|  | import { shallowMount } from '@vue/test-utils'; | ||||||
|  | import TodoItem from '~/todos/components/todo_item.vue'; | ||||||
|  | import TodoItemTitle from '~/todos/components/todo_item_title.vue'; | ||||||
|  | import TodoItemBody from '~/todos/components/todo_item_body.vue'; | ||||||
|  | import TodoItemTimestamp from '~/todos/components/todo_item_timestamp.vue'; | ||||||
|  | import TodoItemActions from '~/todos/components/todo_item_actions.vue'; | ||||||
|  | import { TODO_STATE_DONE, TODO_STATE_PENDING } from '~/todos/constants'; | ||||||
|  | 
 | ||||||
|  | describe('TodoItem', () => { | ||||||
|  |   let wrapper; | ||||||
|  | 
 | ||||||
|  |   const createComponent = (props = {}) => { | ||||||
|  |     wrapper = shallowMount(TodoItem, { | ||||||
|  |       propsData: { | ||||||
|  |         currentUserId: '1', | ||||||
|  |         todo: { | ||||||
|  |           id: '1', | ||||||
|  |           state: TODO_STATE_PENDING, | ||||||
|  |           targetType: 'Issue', | ||||||
|  |           targetUrl: '/project/issue/1', | ||||||
|  |         }, | ||||||
|  |         ...props, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   it('renders the component', () => { | ||||||
|  |     createComponent(); | ||||||
|  |     expect(wrapper.exists()).toBe(true); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('renders TodoItemTitle component', () => { | ||||||
|  |     createComponent(); | ||||||
|  |     expect(wrapper.findComponent(TodoItemTitle).exists()).toBe(true); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('renders TodoItemBody component', () => { | ||||||
|  |     createComponent(); | ||||||
|  |     expect(wrapper.findComponent(TodoItemBody).exists()).toBe(true); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('renders TodoItemTimestamp component', () => { | ||||||
|  |     createComponent(); | ||||||
|  |     expect(wrapper.findComponent(TodoItemTimestamp).exists()).toBe(true); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('renders TodoItemActions component', () => { | ||||||
|  |     createComponent(); | ||||||
|  |     expect(wrapper.findComponent(TodoItemActions).exists()).toBe(true); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('computed properties', () => { | ||||||
|  |     it('isDone returns true when todo state is done', () => { | ||||||
|  |       createComponent({ todo: { state: TODO_STATE_DONE } }); | ||||||
|  |       expect(wrapper.vm.isDone).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('isPending returns true when todo state is pending', () => { | ||||||
|  |       createComponent({ todo: { state: TODO_STATE_PENDING } }); | ||||||
|  |       expect(wrapper.vm.isPending).toBe(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('emits change event when TodoItemActions emits change', async () => { | ||||||
|  |     createComponent(); | ||||||
|  |     const todoItemActions = wrapper.findComponent(TodoItemActions); | ||||||
|  |     await todoItemActions.vm.$emit('change', '1', true); | ||||||
|  |     expect(wrapper.emitted('change')).toEqual([['1', true]]); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Resolvers::TodosResolver, feature_category: :team_planning do | RSpec.describe Resolvers::TodosResolver, feature_category: :notifications do | ||||||
|   include GraphqlHelpers |   include GraphqlHelpers | ||||||
|   include DesignManagementTestHelpers |   include DesignManagementTestHelpers | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe GitlabSchema.types['Todo'], feature_category: :team_planning do | RSpec.describe GitlabSchema.types['Todo'], feature_category: :notifications do | ||||||
|   let_it_be(:current_user) { create(:user) } |   let_it_be(:current_user) { create(:user) } | ||||||
|   let_it_be(:author) { create(:user) } |   let_it_be(:author) { create(:user) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Types::TodoableInterface, feature_category: :team_planning do | RSpec.describe Types::TodoableInterface, feature_category: :notifications do | ||||||
|   include GraphqlHelpers |   include GraphqlHelpers | ||||||
| 
 | 
 | ||||||
|   it 'exposes the expected fields' do |   it 'exposes the expected fields' do | ||||||
|  |  | ||||||
|  | @ -137,4 +137,38 @@ RSpec.describe Keeps::Helpers::FileHelper, feature_category: :tooling do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '#replace_as_string' do | ||||||
|  |     let(:filename) { 'file.txt' } | ||||||
|  |     let(:new_milestone) { '17.5' } | ||||||
|  |     let(:parsed_file) do | ||||||
|  |       <<~RUBY | ||||||
|  |         # Migration type +class+ | ||||||
|  |         # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  |         # See https://docs.gitlab.com/ee/development/migration_style_guide.html | ||||||
|  |         # for more information on how to write migrations for GitLab. | ||||||
|  | 
 | ||||||
|  |         =begin | ||||||
|  |           This migration adds | ||||||
|  |           a new column to project | ||||||
|  |         =end | ||||||
|  |         class AddColToProjects < Gitlab::Database::Migration[2.2] | ||||||
|  |           milestone #{new_milestone} # Inline comment | ||||||
|  | 
 | ||||||
|  |           def change | ||||||
|  |             add_column :projects, :bool_col, :boolean, default: false, null: false # adds a new column | ||||||
|  |           end | ||||||
|  |         end# Another inline comment | ||||||
|  |       RUBY | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       described_class.def_node_matcher(:milestone_node, '`(send nil? :milestone $(str _) ...)') | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'parses the file as expected' do | ||||||
|  |       expect(helper.replace_as_string(helper.milestone_node, new_milestone)).to eq(parsed_file) | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -168,4 +168,12 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'variables collection' |     it_behaves_like 'variables collection' | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '#variables_hash_expanded' do | ||||||
|  |     subject { context.variables_hash_expanded } | ||||||
|  | 
 | ||||||
|  |     it { expect(context.variables_hash_expanded).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } | ||||||
|  | 
 | ||||||
|  |     it_behaves_like 'variables collection' | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -158,10 +158,35 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         before do |         before do | ||||||
|           allow(context).to receive(:variables_hash).and_return(variables_hash) |           allow(context).to receive(:variables_hash_expanded).and_return(variables_hash) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it { is_expected.to be_truthy } |         it { is_expected.to be_truthy } | ||||||
|  | 
 | ||||||
|  |         context 'when the variable is nested' do | ||||||
|  |           let(:variables_hash) do | ||||||
|  |             { 'HELM_DIR' => 'he$SUFFIX', 'SUFFIX' => 'lm' } | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           let(:variables_hash_expanded) do | ||||||
|  |             { 'HELM_DIR' => 'helm', 'SUFFIX' => 'lm' } | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           before do | ||||||
|  |             allow(context).to receive(:variables_hash_expanded).and_return(variables_hash_expanded) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it { is_expected.to be_truthy } | ||||||
|  | 
 | ||||||
|  |           context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |             before do | ||||||
|  |               stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |               allow(context).to receive(:variables_hash).and_return(variables_hash) | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             it { is_expected.to be_falsey } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when variable expansion does not match' do |       context 'when variable expansion does not match' do | ||||||
|  | @ -169,7 +194,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category | ||||||
|         let(:modified_paths) { ['path/with/$in/it/file.txt'] } |         let(:modified_paths) { ['path/with/$in/it/file.txt'] } | ||||||
| 
 | 
 | ||||||
|         before do |         before do | ||||||
|           allow(context).to receive(:variables_hash).and_return({}) |           allow(context).to receive(:variables_hash_expanded).and_return({}) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it { is_expected.to be_truthy } |         it { is_expected.to be_truthy } | ||||||
|  | @ -263,10 +288,54 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category | ||||||
|         let(:pipeline) { build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) } |         let(:pipeline) { build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) } | ||||||
| 
 | 
 | ||||||
|         before do |         before do | ||||||
|           allow(context).to receive(:variables_hash).and_return(variables_hash) |           allow(context).to receive(:variables_hash_expanded).and_return(variables_hash) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it { is_expected.to be_truthy } |         it { is_expected.to be_truthy } | ||||||
|  | 
 | ||||||
|  |         context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |           before do | ||||||
|  |             stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |             allow(context).to receive(:variables_hash).and_return(variables_hash) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it { is_expected.to be_truthy } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when the variable is nested' do | ||||||
|  |           let(:context) { instance_double(Gitlab::Ci::Build::Context::Base) } | ||||||
|  |           let(:variables_hash) do | ||||||
|  |             { 'FEATURE_BRANCH_NAME_PREFIX' => 'feature_', 'NESTED_REF_VAR' => '${FEATURE_BRANCH_NAME_PREFIX}1' } | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           let(:variables_hash_expanded) do | ||||||
|  |             { 'FEATURE_BRANCH_NAME_PREFIX' => 'feature_', 'NESTED_REF_VAR' => 'feature_1' } | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           let(:globs) { { paths: ['file2.txt'], compare_to: '$NESTED_REF_VAR' } } | ||||||
|  |           let(:pipeline) do | ||||||
|  |             build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           before do | ||||||
|  |             allow(context).to receive(:variables_hash_expanded).and_return(variables_hash_expanded) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it { is_expected.to be_truthy } | ||||||
|  | 
 | ||||||
|  |           context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |             before do | ||||||
|  |               stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |               allow(context).to receive(:variables_hash).and_return(variables_hash) | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             it 'raises ParseError' do | ||||||
|  |               expect { satisfied_by }.to raise_error( | ||||||
|  |                 ::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' | ||||||
|  |               ) | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
|   let_it_be(:user) { create(:user) } |   let_it_be(:user) { create(:user) } | ||||||
|   let_it_be(:project) { create(:project, :small_repo, files: { 'subdir/my_file.txt' => '' }) } |   let_it_be(:project) { create(:project, :small_repo, files: { 'subdir/my_file.txt' => '' }) } | ||||||
|   let_it_be(:other_project) { create(:project, :small_repo, files: { 'file.txt' => '' }) } |   let_it_be(:other_project) { create(:project, :small_repo, files: { 'file.txt' => '' }) } | ||||||
|  |   let(:pipeline) { instance_double(Ci::Pipeline, project: project, sha: 'sha', user: user) } | ||||||
| 
 | 
 | ||||||
|   let(:variables) do |   let(:variables) do | ||||||
|     Gitlab::Ci::Variables::Collection.new([ |     Gitlab::Ci::Variables::Collection.new([ | ||||||
|  | @ -13,6 +14,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
|       { key: 'FILE_TXT', value: 'file.txt' }, |       { key: 'FILE_TXT', value: 'file.txt' }, | ||||||
|       { key: 'FULL_PATH_VALID', value: 'subdir/my_file.txt' }, |       { key: 'FULL_PATH_VALID', value: 'subdir/my_file.txt' }, | ||||||
|       { key: 'FULL_PATH_INVALID', value: 'subdir/does_not_exist.txt' }, |       { key: 'FULL_PATH_INVALID', value: 'subdir/does_not_exist.txt' }, | ||||||
|  |       { key: 'NESTED_FULL_PATH_VALID', value: '$SUBDIR/my_file.txt' }, | ||||||
|       { key: 'NEW_BRANCH', value: 'new_branch' }, |       { key: 'NEW_BRANCH', value: 'new_branch' }, | ||||||
|       { key: 'MASKED_VAR', value: 'masked_value', masked: true } |       { key: 'MASKED_VAR', value: 'masked_value', masked: true } | ||||||
|     ]) |     ]) | ||||||
|  | @ -29,7 +31,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#satisfied_by?' do |   describe '#satisfied_by?' do | ||||||
|     subject(:satisfied_by?) { described_class.new(clause).satisfied_by?(nil, context) } |     subject(:satisfied_by?) { described_class.new(clause).satisfied_by?(pipeline, context) } | ||||||
| 
 | 
 | ||||||
|     before do |     before do | ||||||
|       allow(context).to receive(:variables).and_return(variables) |       allow(context).to receive(:variables).and_return(variables) | ||||||
|  | @ -65,6 +67,20 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
| 
 | 
 | ||||||
|           it { is_expected.to be_falsey } |           it { is_expected.to be_falsey } | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         context 'when the variable is nested and matches' do | ||||||
|  |           let(:globs) { ['$NESTED_FULL_PATH_VALID'] } | ||||||
|  | 
 | ||||||
|  |           it { is_expected.to be_truthy } | ||||||
|  | 
 | ||||||
|  |           context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |             before do | ||||||
|  |               stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             it { is_expected.to be_falsey } | ||||||
|  |           end | ||||||
|  |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when a file path has a variable' do |       context 'when a file path has a variable' do | ||||||
|  | @ -114,6 +130,14 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
|             let(:globs) { ['$FILE_TXT'] } |             let(:globs) { ['$FILE_TXT'] } | ||||||
| 
 | 
 | ||||||
|             it { is_expected.to be_truthy } |             it { is_expected.to be_truthy } | ||||||
|  | 
 | ||||||
|  |             context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |               before do | ||||||
|  |                 stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |               end | ||||||
|  | 
 | ||||||
|  |               it { is_expected.to be_truthy } | ||||||
|  |             end | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           context 'when the project path is invalid' do |           context 'when the project path is invalid' do | ||||||
|  | @ -135,6 +159,19 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
|                   "rules:exists:project `invalid/path/subdir` is not a valid project path" |                   "rules:exists:project `invalid/path/subdir` is not a valid project path" | ||||||
|                 ) |                 ) | ||||||
|               end |               end | ||||||
|  | 
 | ||||||
|  |               context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |                 before do | ||||||
|  |                   stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 it 'raises an error' do | ||||||
|  |                   expect { satisfied_by? }.to raise_error( | ||||||
|  |                     Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, | ||||||
|  |                     "rules:exists:project `invalid/path/subdir` is not a valid project path" | ||||||
|  |                   ) | ||||||
|  |                 end | ||||||
|  |               end | ||||||
|             end |             end | ||||||
| 
 | 
 | ||||||
|             context 'when the project path contains a masked variable' do |             context 'when the project path contains a masked variable' do | ||||||
|  | @ -165,6 +202,14 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
|               let(:ref) { '$NEW_BRANCH' } |               let(:ref) { '$NEW_BRANCH' } | ||||||
| 
 | 
 | ||||||
|               it { is_expected.to be_truthy } |               it { is_expected.to be_truthy } | ||||||
|  | 
 | ||||||
|  |               context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |                 before do | ||||||
|  |                   stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 it { is_expected.to be_truthy } | ||||||
|  |               end | ||||||
|             end |             end | ||||||
| 
 | 
 | ||||||
|             context 'when the ref is invalid' do |             context 'when the ref is invalid' do | ||||||
|  | @ -187,6 +232,20 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category: | ||||||
|                     "in project `#{other_project.full_path}`" |                     "in project `#{other_project.full_path}`" | ||||||
|                   ) |                   ) | ||||||
|                 end |                 end | ||||||
|  | 
 | ||||||
|  |                 context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |                   before do | ||||||
|  |                     stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |                   end | ||||||
|  | 
 | ||||||
|  |                   it 'raises an error' do | ||||||
|  |                     expect { satisfied_by? }.to raise_error( | ||||||
|  |                       Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, | ||||||
|  |                       "rules:exists:ref `invalid/ref/new_branch` is not a valid ref " \ | ||||||
|  |                         "in project `#{other_project.full_path}`" | ||||||
|  |                     ) | ||||||
|  |                   end | ||||||
|  |                 end | ||||||
|               end |               end | ||||||
| 
 | 
 | ||||||
|               context 'when the ref contains a masked variable' do |               context 'when the ref contains a masked variable' do | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ require 'spec_helper' | ||||||
| RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_composition do | RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_composition do | ||||||
|   let(:context) { double(variables_hash: {}) } |   let(:context) { double(variables_hash: {}) } | ||||||
|   let(:rule_hashes) {} |   let(:rule_hashes) {} | ||||||
|   let(:pipeline) { instance_double(Ci::Pipeline, project_id: project.id, sha: 'sha') } |   let(:pipeline) { instance_double(Ci::Pipeline, project: project, project_id: project.id, sha: 'sha') } | ||||||
|   let_it_be(:project) { create(:project, :custom_repo, files: { 'file.txt' => 'file' }) } |   let_it_be(:project) { create(:project, :custom_repo, files: { 'file.txt' => 'file' }) } | ||||||
| 
 | 
 | ||||||
|   subject(:rules) { described_class.new(rule_hashes) } |   subject(:rules) { described_class.new(rule_hashes) } | ||||||
|  |  | ||||||
|  | @ -77,36 +77,6 @@ RSpec.describe Ci::PipelineCreation::Requests, :clean_gitlab_redis_shared_state, | ||||||
|         expect(described_class.pipeline_creating_for_merge_request?(merge_request)).to be_falsey |         expect(described_class.pipeline_creating_for_merge_request?(merge_request)).to be_falsey | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 |  | ||||||
|     context 'when delete_if_all_complete is true' do |  | ||||||
|       context 'when there are only finished creations for the merge request' do |  | ||||||
|         it 'deletes the MR pipeline creations key from Redis' do |  | ||||||
|           request_1 = described_class.start_for_merge_request(merge_request) |  | ||||||
|           request_2 = described_class.start_for_merge_request(merge_request) |  | ||||||
|           described_class.succeeded(request_1) |  | ||||||
|           described_class.failed(request_2) |  | ||||||
| 
 |  | ||||||
|           expect(described_class.pipeline_creating_for_merge_request?(merge_request, delete_if_all_complete: true)) |  | ||||||
|             .to be_falsey |  | ||||||
|           expect(read(request_1)).to be_nil |  | ||||||
|           expect(read(request_2)).to be_nil |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'when there are unfinished creations for the merge request' do |  | ||||||
|         it 'does not delete the MR pipeline creations key from Redis' do |  | ||||||
|           request_1 = described_class.start_for_merge_request(merge_request) |  | ||||||
|           request_2 = described_class.start_for_merge_request(merge_request) |  | ||||||
|           described_class.start_for_merge_request(merge_request) |  | ||||||
|           described_class.succeeded(request_1) |  | ||||||
| 
 |  | ||||||
|           expect(described_class.pipeline_creating_for_merge_request?(merge_request, delete_if_all_complete: false)) |  | ||||||
|             .to be_truthy |  | ||||||
|           expect(read(request_1)).to eq({ 'status' => 'succeeded' }) |  | ||||||
|           expect(read(request_2)).to eq({ 'status' => 'in_progress' }) |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '.hset' do |   describe '.hset' do | ||||||
|  |  | ||||||
|  | @ -277,6 +277,23 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   describe '#owner' do | ||||||
|  |     subject(:owner) { runner.owner } | ||||||
|  | 
 | ||||||
|  |     context 'when runner does not have creator_id' do | ||||||
|  |       let_it_be(:runner) { create(:ci_runner, :instance) } | ||||||
|  | 
 | ||||||
|  |       it { is_expected.to be_nil } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when runner has creator' do | ||||||
|  |       let_it_be(:creator) { create(:user) } | ||||||
|  |       let_it_be(:runner) { create(:ci_runner, :instance, creator: creator) } | ||||||
|  | 
 | ||||||
|  |       it { is_expected.to eq creator } | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe '.instance_type' do |   describe '.instance_type' do | ||||||
|     let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } |     let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } | ||||||
|     let!(:project_runner) { create(:ci_runner, :project, projects: [project]) } |     let!(:project_runner) { create(:ci_runner, :project, projects: [project]) } | ||||||
|  | @ -1125,8 +1142,8 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do | ||||||
|     let_it_be(:project1) { create(:project) } |     let_it_be(:project1) { create(:project) } | ||||||
|     let_it_be(:project2) { create(:project) } |     let_it_be(:project2) { create(:project) } | ||||||
| 
 | 
 | ||||||
|     describe '#owner_project' do |     describe '#owner' do | ||||||
|       subject(:owner_project) { project_runner.owner_project } |       subject(:owner) { project_runner.owner } | ||||||
| 
 | 
 | ||||||
|       context 'with project1 as first project associated with runner' do |       context 'with project1 as first project associated with runner' do | ||||||
|         let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1, project2]) } |         let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1, project2]) } | ||||||
|  | @ -1638,6 +1655,22 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     describe '#owner' do | ||||||
|  |       subject(:owner) { runner.owner } | ||||||
|  | 
 | ||||||
|  |       context 'with runner assigned to child_group' do | ||||||
|  |         let(:runner) { child_group_runner } | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to eq child_group } | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with runner assigned to top_level_group_runner' do | ||||||
|  |         let(:runner) { top_level_group_runner } | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to eq top_level_group } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#short_sha' do |   describe '#short_sha' do | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Todo, feature_category: :team_planning do | RSpec.describe Todo, feature_category: :notifications do | ||||||
|   let(:issue) { create(:issue) } |   let(:issue) { create(:issue) } | ||||||
| 
 | 
 | ||||||
|   describe 'relationships' do |   describe 'relationships' do | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe TodoPolicy, feature_category: :team_planning do | RSpec.describe TodoPolicy, feature_category: :notifications do | ||||||
|   using RSpec::Parameterized::TableSyntax |   using RSpec::Parameterized::TableSyntax | ||||||
| 
 | 
 | ||||||
|   let_it_be(:project) { create(:project) } |   let_it_be(:project) { create(:project) } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'A Todoable that implements the CurrentUserTodos interface', | RSpec.describe 'A Todoable that implements the CurrentUserTodos interface', | ||||||
|   feature_category: :team_planning do |   feature_category: :notifications do | ||||||
|   include GraphqlHelpers |   include GraphqlHelpers | ||||||
| 
 | 
 | ||||||
|   let_it_be(:current_user) { create(:user) } |   let_it_be(:current_user) { create(:user) } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe 'Todo Query', feature_category: :team_planning do | RSpec.describe 'Todo Query', feature_category: :notifications do | ||||||
|   include GraphqlHelpers |   include GraphqlHelpers | ||||||
| 
 | 
 | ||||||
|   let_it_be(:current_user) { nil } |   let_it_be(:current_user) { nil } | ||||||
|  |  | ||||||
|  | @ -1,66 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| 
 |  | ||||||
| RSpec.describe Ci::DagJobEntity do |  | ||||||
|   let_it_be(:request) { double(:request) } |  | ||||||
| 
 |  | ||||||
|   let(:job) { create(:ci_build, name: 'dag_job') } |  | ||||||
|   let(:entity) { described_class.new(job, request: request) } |  | ||||||
| 
 |  | ||||||
|   describe '#as_json' do |  | ||||||
|     subject { entity.as_json } |  | ||||||
| 
 |  | ||||||
|     RSpec.shared_examples "matches schema" do |  | ||||||
|       it "matches schema" do |  | ||||||
|         expect(subject.to_json).to match_schema('entities/dag_job') |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'contains the name' do |  | ||||||
|       expect(subject[:name]).to eq 'dag_job' |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it_behaves_like "matches schema" |  | ||||||
| 
 |  | ||||||
|     context 'when job is stage scheduled' do |  | ||||||
|       it 'contains the name scheduling_type' do |  | ||||||
|         expect(subject[:scheduling_type]).to eq 'stage' |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'does not expose needs' do |  | ||||||
|         expect(subject).not_to include(:needs) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it_behaves_like "matches schema" |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when job is dag scheduled' do |  | ||||||
|       let(:job) { create(:ci_build, scheduling_type: 'dag') } |  | ||||||
| 
 |  | ||||||
|       it 'contains the name scheduling_type' do |  | ||||||
|         expect(subject[:scheduling_type]).to eq 'dag' |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it_behaves_like "matches schema" |  | ||||||
| 
 |  | ||||||
|       context 'when job has needs' do |  | ||||||
|         let!(:need) { create(:ci_build_need, build: job, name: 'compile') } |  | ||||||
| 
 |  | ||||||
|         it 'exposes the array of needs' do |  | ||||||
|           expect(subject[:needs]).to eq ['compile'] |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it_behaves_like "matches schema" |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'when job has empty needs' do |  | ||||||
|         it 'exposes an empty array of needs' do |  | ||||||
|           expect(subject[:needs]).to eq [] |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it_behaves_like "matches schema" |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,66 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| 
 |  | ||||||
| RSpec.describe Ci::DagJobGroupEntity do |  | ||||||
|   let_it_be(:request) { double(:request) } |  | ||||||
|   let_it_be(:pipeline) { create(:ci_pipeline) } |  | ||||||
|   let_it_be(:stage) { create(:ci_stage, pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|   let(:group) { Ci::Group.new(pipeline.project, stage, name: 'test', jobs: jobs) } |  | ||||||
|   let(:entity) { described_class.new(group, request: request) } |  | ||||||
| 
 |  | ||||||
|   describe '#as_json' do |  | ||||||
|     subject { entity.as_json } |  | ||||||
| 
 |  | ||||||
|     context 'when group contains 1 job' do |  | ||||||
|       let(:job) { create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'test') } |  | ||||||
|       let(:jobs) { [job] } |  | ||||||
| 
 |  | ||||||
|       it 'exposes a name' do |  | ||||||
|         expect(subject.fetch(:name)).to eq 'test' |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'exposes the size' do |  | ||||||
|         expect(subject.fetch(:size)).to eq 1 |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'exposes the jobs' do |  | ||||||
|         exposed_jobs = subject.fetch(:jobs) |  | ||||||
| 
 |  | ||||||
|         expect(exposed_jobs.size).to eq 1 |  | ||||||
|         expect(exposed_jobs.first.fetch(:name)).to eq 'test' |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'matches schema' do |  | ||||||
|         expect(subject.to_json).to match_schema('entities/dag_job_group') |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when group contains multiple parallel jobs' do |  | ||||||
|       let(:job_1) { create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'test 1/2') } |  | ||||||
|       let(:job_2) { create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'test 2/2') } |  | ||||||
|       let(:jobs) { [job_1, job_2] } |  | ||||||
| 
 |  | ||||||
|       it 'exposes a name' do |  | ||||||
|         expect(subject.fetch(:name)).to eq 'test' |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'exposes the size' do |  | ||||||
|         expect(subject.fetch(:size)).to eq 2 |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'exposes the jobs' do |  | ||||||
|         exposed_jobs = subject.fetch(:jobs) |  | ||||||
| 
 |  | ||||||
|         expect(exposed_jobs.size).to eq 2 |  | ||||||
|         expect(exposed_jobs.first.fetch(:name)).to eq 'test 1/2' |  | ||||||
|         expect(exposed_jobs.last.fetch(:name)).to eq 'test 2/2' |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'matches schema' do |  | ||||||
|         expect(subject.to_json).to match_schema('entities/dag_job_group') |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,163 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| 
 |  | ||||||
| RSpec.describe Ci::DagPipelineEntity do |  | ||||||
|   let_it_be(:request) { double(:request) } |  | ||||||
| 
 |  | ||||||
|   let_it_be(:pipeline) { create(:ci_pipeline) } |  | ||||||
| 
 |  | ||||||
|   let(:entity) { described_class.new(pipeline, request: request) } |  | ||||||
| 
 |  | ||||||
|   describe '#as_json' do |  | ||||||
|     subject { entity.as_json } |  | ||||||
| 
 |  | ||||||
|     RSpec.shared_examples "matches schema" do |  | ||||||
|       it 'matches schema' do |  | ||||||
|         expect(subject.to_json).to match_schema('entities/dag_pipeline') |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when pipeline is empty' do |  | ||||||
|       it 'contains stages' do |  | ||||||
|         expect(subject).to include(:stages) |  | ||||||
| 
 |  | ||||||
|         expect(subject[:stages]).to be_empty |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it_behaves_like "matches schema" |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when pipeline has jobs' do |  | ||||||
|       let_it_be(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) } |  | ||||||
|       let_it_be(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) } |  | ||||||
|       let_it_be(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|       let!(:build_job)  { create(:ci_build, ci_stage: build_stage,  pipeline: pipeline) } |  | ||||||
|       let!(:test_job)   { create(:ci_build, ci_stage: test_stage,   pipeline: pipeline) } |  | ||||||
|       let!(:deploy_job) { create(:ci_build, ci_stage: deploy_stage, pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|       it 'contains 3 stages' do |  | ||||||
|         stages = subject[:stages] |  | ||||||
| 
 |  | ||||||
|         expect(stages.size).to eq 3 |  | ||||||
|         expect(stages.map { |s| s[:name] }).to contain_exactly('build', 'test', 'deploy') |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it_behaves_like "matches schema" |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when pipeline has parallel jobs, DAG needs and GenericCommitStatus' do |  | ||||||
|       let!(:stage_build)  { create(:ci_stage, name: 'build',  position: 1, pipeline: pipeline) } |  | ||||||
|       let!(:stage_test)   { create(:ci_stage, name: 'test',   position: 2, pipeline: pipeline) } |  | ||||||
|       let!(:stage_deploy) { create(:ci_stage, name: 'deploy', position: 3, pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|       let!(:job_build_1)   { create(:ci_build, name: 'build 1', ci_stage: stage_build, pipeline: pipeline) } |  | ||||||
|       let!(:job_build_2)   { create(:ci_build, name: 'build 2', ci_stage: stage_build, pipeline: pipeline) } |  | ||||||
|       let!(:commit_status) { create(:generic_commit_status, ci_stage: stage_build, pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|       let!(:job_rspec_1) { create(:ci_build, name: 'rspec 1/2', ci_stage: stage_test, pipeline: pipeline) } |  | ||||||
|       let!(:job_rspec_2) { create(:ci_build, name: 'rspec 2/2', ci_stage: stage_test, pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|       let!(:job_jest) do |  | ||||||
|         create(:ci_build, name: 'jest', ci_stage: stage_test, scheduling_type: 'dag', pipeline: pipeline) |  | ||||||
|           .tap do |job| |  | ||||||
|           create(:ci_build_need, name: 'build 1', build: job) |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       let!(:job_deploy_ruby) do |  | ||||||
|         create(:ci_build, name: 'deploy_ruby', ci_stage: stage_deploy, scheduling_type: 'dag', pipeline: pipeline) |  | ||||||
|           .tap do |job| |  | ||||||
|           create(:ci_build_need, name: 'rspec 1/2', build: job) |  | ||||||
|           create(:ci_build_need, name: 'rspec 2/2', build: job) |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       let!(:job_deploy_js) do |  | ||||||
|         create(:ci_build, name: 'deploy_js', ci_stage: stage_deploy, scheduling_type: 'dag', pipeline: pipeline) |  | ||||||
|           .tap do |job| |  | ||||||
|           create(:ci_build_need, name: 'jest', build: job) |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'performs the smallest number of queries', :request_store do |  | ||||||
|         log = ActiveRecord::QueryRecorder.new { subject } |  | ||||||
| 
 |  | ||||||
|         # stages, project, builds, build_needs |  | ||||||
|         expect(log.count).to eq 4 |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'contains all the data' do |  | ||||||
|         expected_result = { |  | ||||||
|           stages: [ |  | ||||||
|             { |  | ||||||
|               name: 'build', |  | ||||||
|               groups: [ |  | ||||||
|                 { |  | ||||||
|                   name: 'build 1', size: 1, jobs: [ |  | ||||||
|                     { name: 'build 1', scheduling_type: 'stage' } |  | ||||||
|                   ] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   name: 'build 2', size: 1, jobs: [ |  | ||||||
|                     { name: 'build 2', scheduling_type: 'stage' } |  | ||||||
|                   ] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   name: 'generic', size: 1, jobs: [ |  | ||||||
|                     { name: 'generic', scheduling_type: nil } |  | ||||||
|                   ] |  | ||||||
|                 } |  | ||||||
|               ] |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               name: 'test', |  | ||||||
|               groups: [ |  | ||||||
|                 { |  | ||||||
|                   name: 'jest', size: 1, jobs: [ |  | ||||||
|                     { name: 'jest', scheduling_type: 'dag', needs: ['build 1'] } |  | ||||||
|                   ] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   name: 'rspec', size: 2, jobs: [ |  | ||||||
|                     { name: 'rspec 1/2', scheduling_type: 'stage' }, |  | ||||||
|                     { name: 'rspec 2/2', scheduling_type: 'stage' } |  | ||||||
|                   ] |  | ||||||
|                 } |  | ||||||
|               ] |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               name: 'deploy', |  | ||||||
|               groups: [ |  | ||||||
|                 { |  | ||||||
|                   name: 'deploy_js', size: 1, jobs: [ |  | ||||||
|                     { name: 'deploy_js', scheduling_type: 'dag', needs: ['jest'] } |  | ||||||
|                   ] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   name: 'deploy_ruby', size: 1, jobs: [ |  | ||||||
|                     { name: 'deploy_ruby', scheduling_type: 'dag', needs: ['rspec 1/2', 'rspec 2/2'] } |  | ||||||
|                   ] |  | ||||||
|                 } |  | ||||||
|               ] |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         expect(subject.fetch(:stages)).not_to be_empty |  | ||||||
| 
 |  | ||||||
|         expect(subject.fetch(:stages)[0].fetch(:name)).to eq 'build' |  | ||||||
|         expect(subject.fetch(:stages)[0]).to eq expected_result.fetch(:stages)[0] |  | ||||||
| 
 |  | ||||||
|         expect(subject.fetch(:stages)[1].fetch(:name)).to eq 'test' |  | ||||||
|         expect(subject.fetch(:stages)[1]).to eq expected_result.fetch(:stages)[1] |  | ||||||
| 
 |  | ||||||
|         expect(subject.fetch(:stages)[2].fetch(:name)).to eq 'deploy' |  | ||||||
|         expect(subject.fetch(:stages)[2]).to eq expected_result.fetch(:stages)[2] |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it_behaves_like "matches schema" |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,21 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| 
 |  | ||||||
| RSpec.describe Ci::DagPipelineSerializer do |  | ||||||
|   describe '#represent' do |  | ||||||
|     subject { described_class.new.represent(pipeline) } |  | ||||||
| 
 |  | ||||||
|     let(:pipeline) { create(:ci_pipeline) } |  | ||||||
|     let!(:job) { create(:ci_build, pipeline: pipeline) } |  | ||||||
| 
 |  | ||||||
|     it 'includes stages' do |  | ||||||
|       expect(subject[:stages]).to be_present |  | ||||||
|       expect(subject[:stages].size).to eq 1 |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'matches schema' do |  | ||||||
|       expect(subject.to_json).to match_schema('entities/dag_pipeline') |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| 
 |  | ||||||
| RSpec.describe Ci::DagStageEntity do |  | ||||||
|   let_it_be(:pipeline) { create(:ci_pipeline) } |  | ||||||
|   let_it_be(:request) { double(:request) } |  | ||||||
| 
 |  | ||||||
|   let(:stage) { create(:ci_stage, pipeline: pipeline, name: 'test') } |  | ||||||
|   let(:entity) { described_class.new(stage, request: request) } |  | ||||||
| 
 |  | ||||||
|   let!(:job) { create(:ci_build, :success, pipeline: pipeline, stage_id: stage.id) } |  | ||||||
| 
 |  | ||||||
|   describe '#as_json' do |  | ||||||
|     subject { entity.as_json } |  | ||||||
| 
 |  | ||||||
|     it 'contains valid name' do |  | ||||||
|       expect(subject[:name]).to eq 'test' |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it 'contains the job groups' do |  | ||||||
|       expect(subject).to include :groups |  | ||||||
|       expect(subject[:groups]).not_to be_empty |  | ||||||
| 
 |  | ||||||
|       job_group = subject[:groups].first |  | ||||||
|       expect(job_group[:name]).to eq 'test' |  | ||||||
|       expect(job_group[:size]).to eq 1 |  | ||||||
|       expect(job_group[:jobs]).not_to be_empty |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     it "matches schema" do |  | ||||||
|       expect(subject.to_json).to match_schema('entities/dag_stage') |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -227,7 +227,7 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio | ||||||
|           script: echo Hello, World! |           script: echo Hello, World! | ||||||
|           rules: |           rules: | ||||||
|             - exists: |             - exists: | ||||||
|               - $VAR_NESTED # does not match because of https://gitlab.com/gitlab-org/gitlab/-/issues/411344 |               - $VAR_NESTED | ||||||
|         YAML |         YAML | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -247,7 +247,18 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio | ||||||
| 
 | 
 | ||||||
|         it 'creates all relevant jobs' do |         it 'creates all relevant jobs' do | ||||||
|           expect(pipeline).to be_persisted |           expect(pipeline).to be_persisted | ||||||
|           expect(build_names).to contain_exactly('job1', 'job2') |           expect(build_names).to contain_exactly('job1', 'job2', 'job4') | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |           before do | ||||||
|  |             stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it 'creates all relevant jobs' do | ||||||
|  |             expect(pipeline).to be_persisted | ||||||
|  |             expect(build_names).to contain_exactly('job1', 'job2') | ||||||
|  |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | @ -808,6 +819,10 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio | ||||||
|                 VALID_BRANCH_NAME: feature_1 |                 VALID_BRANCH_NAME: feature_1 | ||||||
|                 FEATURE_BRANCH_NAME_PREFIX: feature_ |                 FEATURE_BRANCH_NAME_PREFIX: feature_ | ||||||
|                 INVALID_BRANCH_NAME: invalid-branch |                 INVALID_BRANCH_NAME: invalid-branch | ||||||
|  |                 VALID_FILENAME: file2.txt | ||||||
|  |                 INVALID_FILENAME: file1.txt | ||||||
|  |                 VALID_BASENAME: file2 | ||||||
|  |                 VALID_NESTED_VARIABLE: ${VALID_BASENAME}.txt | ||||||
|               job1: |               job1: | ||||||
|                 script: exit 0 |                 script: exit 0 | ||||||
|                 rules: |                 rules: | ||||||
|  | @ -857,6 +872,52 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio | ||||||
|                 ) |                 ) | ||||||
|               end |               end | ||||||
|             end |             end | ||||||
|  | 
 | ||||||
|  |             context 'when paths is defined by a variable' do | ||||||
|  |               let(:compare_to) { '${VALID_BRANCH_NAME}' } | ||||||
|  | 
 | ||||||
|  |               context 'when the variable does not exist' do | ||||||
|  |                 let(:changed_file) { '$NON_EXISTENT_VAR' } | ||||||
|  | 
 | ||||||
|  |                 it 'does not create job1' do | ||||||
|  |                   expect(build_names).to contain_exactly('job2') | ||||||
|  |                 end | ||||||
|  |               end | ||||||
|  | 
 | ||||||
|  |               context 'when the variable contains a matching filename' do | ||||||
|  |                 let(:changed_file) { '$VALID_FILENAME' } | ||||||
|  | 
 | ||||||
|  |                 it 'creates both jobs' do | ||||||
|  |                   expect(build_names).to contain_exactly('job1', 'job2') | ||||||
|  |                 end | ||||||
|  |               end | ||||||
|  | 
 | ||||||
|  |               context 'when the variable does not contain a matching filename' do | ||||||
|  |                 let(:changed_file) { '$INVALID_FILENAME' } | ||||||
|  | 
 | ||||||
|  |                 it 'does not create job1' do | ||||||
|  |                   expect(build_names).to contain_exactly('job2') | ||||||
|  |                 end | ||||||
|  |               end | ||||||
|  | 
 | ||||||
|  |               context 'when the variable is nested and contains a matching filename' do | ||||||
|  |                 let(:changed_file) { '$VALID_NESTED_VARIABLE' } | ||||||
|  | 
 | ||||||
|  |                 it 'creates both jobs' do | ||||||
|  |                   expect(build_names).to contain_exactly('job1', 'job2') | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do | ||||||
|  |                   before do | ||||||
|  |                     stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false) | ||||||
|  |                   end | ||||||
|  | 
 | ||||||
|  |                   it 'does not create job1' do | ||||||
|  |                     expect(build_names).to contain_exactly('job2') | ||||||
|  |                   end | ||||||
|  |                 end | ||||||
|  |               end | ||||||
|  |             end | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue