Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									021a832cb8
								
							
						
					
					
						commit
						eef2437c0a
					
				|  | @ -167,9 +167,6 @@ e2e-test-pipeline-generate: | |||
|   script: | ||||
|     - bundle exec rake "ci:detect_changes[$ENV_FILE]" | ||||
|     - cd $CI_PROJECT_DIR && scripts/generate-e2e-pipeline | ||||
|     - source scripts/utils.sh | ||||
|     - install_gitlab_gem | ||||
|     - scripts/generate-message-to-run-e2e-pipeline.rb | ||||
|   artifacts: | ||||
|     expire_in: 1 day | ||||
|     paths: | ||||
|  |  | |||
|  | @ -22,6 +22,12 @@ export default { | |||
|     paused() { | ||||
|       return this.runner.paused; | ||||
|     }, | ||||
|     contactedAt() { | ||||
|       return this.runner.contactedAt; | ||||
|     }, | ||||
|     status() { | ||||
|       return this.runner.status; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | @ -29,7 +35,8 @@ export default { | |||
| <template> | ||||
|   <div> | ||||
|     <runner-status-badge | ||||
|       :runner="runner" | ||||
|       :contacted-at="contactedAt" | ||||
|       :status="status" | ||||
|       class="gl-display-inline-block gl-max-w-full gl-text-truncate" | ||||
|     /> | ||||
|     <runner-paused-badge | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ export default { | |||
|     <div> | ||||
|       <h1 class="gl-font-size-h-display gl-my-0">{{ name }}</h1> | ||||
|       <div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-mt-3"> | ||||
|         <runner-status-badge :runner="runner" /> | ||||
|         <runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" /> | ||||
|         <runner-type-badge :type="runner.runnerType" /> | ||||
|         <span v-if="runner.createdAt"> | ||||
|           <gl-sprintf :message="__('%{locked} created %{timeago}')"> | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import { s__ } from '~/locale'; | |||
| import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; | ||||
| import { tableField } from '../utils'; | ||||
| import { I18N_STATUS_NEVER_CONTACTED } from '../constants'; | ||||
| import RunnerStatusBadge from './runner_status_badge.vue'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'RunnerManagersTable', | ||||
|  | @ -13,6 +14,7 @@ export default { | |||
|     TimeAgo, | ||||
|     HelpPopover, | ||||
|     GlIntersperse, | ||||
|     RunnerStatusBadge, | ||||
|     RunnerUpgradeStatusIcon: () => | ||||
|       import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'), | ||||
|   }, | ||||
|  | @ -25,6 +27,7 @@ export default { | |||
|   }, | ||||
|   fields: [ | ||||
|     tableField({ key: 'systemId', label: s__('Runners|System ID') }), | ||||
|     tableField({ key: 'status', label: s__('Runners|Status') }), | ||||
|     tableField({ key: 'version', label: s__('Runners|Version') }), | ||||
|     tableField({ key: 'ipAddress', label: s__('Runners|IP Address') }), | ||||
|     tableField({ key: 'executorName', label: s__('Runners|Executor') }), | ||||
|  | @ -48,6 +51,9 @@ export default { | |||
|         {{ s__('Runners|The unique ID for each runner that uses this configuration.') }} | ||||
|       </help-popover> | ||||
|     </template> | ||||
|     <template #cell(status)="{ item = {} }"> | ||||
|       <runner-status-badge :contacted-at="item.contactedAt" :status="item.status" /> | ||||
|     </template> | ||||
|     <template #cell(version)="{ item = {} }"> | ||||
|       {{ item.version }} | ||||
|       <template v-if="item.revision">({{ item.revision }})</template> | ||||
|  |  | |||
|  | @ -26,21 +26,27 @@ export default { | |||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   props: { | ||||
|     runner: { | ||||
|       required: true, | ||||
|       type: Object, | ||||
|     contactedAt: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|       default: null, | ||||
|     }, | ||||
|     status: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     contactedAtTimeAgo() { | ||||
|       if (this.runner.contactedAt) { | ||||
|         return getTimeago().format(this.runner.contactedAt); | ||||
|       if (this.contactedAt) { | ||||
|         return getTimeago().format(this.contactedAt); | ||||
|       } | ||||
|       // Prevent "just now" from being rendered, in case data is missing. | ||||
|       return __('never'); | ||||
|     }, | ||||
|     badge() { | ||||
|       switch (this.runner?.status) { | ||||
|       switch (this.status) { | ||||
|         case STATUS_ONLINE: | ||||
|           return { | ||||
|             icon: 'status-active', | ||||
|  | @ -68,7 +74,7 @@ export default { | |||
|             variant: 'warning', | ||||
|             label: I18N_STATUS_STALE, | ||||
|             // runner may have contacted (or not) and be stale: consider both cases. | ||||
|             tooltip: this.runner.contactedAt | ||||
|             tooltip: this.contactedAt | ||||
|               ? this.timeAgoTooltip(I18N_STALE_TIMEAGO_TOOLTIP) | ||||
|               : I18N_STALE_NEVER_CONTACTED_TOOLTIP, | ||||
|           }; | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| fragment CiRunnerManagerShared on CiRunnerManager { | ||||
|   id | ||||
|   systemId | ||||
|   status | ||||
|   version | ||||
|   revision | ||||
|   executorName | ||||
|  |  | |||
|  | @ -154,6 +154,9 @@ export default { | |||
|     isWorkItemAuthor() { | ||||
|       return getIdFromGraphQLId(this.workItem?.author?.id) === getIdFromGraphQLId(this.author.id); | ||||
|     }, | ||||
|     projectName() { | ||||
|       return this.workItem?.project?.name; | ||||
|     }, | ||||
|   }, | ||||
|   apollo: { | ||||
|     workItem: { | ||||
|  | @ -329,6 +332,9 @@ export default { | |||
|               :can-report-abuse="!isCurrentUserAuthorOfNote" | ||||
|               :is-work-item-author="isWorkItemAuthor" | ||||
|               :work-item-type="workItemType" | ||||
|               :is-author-contributor="note.authorIsContributor" | ||||
|               :max-access-level-of-author="note.maxAccessLevelOfAuthor" | ||||
|               :project-name="projectName" | ||||
|               @startReplying="showReplyForm" | ||||
|               @startEditing="startEditing" | ||||
|               @error="($event) => $emit('error', $event)" | ||||
|  |  | |||
|  | @ -84,6 +84,21 @@ export default { | |||
|       required: false, | ||||
|       default: false, | ||||
|     }, | ||||
|     isAuthorContributor: { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false, | ||||
|     }, | ||||
|     maxAccessLevelOfAuthor: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|       default: '', | ||||
|     }, | ||||
|     projectName: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     assignUserActionText() { | ||||
|  | @ -96,6 +111,17 @@ export default { | |||
|         workItemType: this.workItemType.toLowerCase(), | ||||
|       }); | ||||
|     }, | ||||
|     displayMemberBadgeText() { | ||||
|       return sprintf(__('This user has the %{access} role in the %{name} project.'), { | ||||
|         access: this.maxAccessLevelOfAuthor.toLowerCase(), | ||||
|         name: this.projectName, | ||||
|       }); | ||||
|     }, | ||||
|     displayContributorBadgeText() { | ||||
|       return sprintf(__('This user has previously committed to the %{name} project.'), { | ||||
|         name: this.projectName, | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   methods: { | ||||
|  | @ -140,6 +166,24 @@ export default { | |||
|     > | ||||
|       {{ __('Author') }} | ||||
|     </user-access-role-badge> | ||||
|     <user-access-role-badge | ||||
|       v-if="maxAccessLevelOfAuthor" | ||||
|       v-gl-tooltip | ||||
|       class="gl-mr-3 gl-display-none gl-sm-display-block" | ||||
|       :title="displayMemberBadgeText" | ||||
|       data-testid="max-access-level-badge" | ||||
|     > | ||||
|       {{ maxAccessLevelOfAuthor }} | ||||
|     </user-access-role-badge> | ||||
|     <user-access-role-badge | ||||
|       v-else-if="isAuthorContributor" | ||||
|       v-gl-tooltip | ||||
|       class="gl-mr-3 gl-display-none gl-sm-display-block" | ||||
|       :title="displayContributorBadgeText" | ||||
|       data-testid="contributor-badge" | ||||
|     > | ||||
|       {{ __('Contributor') }} | ||||
|     </user-access-role-badge> | ||||
|     <emoji-picker | ||||
|       v-if="showAwardEmoji && glFeatures.workItemsMvc2" | ||||
|       toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary" | ||||
|  |  | |||
|  | @ -8,11 +8,14 @@ import { | |||
|   GlModalDirective, | ||||
|   GlToggle, | ||||
| } from '@gitlab/ui'; | ||||
| 
 | ||||
| import * as Sentry from '@sentry/browser'; | ||||
| import { s__ } from '~/locale'; | ||||
| 
 | ||||
| import { __, s__ } from '~/locale'; | ||||
| import Tracking from '~/tracking'; | ||||
| import toast from '~/vue_shared/plugins/global_toast'; | ||||
| import { isLoggedIn } from '~/lib/utils/common_utils'; | ||||
| 
 | ||||
| import { | ||||
|   sprintfWorkItem, | ||||
|   I18N_WORK_ITEM_DELETE, | ||||
|  | @ -22,10 +25,15 @@ import { | |||
|   TEST_ID_NOTIFICATIONS_TOGGLE_FORM, | ||||
|   TEST_ID_DELETE_ACTION, | ||||
|   TEST_ID_PROMOTE_ACTION, | ||||
|   TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION, | ||||
|   TEST_ID_COPY_REFERENCE_ACTION, | ||||
|   WIDGET_TYPE_NOTIFICATIONS, | ||||
|   I18N_WORK_ITEM_ERROR_CONVERTING, | ||||
|   WORK_ITEM_TYPE_VALUE_KEY_RESULT, | ||||
|   WORK_ITEM_TYPE_VALUE_OBJECTIVE, | ||||
|   I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL, | ||||
|   I18N_WORK_ITEM_ERROR_COPY_REFERENCE, | ||||
|   I18N_WORK_ITEM_ERROR_COPY_EMAIL, | ||||
| } from '../constants'; | ||||
| import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql'; | ||||
| import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql'; | ||||
|  | @ -38,6 +46,9 @@ export default { | |||
|     notifications: s__('WorkItem|Notifications'), | ||||
|     notificationOn: s__('WorkItem|Notifications turned on.'), | ||||
|     notificationOff: s__('WorkItem|Notifications turned off.'), | ||||
|     copyReference: __('Copy reference'), | ||||
|     referenceCopied: __('Reference copied'), | ||||
|     emailAddressCopied: __('Email address copied'), | ||||
|   }, | ||||
|   components: { | ||||
|     GlDropdown, | ||||
|  | @ -55,6 +66,8 @@ export default { | |||
|   notificationsToggleTestId: TEST_ID_NOTIFICATIONS_TOGGLE_ACTION, | ||||
|   notificationsToggleFormTestId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM, | ||||
|   confidentialityTestId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION, | ||||
|   copyReferenceTestId: TEST_ID_COPY_REFERENCE_ACTION, | ||||
|   copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION, | ||||
|   deleteActionTestId: TEST_ID_DELETE_ACTION, | ||||
|   promoteActionTestId: TEST_ID_PROMOTE_ACTION, | ||||
|   inject: ['fullPath'], | ||||
|  | @ -99,6 +112,21 @@ export default { | |||
|       required: false, | ||||
|       default: false, | ||||
|     }, | ||||
|     workItemReference: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|       default: null, | ||||
|     }, | ||||
|     workItemCreateNoteEmail: { | ||||
|       type: String, | ||||
|       required: false, | ||||
|       default: null, | ||||
|     }, | ||||
|     isModal: { | ||||
|       type: Boolean, | ||||
|       required: false, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   apollo: { | ||||
|     workItemTypes: { | ||||
|  | @ -122,6 +150,15 @@ export default { | |||
|         deleteWorkItem: sprintfWorkItem(I18N_WORK_ITEM_DELETE, this.workItemType), | ||||
|         areYouSureDelete: sprintfWorkItem(I18N_WORK_ITEM_ARE_YOU_SURE_DELETE, this.workItemType), | ||||
|         convertError: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CONVERTING, this.workItemType), | ||||
|         copyCreateNoteEmail: sprintfWorkItem( | ||||
|           I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL, | ||||
|           this.workItemType, | ||||
|         ), | ||||
|         copyReferenceError: sprintfWorkItem(I18N_WORK_ITEM_ERROR_COPY_REFERENCE, this.workItemType), | ||||
|         copyCreateNoteEmailError: sprintfWorkItem( | ||||
|           I18N_WORK_ITEM_ERROR_COPY_EMAIL, | ||||
|           this.workItemType, | ||||
|         ), | ||||
|       }; | ||||
|     }, | ||||
|     canPromoteToObjective() { | ||||
|  | @ -142,6 +179,12 @@ export default { | |||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     copyToClipboard(text, message) { | ||||
|       if (this.isModal) { | ||||
|         navigator.clipboard.writeText(text); | ||||
|       } | ||||
|       toast(message); | ||||
|     }, | ||||
|     handleToggleWorkItemConfidentiality() { | ||||
|       this.track('click_toggle_work_item_confidentiality'); | ||||
|       this.$emit('toggleWorkItemConfidentiality', !this.isConfidential); | ||||
|  | @ -287,6 +330,22 @@ export default { | |||
|               : $options.i18n.enableTaskConfidentiality | ||||
|           }}</gl-dropdown-item | ||||
|         > | ||||
|       </template> | ||||
|       <gl-dropdown-item | ||||
|         ref="workItemReference" | ||||
|         :data-testid="$options.copyReferenceTestId" | ||||
|         :data-clipboard-text="workItemReference" | ||||
|         @click="copyToClipboard(workItemReference, $options.i18n.referenceCopied)" | ||||
|         >{{ $options.i18n.copyReference }}</gl-dropdown-item | ||||
|       > | ||||
|       <template v-if="$options.isLoggedIn && workItemCreateNoteEmail"> | ||||
|         <gl-dropdown-item | ||||
|           ref="workItemCreateNoteEmail" | ||||
|           :data-testid="$options.copyCreateNoteEmailTestId" | ||||
|           :data-clipboard-text="workItemCreateNoteEmail" | ||||
|           @click="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)" | ||||
|           >{{ i18n.copyCreateNoteEmail }}</gl-dropdown-item | ||||
|         > | ||||
|         <gl-dropdown-divider v-if="canDelete" /> | ||||
|       </template> | ||||
|       <gl-dropdown-item | ||||
|  |  | |||
|  | @ -515,7 +515,6 @@ export default { | |||
|             @error="updateError = $event" | ||||
|           /> | ||||
|           <work-item-actions | ||||
|             v-if="canUpdate || canDelete" | ||||
|             :work-item-id="workItem.id" | ||||
|             :subscribed-to-notifications="workItemNotificationsSubscribed" | ||||
|             :work-item-type="workItemType" | ||||
|  | @ -524,6 +523,9 @@ export default { | |||
|             :can-update="canUpdate" | ||||
|             :is-confidential="workItem.confidential" | ||||
|             :is-parent-confidential="parentWorkItemConfidentiality" | ||||
|             :work-item-reference="workItem.reference" | ||||
|             :work-item-create-note-email="workItem.createNoteEmail" | ||||
|             :is-modal="isModal" | ||||
|             @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })" | ||||
|             @toggleWorkItemConfidentiality="toggleConfidentiality" | ||||
|             @error="updateError = $event" | ||||
|  |  | |||
|  | @ -92,6 +92,17 @@ export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP = s__( | |||
|   'WorkItem|A non-confidential %{workItemType} cannot be assigned to a confidential parent %{parentWorkItemType}.', | ||||
| ); | ||||
| 
 | ||||
| export const I18N_WORK_ITEM_ERROR_COPY_REFERENCE = s__( | ||||
|   'WorkItem|Something went wrong while copying the %{workItemType} reference. Please try again.', | ||||
| ); | ||||
| export const I18N_WORK_ITEM_ERROR_COPY_EMAIL = s__( | ||||
|   'WorkItem|Something went wrong while copying the %{workItemType} email address. Please try again.', | ||||
| ); | ||||
| 
 | ||||
| export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__( | ||||
|   'WorkItem|Copy %{workItemType} email address', | ||||
| ); | ||||
| 
 | ||||
| export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => { | ||||
|   const workItemType = workItemTypeArg || s__('WorkItem|Work item'); | ||||
|   return capitalizeFirstCharacter( | ||||
|  | @ -217,6 +228,8 @@ export const TEST_ID_NOTIFICATIONS_TOGGLE_ACTION = 'notifications-toggle-action' | |||
| export const TEST_ID_NOTIFICATIONS_TOGGLE_FORM = 'notifications-toggle-form'; | ||||
| export const TEST_ID_DELETE_ACTION = 'delete-action'; | ||||
| export const TEST_ID_PROMOTE_ACTION = 'promote-action'; | ||||
| export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action'; | ||||
| export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action'; | ||||
| 
 | ||||
| export const ADD = 'ADD'; | ||||
| export const MARK_AS_DONE = 'MARK_AS_DONE'; | ||||
|  |  | |||
|  | @ -10,6 +10,8 @@ fragment WorkItemNote on Note { | |||
|   createdAt | ||||
|   lastEditedAt | ||||
|   url | ||||
|   authorIsContributor | ||||
|   maxAccessLevelOfAuthor | ||||
|   lastEditedBy { | ||||
|     ...User | ||||
|     webPath | ||||
|  |  | |||
|  | @ -11,10 +11,13 @@ fragment WorkItem on WorkItem { | |||
|   createdAt | ||||
|   updatedAt | ||||
|   closedAt | ||||
|   reference(full: true) | ||||
|   createNoteEmail | ||||
|   project { | ||||
|     id | ||||
|     fullPath | ||||
|     archived | ||||
|     name | ||||
|   } | ||||
|   author { | ||||
|     ...Author | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ Usage: | |||
| */ | ||||
| @font-face { | ||||
|   font-family: 'GitLab Mono'; | ||||
|   font-weight: 100 900; | ||||
|   font-display: optional; | ||||
|   font-style: normal; | ||||
|   src: font-url('gitlab-mono/GitLabMono.woff2') format('woff2'); | ||||
|  | @ -28,6 +29,7 @@ Usage: | |||
| 
 | ||||
| @font-face { | ||||
|   font-family: 'GitLab Mono'; | ||||
|   font-weight: 100 900; | ||||
|   font-display: optional; | ||||
|   font-style: italic; | ||||
|   src: font-url('gitlab-mono/GitLabMono-Italic.woff2') format('woff2'); | ||||
|  |  | |||
|  | @ -1258,7 +1258,7 @@ module Ci | |||
|     def id_tokens_variables | ||||
|       Gitlab::Ci::Variables::Collection.new.tap do |variables| | ||||
|         id_tokens.each do |var_name, token_data| | ||||
|           token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['aud']) | ||||
|           token = Gitlab::Ci::JwtV2.for_build(self, aud: expanded_id_token_aud(token_data['aud'])) | ||||
| 
 | ||||
|           variables.append(key: var_name, value: token, public: false, masked: true) | ||||
|         end | ||||
|  | @ -1267,6 +1267,19 @@ module Ci | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def expanded_id_token_aud(aud) | ||||
|       return unless aud | ||||
| 
 | ||||
|       strong_memoize_with(:expanded_id_token_aud, aud) do | ||||
|         # `aud` can be a string or an array of strings. | ||||
|         if aud.is_a?(Array) | ||||
|           aud.map { |x| ExpandVariables.expand(x, -> { scoped_variables.sort_and_expand_all }) } | ||||
|         else | ||||
|           ExpandVariables.expand(aud, -> { scoped_variables.sort_and_expand_all }) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def cache_for_online_runners(&block) | ||||
|       Rails.cache.fetch( | ||||
|         ['has-online-runners', id], | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ To scale GitLab, you can configure GitLab to use multiple application databases. | |||
| Due to [known issues](#known-issues), configuring GitLab with multiple databases is an [Experiment](../../policy/experiment-beta-support.md#experiment). | ||||
| 
 | ||||
| After you have set up multiple databases, GitLab uses a second application database for | ||||
| [CI/CD features](../../ci/index.md), referred to as the `ci` database. | ||||
| [CI/CD features](../../ci/index.md), referred to as the `ci` database. We do not exclude hosting both databases on a single PostgreSQL instance. | ||||
| 
 | ||||
| All tables have exactly the same structure in both the `main`, and `ci` | ||||
| databases. Some examples: | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| --- | ||||
| status: accepted | ||||
| creation-date: "2022-09-07" | ||||
| authors: [ "@ayufan", "@fzimmer", "@DylanGriffith" ] | ||||
| authors: [ "@ayufan", "@fzimmer", "@DylanGriffith", "@lohrc" ] | ||||
| coach: "@ayufan" | ||||
| approvers: [ "@fzimmer" ] | ||||
| approvers: [ "@lohrc" ] | ||||
| owning-stage: "~devops::enablement" | ||||
| participating-stages: [] | ||||
| --- | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ There are two places defined variables can be used. On the: | |||
| | [`environment:url`](../yaml/index.md#environmenturl)                  | yes              | GitLab                 | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab.<br/><br/>Supported are all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules).<br/><br/>Not supported are variables defined in the GitLab Runner `config.toml` and variables created in the job's `script`. | | ||||
| | [`environment:auto_stop_in`](../yaml/index.md#environmentauto_stop_in)| yes              | GitLab                 | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab.<br/><br/> The value of the variable being substituted should be a period of time in a human readable natural language form. See [possible inputs](../yaml/index.md#environmentauto_stop_in) for more information.| | ||||
| | [`except:variables`](../yaml/index.md#onlyvariables--exceptvariables) | no               | Not applicable         | The variable must be in the form of `$variable`. Not supported are the following:<br/><br/>- `CI_ENVIRONMENT_*` variables, except `CI_ENVIRONMENT_NAME` which is supported.<br/>- [Persisted variables](#persisted-variables). | | ||||
| | [`id_tokens:aud`](../yaml/index.md#id_tokens)                         | yes              | GitLab                 | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab. Variable expansion [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/414293) in GitLab 16.1. | | ||||
| | [`image`](../yaml/index.md#image)                                     | yes              | Runner                 | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). | | ||||
| | [`include`](../yaml/index.md#include)                                 | yes              | GitLab                 | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab. <br/><br/>See [Use variables with include](../yaml/includes.md#use-variables-with-include) for more information on supported variables. | | ||||
| | [`only:variables`](../yaml/index.md#onlyvariables--exceptvariables)   | no               | Not applicable         | The variable must be in the form of `$variable`. Not supported are the following:<br/><br/>- `CI_ENVIRONMENT_*` variables, except `CI_ENVIRONMENT_NAME` which is supported.<br/>- [Persisted variables](#persisted-variables). | | ||||
|  |  | |||
|  | @ -2023,7 +2023,10 @@ JWTs created this way support OIDC authentication. The required `aud` sub-keywor | |||
| 
 | ||||
| **Possible inputs**: | ||||
| 
 | ||||
| - Token names with their `aud` claims. `aud` can be a single string or as an array of strings. | ||||
| - Token names with their `aud` claims. `aud` supports: | ||||
|   - A single string. | ||||
|   - An array of strings. | ||||
|   - [CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file). | ||||
| 
 | ||||
| **Example of `id_tokens`**: | ||||
| 
 | ||||
|  |  | |||
|  | @ -53,14 +53,12 @@ in a different color. | |||
| 
 | ||||
| ### Mentioning all members | ||||
| 
 | ||||
| > [Flag](../../administration/feature_flags.md) named `disable_all_mention` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110586) in GitLab 16.1. Disabled by default. | ||||
| > [Flag](../../administration/feature_flags.md) named `disable_all_mention` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110586) in GitLab 16.1. Disabled by default. [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/18442). | ||||
| 
 | ||||
| FLAG: | ||||
| On self-managed GitLab, by default the feature is available. | ||||
| To make it unavailable, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) | ||||
| On self-managed GitLab, by default this flag is not enabled. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) | ||||
| named `disable_all_mention`. | ||||
| On GitLab.com, this feature is available. | ||||
| Disabling this feature on GitLab.com is tracked in [issue 18442](https://gitlab.com/gitlab-org/gitlab/-/issues/18442). | ||||
| On GitLab.com, this flag is enabled. | ||||
| 
 | ||||
| When this feature flag is enabled, typing `@all` in comments and descriptions | ||||
| results in plain text instead of a mention. | ||||
|  |  | |||
|  | @ -237,6 +237,39 @@ To promote a key result: | |||
| 
 | ||||
| Alternatively, use the `/promote_to objective` [quick action](../user/project/quick_actions.md). | ||||
| 
 | ||||
| ## Copy objective or key result reference | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396553) in GitLab 16.1. | ||||
| 
 | ||||
| To refer to an objective or key result elsewhere in GitLab, you can use its full URL or a short reference, which looks like | ||||
| `namespace/project-name#123`, where `namespace` is either a group or a username. | ||||
| 
 | ||||
| To copy the objective or key result reference to your clipboard: | ||||
| 
 | ||||
| 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. | ||||
| 1. Select **Plan > Issues**, then select your objective or key result to view it. | ||||
| 1. In the top right corner, select the vertical ellipsis (**{ellipsis_v}**), then select **Copy Reference**. | ||||
| 
 | ||||
| You can now paste the reference into another description or comment. | ||||
| 
 | ||||
| Read more about objective or key result references in [GitLab-Flavored Markdown](markdown.md#gitlab-specific-references). | ||||
| 
 | ||||
| ## Copy objective or key result email address | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396553) in GitLab 16.1. | ||||
| 
 | ||||
| You can create a comment in an objective or key result by sending an email. | ||||
| Sending an email to this address creates a comment that contains the email body. | ||||
| 
 | ||||
| For more information about creating comments by sending an email and the necessary configuration, see | ||||
| [Reply to a comment by sending email](discussions/index.md#reply-to-a-comment-by-sending-email). | ||||
| 
 | ||||
| To copy the objective's or key result's email address: | ||||
| 
 | ||||
| 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. | ||||
| 1. Select **Plan > Issues**, then select your issue to view it. | ||||
| 1. In the top right corner, select the vertical ellipsis (**{ellipsis_v}**), then select **Copy objective email address** or **Copy key result email address**. | ||||
| 
 | ||||
| ## Close an OKR | ||||
| 
 | ||||
| When an OKR is achieved, you can close it. | ||||
|  |  | |||
|  | @ -0,0 +1,19 @@ | |||
| --- | ||||
| stage: Create | ||||
| group: Source Code | ||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments | ||||
| type: reference | ||||
| --- | ||||
| 
 | ||||
| # GeoJSON files **(FREE)** | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14134) in GitLab 16.1. | ||||
| 
 | ||||
| A GeoJSON file is a format for encoding geographical data structures using JavaScript Object Notation (JSON). | ||||
| It is commonly used for representing geographic features, such as points, lines, and polygons, along with their associated attributes. | ||||
| 
 | ||||
| When added to a repository, files with a `.geojson` extension are rendered as a map containing the GeoJSON data when viewed in GitLab. | ||||
| 
 | ||||
| Map data comes from [OpenStreetMap](https://www.openstreetmap.org/) under the [Open Database License](https://www.openstreetmap.org/copyright). | ||||
| 
 | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 264 KiB | 
|  | @ -326,3 +326,36 @@ You can also filter activity by **Comments only** and **History only** in additi | |||
| ## Comments and threads | ||||
| 
 | ||||
| You can add [comments](discussions/index.md) and reply to threads in tasks. | ||||
| 
 | ||||
| ## Copy task reference | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396553) in GitLab 16.1. | ||||
| 
 | ||||
| To refer to a task elsewhere in GitLab, you can use its full URL or a short reference, which looks like | ||||
| `namespace/project-name#123`, where `namespace` is either a group or a username. | ||||
| 
 | ||||
| To copy the task reference to your clipboard: | ||||
| 
 | ||||
| 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. | ||||
| 1. Select **Plan > Issues**, then select your task to view it. | ||||
| 1. In the top right corner, select the vertical ellipsis (**{ellipsis_v}**), then select **Copy Reference**. | ||||
| 
 | ||||
| You can now paste the reference into another description or comment. | ||||
| 
 | ||||
| For more information about task references, see [GitLab-Flavored Markdown](markdown.md#gitlab-specific-references). | ||||
| 
 | ||||
| ## Copy task email address | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396553) in GitLab 16.1. | ||||
| 
 | ||||
| You can create a comment in a task by sending an email. | ||||
| Sending an email to this address creates a comment that contains the email body. | ||||
| 
 | ||||
| For more information about creating comments by sending an email and the necessary configuration, see | ||||
| [Reply to a comment by sending email](discussions/index.md#reply-to-a-comment-by-sending-email). | ||||
| 
 | ||||
| To copy the task's email address: | ||||
| 
 | ||||
| 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. | ||||
| 1. Select **Plan > Issues**, then select your issue to view it. | ||||
| 1. In the top right corner, select the vertical ellipsis (**{ellipsis_v}**), then select **Copy task email address**. | ||||
|  |  | |||
|  | @ -18,7 +18,10 @@ module Gitlab | |||
|         return unless should_run_validations? | ||||
|         return if commits.empty? | ||||
| 
 | ||||
|         paths = project.repository.find_changed_paths(commits.map(&:sha)) | ||||
|         paths = project.repository.find_changed_paths( | ||||
|           commits.map(&:sha), merge_commit_diff_mode: :all_parents | ||||
|         ) | ||||
| 
 | ||||
|         paths.each do |path| | ||||
|           validate_path(path) | ||||
|         end | ||||
|  |  | |||
|  | @ -51845,6 +51845,9 @@ msgstr "" | |||
| msgid "WorkItem|Converted to task" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "WorkItem|Copy %{workItemType} email address" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "WorkItem|Create %{workItemType}" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -51986,6 +51989,12 @@ msgstr "" | |||
| msgid "WorkItem|Something went wrong when trying to create a child. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "WorkItem|Something went wrong while copying the %{workItemType} email address. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "WorkItem|Something went wrong while copying the %{workItemType} reference. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "WorkItem|Something went wrong while fetching milestones. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -35,6 +35,10 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it 'actions dropdown is displayed' do | ||||
|       expect(page).to have_selector('[data-testid="work-item-actions-dropdown"]') | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'work items title' | ||||
|     it_behaves_like 'work items status' | ||||
|     it_behaves_like 'work items assignees' | ||||
|  | @ -76,10 +80,6 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do | |||
|       visit work_items_path | ||||
|     end | ||||
| 
 | ||||
|     it 'actions dropdown is not displayed' do | ||||
|       expect(page).not_to have_selector('[data-testid="work-item-actions-dropdown"]') | ||||
|     end | ||||
| 
 | ||||
|     it 'todos action is not displayed' do | ||||
|       expect(page).not_to have_selector('[data-testid="work-item-todos-action"]') | ||||
|     end | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ describe('RunnerJobs', () => { | |||
|     createComponent(); | ||||
|     expect(findHeaders().wrappers.map((w) => w.text())).toEqual([ | ||||
|       expect.stringContaining(s__('Runners|System ID')), | ||||
|       s__('Runners|Status'), | ||||
|       s__('Runners|Version'), | ||||
|       s__('Runners|IP Address'), | ||||
|       s__('Runners|Executor'), | ||||
|  | @ -57,6 +58,12 @@ describe('RunnerJobs', () => { | |||
|     expect(findCellText({ field: 'systemId', i: 1 })).toBe(mockItems[1].systemId); | ||||
|   }); | ||||
| 
 | ||||
|   it('shows status', () => { | ||||
|     createComponent(); | ||||
|     expect(findCellText({ field: 'status', i: 0 })).toBe(s__('Runners|Online')); | ||||
|     expect(findCellText({ field: 'status', i: 1 })).toBe(s__('Runners|Online')); | ||||
|   }); | ||||
| 
 | ||||
|   it('shows version', () => { | ||||
|     createComponent({ | ||||
|       item: { version: '1.0' }, | ||||
|  |  | |||
|  | @ -21,13 +21,11 @@ describe('RunnerTypeBadge', () => { | |||
|   const findBadge = () => wrapper.findComponent(GlBadge); | ||||
|   const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip'); | ||||
| 
 | ||||
|   const createComponent = (props = {}) => { | ||||
|   const createComponent = ({ props = {} } = {}) => { | ||||
|     wrapper = shallowMount(RunnerStatusBadge, { | ||||
|       propsData: { | ||||
|         runner: { | ||||
|         contactedAt: '2020-12-31T23:59:00Z', | ||||
|         status: STATUS_ONLINE, | ||||
|         }, | ||||
|         ...props, | ||||
|       }, | ||||
|       directives: { | ||||
|  | @ -55,7 +53,7 @@ describe('RunnerTypeBadge', () => { | |||
| 
 | ||||
|   it('renders never contacted state', () => { | ||||
|     createComponent({ | ||||
|       runner: { | ||||
|       props: { | ||||
|         contactedAt: null, | ||||
|         status: STATUS_NEVER_CONTACTED, | ||||
|       }, | ||||
|  | @ -68,7 +66,7 @@ describe('RunnerTypeBadge', () => { | |||
| 
 | ||||
|   it('renders offline state', () => { | ||||
|     createComponent({ | ||||
|       runner: { | ||||
|       props: { | ||||
|         contactedAt: '2020-12-31T00:00:00Z', | ||||
|         status: STATUS_OFFLINE, | ||||
|       }, | ||||
|  | @ -81,7 +79,7 @@ describe('RunnerTypeBadge', () => { | |||
| 
 | ||||
|   it('renders stale state', () => { | ||||
|     createComponent({ | ||||
|       runner: { | ||||
|       props: { | ||||
|         contactedAt: '2020-01-01T00:00:00Z', | ||||
|         status: STATUS_STALE, | ||||
|       }, | ||||
|  | @ -94,7 +92,7 @@ describe('RunnerTypeBadge', () => { | |||
| 
 | ||||
|   it('renders stale state with no contact time', () => { | ||||
|     createComponent({ | ||||
|       runner: { | ||||
|       props: { | ||||
|         contactedAt: null, | ||||
|         status: STATUS_STALE, | ||||
|       }, | ||||
|  | @ -108,7 +106,7 @@ describe('RunnerTypeBadge', () => { | |||
|   describe('does not fail when data is missing', () => { | ||||
|     it('contacted_at is missing', () => { | ||||
|       createComponent({ | ||||
|         runner: { | ||||
|         props: { | ||||
|           contactedAt: null, | ||||
|           status: STATUS_ONLINE, | ||||
|         }, | ||||
|  | @ -120,7 +118,7 @@ describe('RunnerTypeBadge', () => { | |||
| 
 | ||||
|     it('status is missing', () => { | ||||
|       createComponent({ | ||||
|         runner: { | ||||
|         props: { | ||||
|           status: null, | ||||
|         }, | ||||
|       }); | ||||
|  |  | |||
|  | @ -25,6 +25,8 @@ describe('Work Item Note Actions', () => { | |||
|   const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]'); | ||||
|   const findReportAbuseToAdminButton = () => wrapper.find('[data-testid="abuse-note-action"]'); | ||||
|   const findAuthorBadge = () => wrapper.find('[data-testid="author-badge"]'); | ||||
|   const findMaxAccessLevelBadge = () => wrapper.find('[data-testid="max-access-level-badge"]'); | ||||
|   const findContributorBadge = () => wrapper.find('[data-testid="contributor-badge"]'); | ||||
| 
 | ||||
|   const addEmojiMutationResolver = jest.fn().mockResolvedValue({ | ||||
|     data: { | ||||
|  | @ -45,6 +47,9 @@ describe('Work Item Note Actions', () => { | |||
|     canReportAbuse = false, | ||||
|     workItemType = 'Task', | ||||
|     isWorkItemAuthor = false, | ||||
|     isAuthorContributor = false, | ||||
|     maxAccessLevelOfAuthor = '', | ||||
|     projectName = 'Project name', | ||||
|   } = {}) => { | ||||
|     wrapper = shallowMount(WorkItemNoteActions, { | ||||
|       propsData: { | ||||
|  | @ -56,6 +61,9 @@ describe('Work Item Note Actions', () => { | |||
|         canReportAbuse, | ||||
|         workItemType, | ||||
|         isWorkItemAuthor, | ||||
|         isAuthorContributor, | ||||
|         maxAccessLevelOfAuthor, | ||||
|         projectName, | ||||
|       }, | ||||
|       provide: { | ||||
|         glFeatures: { | ||||
|  | @ -251,5 +259,41 @@ describe('Work Item Note Actions', () => { | |||
|         expect(findAuthorBadge().attributes('title')).toBe('This user is the author of this task.'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('Max access level badge', () => { | ||||
|       it('does not show the access level badge by default', () => { | ||||
|         createComponent(); | ||||
| 
 | ||||
|         expect(findMaxAccessLevelBadge().exists()).toBe(false); | ||||
|       }); | ||||
| 
 | ||||
|       it('shows the access badge when we have a valid value', () => { | ||||
|         createComponent({ maxAccessLevelOfAuthor: 'Owner' }); | ||||
| 
 | ||||
|         expect(findMaxAccessLevelBadge().exists()).toBe(true); | ||||
|         expect(findMaxAccessLevelBadge().text()).toBe('Owner'); | ||||
|         expect(findMaxAccessLevelBadge().attributes('title')).toBe( | ||||
|           'This user has the owner role in the Project name project.', | ||||
|         ); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('Contributor badge', () => { | ||||
|       it('does not show the contributor badge by default', () => { | ||||
|         createComponent(); | ||||
| 
 | ||||
|         expect(findContributorBadge().exists()).toBe(false); | ||||
|       }); | ||||
| 
 | ||||
|       it('shows the contributor badge the note author is a contributor', () => { | ||||
|         createComponent({ isAuthorContributor: true }); | ||||
| 
 | ||||
|         expect(findContributorBadge().exists()).toBe(true); | ||||
|         expect(findContributorBadge().text()).toBe('Contributor'); | ||||
|         expect(findContributorBadge().attributes('title')).toBe( | ||||
|           'This user has previously committed to the Project name project.', | ||||
|         ); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ import { | |||
|   updateWorkItemMutationResponse, | ||||
|   workItemByIidResponseFactory, | ||||
|   workItemQueryResponse, | ||||
|   mockWorkItemCommentNoteByContributor, | ||||
|   mockWorkItemCommentByMaintainer, | ||||
| } from 'jest/work_items/mock_data'; | ||||
| import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; | ||||
| import { mockTracking } from 'helpers/tracking_helper'; | ||||
|  | @ -236,8 +238,9 @@ describe('Work Item Note', () => { | |||
|     }); | ||||
| 
 | ||||
|     describe('main comment', () => { | ||||
|       beforeEach(() => { | ||||
|       beforeEach(async () => { | ||||
|         createComponent({ isFirstNote: true }); | ||||
|         await waitForPromises(); | ||||
|       }); | ||||
| 
 | ||||
|       it('should have the note header, actions and body', () => { | ||||
|  | @ -250,6 +253,10 @@ describe('Work Item Note', () => { | |||
|       it('should have the reply button props', () => { | ||||
|         expect(findNoteActions().props('showReply')).toBe(true); | ||||
|       }); | ||||
| 
 | ||||
|       it('should have the project name', () => { | ||||
|         expect(findNoteActions().props('projectName')).toBe('Project name'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('comment threads', () => { | ||||
|  | @ -374,6 +381,28 @@ describe('Work Item Note', () => { | |||
|           }, | ||||
|         ); | ||||
|       }); | ||||
| 
 | ||||
|       describe('Max access level badge', () => { | ||||
|         it('should pass the max access badge props', async () => { | ||||
|           createComponent({ note: mockWorkItemCommentByMaintainer }); | ||||
|           await waitForPromises(); | ||||
| 
 | ||||
|           expect(findNoteActions().props('maxAccessLevelOfAuthor')).toBe( | ||||
|             mockWorkItemCommentByMaintainer.maxAccessLevelOfAuthor, | ||||
|           ); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('Contributor badge', () => { | ||||
|         it('should pass the contributor props', async () => { | ||||
|           createComponent({ note: mockWorkItemCommentNoteByContributor }); | ||||
|           await waitForPromises(); | ||||
| 
 | ||||
|           expect(findNoteActions().props('isAuthorContributor')).toBe( | ||||
|             mockWorkItemCommentNoteByContributor.authorIsContributor, | ||||
|           ); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,10 +1,12 @@ | |||
| import { GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui'; | ||||
| import Vue from 'vue'; | ||||
| import VueApollo from 'vue-apollo'; | ||||
| 
 | ||||
| import createMockApollo from 'helpers/mock_apollo_helper'; | ||||
| import { stubComponent } from 'helpers/stub_component'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| 
 | ||||
| import { isLoggedIn } from '~/lib/utils/common_utils'; | ||||
| import toast from '~/vue_shared/plugins/global_toast'; | ||||
| import WorkItemActions from '~/work_items/components/work_item_actions.vue'; | ||||
|  | @ -14,6 +16,8 @@ import { | |||
|   TEST_ID_NOTIFICATIONS_TOGGLE_FORM, | ||||
|   TEST_ID_DELETE_ACTION, | ||||
|   TEST_ID_PROMOTE_ACTION, | ||||
|   TEST_ID_COPY_REFERENCE_ACTION, | ||||
|   TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION, | ||||
| } from '~/work_items/constants'; | ||||
| import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql'; | ||||
| import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; | ||||
|  | @ -33,6 +37,9 @@ describe('WorkItemActions component', () => { | |||
| 
 | ||||
|   let wrapper; | ||||
|   let mockApollo; | ||||
|   const mockWorkItemReference = 'gitlab-org/gitlab-test#1'; | ||||
|   const mockWorkItemCreateNoteEmail = | ||||
|     'gitlab-incoming+gitlab-org-gitlab-test-2-ddpzuq0zd2wefzofcpcdr3dg7-issue-1@gmail.com'; | ||||
| 
 | ||||
|   const findModal = () => wrapper.findComponent(GlModal); | ||||
|   const findConfidentialityToggleButton = () => | ||||
|  | @ -41,6 +48,9 @@ describe('WorkItemActions component', () => { | |||
|     wrapper.findByTestId(TEST_ID_NOTIFICATIONS_TOGGLE_ACTION); | ||||
|   const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION); | ||||
|   const findPromoteButton = () => wrapper.findByTestId(TEST_ID_PROMOTE_ACTION); | ||||
|   const findCopyReferenceButton = () => wrapper.findByTestId(TEST_ID_COPY_REFERENCE_ACTION); | ||||
|   const findCopyCreateNoteEmailButton = () => | ||||
|     wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION); | ||||
|   const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *'); | ||||
|   const findDropdownItemsActual = () => | ||||
|     findDropdownItems().wrappers.map((x) => { | ||||
|  | @ -78,6 +88,8 @@ describe('WorkItemActions component', () => { | |||
|     notificationsMock = [updateWorkItemNotificationsMutation, jest.fn()], | ||||
|     convertWorkItemMutationHandler = convertWorkItemMutationSuccessHandler, | ||||
|     workItemType = 'Task', | ||||
|     workItemReference = mockWorkItemReference, | ||||
|     workItemCreateNoteEmail = mockWorkItemCreateNoteEmail, | ||||
|   } = {}) => { | ||||
|     const handlers = [notificationsMock]; | ||||
|     mockApollo = createMockApollo([ | ||||
|  | @ -96,6 +108,8 @@ describe('WorkItemActions component', () => { | |||
|         subscribed, | ||||
|         isParentConfidential, | ||||
|         workItemType, | ||||
|         workItemReference, | ||||
|         workItemCreateNoteEmail, | ||||
|       }, | ||||
|       provide: { | ||||
|         fullPath: 'gitlab-org/gitlab', | ||||
|  | @ -140,6 +154,14 @@ describe('WorkItemActions component', () => { | |||
|         testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION, | ||||
|         text: 'Turn on confidentiality', | ||||
|       }, | ||||
|       { | ||||
|         testId: TEST_ID_COPY_REFERENCE_ACTION, | ||||
|         text: 'Copy reference', | ||||
|       }, | ||||
|       { | ||||
|         testId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION, | ||||
|         text: 'Copy task email address', | ||||
|       }, | ||||
|       { | ||||
|         divider: true, | ||||
|       }, | ||||
|  | @ -359,4 +381,37 @@ describe('WorkItemActions component', () => { | |||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('copy reference action', () => { | ||||
|     it('shows toast when user clicks on the action', () => { | ||||
|       createComponent(); | ||||
| 
 | ||||
|       expect(findCopyReferenceButton().exists()).toBe(true); | ||||
|       findCopyReferenceButton().vm.$emit('click'); | ||||
| 
 | ||||
|       expect(toast).toHaveBeenCalledWith('Reference copied'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('copy email address action', () => { | ||||
|     it.each(['key result', 'objective'])( | ||||
|       'renders correct button name when work item is %s', | ||||
|       (workItemType) => { | ||||
|         createComponent({ workItemType }); | ||||
| 
 | ||||
|         expect(findCopyCreateNoteEmailButton().text()).toEqual( | ||||
|           `Copy ${workItemType} email address`, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
| 
 | ||||
|     it('shows toast when user clicks on the action', () => { | ||||
|       createComponent(); | ||||
| 
 | ||||
|       expect(findCopyCreateNoteEmailButton().exists()).toBe(true); | ||||
|       findCopyCreateNoteEmailButton().vm.$emit('click'); | ||||
| 
 | ||||
|       expect(toast).toHaveBeenCalledWith('Email address copied'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -97,6 +97,7 @@ export const workItemQueryResponse = { | |||
|         id: '1', | ||||
|         fullPath: 'test-project-path', | ||||
|         archived: false, | ||||
|         name: 'Project name', | ||||
|       }, | ||||
|       workItemType: { | ||||
|         __typename: 'WorkItemType', | ||||
|  | @ -200,6 +201,7 @@ export const updateWorkItemMutationResponse = { | |||
|           id: '1', | ||||
|           fullPath: 'test-project-path', | ||||
|           archived: false, | ||||
|           name: 'Project name', | ||||
|         }, | ||||
|         workItemType: { | ||||
|           __typename: 'WorkItemType', | ||||
|  | @ -214,6 +216,9 @@ export const updateWorkItemMutationResponse = { | |||
|           adminParentLink: false, | ||||
|           __typename: 'WorkItemPermissions', | ||||
|         }, | ||||
|         reference: 'test-project-path#1', | ||||
|         createNoteEmail: | ||||
|           'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com', | ||||
|         widgets: [ | ||||
|           { | ||||
|             type: 'HIERARCHY', | ||||
|  | @ -304,6 +309,7 @@ export const convertWorkItemMutationResponse = { | |||
|           id: '1', | ||||
|           fullPath: 'test-project-path', | ||||
|           archived: false, | ||||
|           name: 'Project name', | ||||
|         }, | ||||
|         workItemType: { | ||||
|           __typename: 'WorkItemType', | ||||
|  | @ -318,6 +324,9 @@ export const convertWorkItemMutationResponse = { | |||
|           adminParentLink: false, | ||||
|           __typename: 'WorkItemPermissions', | ||||
|         }, | ||||
|         reference: 'gitlab-org/gitlab-test#1', | ||||
|         createNoteEmail: | ||||
|           'gitlab-incoming+gitlab-org-gitlab-test-2-ddpzuq0zd2wefzofcpcdr3dg7-issue-1@gmail.com', | ||||
|         widgets: [ | ||||
|           { | ||||
|             type: 'HIERARCHY', | ||||
|  | @ -456,6 +465,7 @@ export const workItemResponseFactory = ({ | |||
|         id: '1', | ||||
|         fullPath: 'test-project-path', | ||||
|         archived: false, | ||||
|         name: 'Project name', | ||||
|       }, | ||||
|       workItemType, | ||||
|       userPermissions: { | ||||
|  | @ -465,6 +475,9 @@ export const workItemResponseFactory = ({ | |||
|         adminParentLink, | ||||
|         __typename: 'WorkItemPermissions', | ||||
|       }, | ||||
|       reference: 'test-project-path#1', | ||||
|       createNoteEmail: | ||||
|         'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com', | ||||
|       widgets: [ | ||||
|         { | ||||
|           __typename: 'WorkItemWidgetDescription', | ||||
|  | @ -725,6 +738,7 @@ export const createWorkItemMutationResponse = { | |||
|           id: '1', | ||||
|           fullPath: 'test-project-path', | ||||
|           archived: false, | ||||
|           name: 'Project name', | ||||
|         }, | ||||
|         workItemType: { | ||||
|           __typename: 'WorkItemType', | ||||
|  | @ -739,6 +753,9 @@ export const createWorkItemMutationResponse = { | |||
|           adminParentLink: false, | ||||
|           __typename: 'WorkItemPermissions', | ||||
|         }, | ||||
|         reference: 'test-project-path#1', | ||||
|         createNoteEmail: | ||||
|           'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com', | ||||
|         widgets: [], | ||||
|       }, | ||||
|       errors: [], | ||||
|  | @ -956,6 +973,7 @@ export const workItemHierarchyEmptyResponse = { | |||
|               id: '1', | ||||
|               fullPath: 'test-project-path', | ||||
|               archived: false, | ||||
|               name: 'Project name', | ||||
|             }, | ||||
|             userPermissions: { | ||||
|               deleteWorkItem: false, | ||||
|  | @ -965,6 +983,9 @@ export const workItemHierarchyEmptyResponse = { | |||
|               __typename: 'WorkItemPermissions', | ||||
|             }, | ||||
|             confidential: false, | ||||
|             reference: 'test-project-path#1', | ||||
|             createNoteEmail: | ||||
|               'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com', | ||||
|             widgets: [ | ||||
|               { | ||||
|                 type: 'HIERARCHY', | ||||
|  | @ -1015,6 +1036,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = { | |||
|         id: '1', | ||||
|         fullPath: 'test-project-path', | ||||
|         archived: false, | ||||
|         name: 'Project name', | ||||
|       }, | ||||
|       confidential: false, | ||||
|       widgets: [ | ||||
|  | @ -1167,12 +1189,16 @@ export const workItemHierarchyResponse = { | |||
|               id: '1', | ||||
|               fullPath: 'test-project-path', | ||||
|               archived: false, | ||||
|               name: 'Project name', | ||||
|             }, | ||||
|             description: 'Issue description', | ||||
|             state: 'OPEN', | ||||
|             createdAt: '2022-08-03T12:41:54Z', | ||||
|             updatedAt: null, | ||||
|             closedAt: null, | ||||
|             reference: 'test-project-path#1', | ||||
|             createNoteEmail: | ||||
|               'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-1@gmail.com', | ||||
|             widgets: [ | ||||
|               { | ||||
|                 type: 'HIERARCHY', | ||||
|  | @ -1244,6 +1270,7 @@ export const workItemObjectiveWithChild = { | |||
|     id: '1', | ||||
|     fullPath: 'test-project-path', | ||||
|     archived: false, | ||||
|     name: 'Project name', | ||||
|   }, | ||||
|   userPermissions: { | ||||
|     deleteWorkItem: true, | ||||
|  | @ -1327,6 +1354,7 @@ export const workItemHierarchyTreeResponse = { | |||
|         id: '1', | ||||
|         fullPath: 'test-project-path', | ||||
|         archived: false, | ||||
|         name: 'Project name', | ||||
|       }, | ||||
|       widgets: [ | ||||
|         { | ||||
|  | @ -1417,7 +1445,11 @@ export const changeIndirectWorkItemParentMutationResponse = { | |||
|           id: '1', | ||||
|           fullPath: 'test-project-path', | ||||
|           archived: false, | ||||
|           name: 'Project name', | ||||
|         }, | ||||
|         reference: 'test-project-path#13', | ||||
|         createNoteEmail: | ||||
|           'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-13@gmail.com', | ||||
|         widgets: [ | ||||
|           { | ||||
|             __typename: 'WorkItemWidgetHierarchy', | ||||
|  | @ -1480,7 +1512,11 @@ export const changeWorkItemParentMutationResponse = { | |||
|           id: '1', | ||||
|           fullPath: 'test-project-path', | ||||
|           archived: false, | ||||
|           name: 'Project name', | ||||
|         }, | ||||
|         reference: 'test-project-path#2', | ||||
|         createNoteEmail: | ||||
|           'gitlab-incoming+test-project-path-13fp7g6i9agekcv71s0jx9p58-issue-2@gmail.com', | ||||
|         widgets: [ | ||||
|           { | ||||
|             __typename: 'WorkItemWidgetHierarchy', | ||||
|  | @ -1953,6 +1989,8 @@ export const mockWorkItemNotesResponse = { | |||
|                       lastEditedBy: null, | ||||
|                       system: true, | ||||
|                       internal: false, | ||||
|                       maxAccessLevelOfAuthor: 'Owner', | ||||
|                       authorIsContributor: false, | ||||
|                       discussion: { | ||||
|                         id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234', | ||||
|                       }, | ||||
|  | @ -2002,6 +2040,8 @@ export const mockWorkItemNotesResponse = { | |||
|                       lastEditedBy: null, | ||||
|                       system: true, | ||||
|                       internal: false, | ||||
|                       maxAccessLevelOfAuthor: 'Owner', | ||||
|                       authorIsContributor: false, | ||||
|                       discussion: { | ||||
|                         id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723565678', | ||||
|                       }, | ||||
|  | @ -2050,6 +2090,8 @@ export const mockWorkItemNotesResponse = { | |||
|                       lastEditedBy: null, | ||||
|                       system: true, | ||||
|                       internal: false, | ||||
|                       maxAccessLevelOfAuthor: 'Owner', | ||||
|                       authorIsContributor: false, | ||||
|                       discussion: { | ||||
|                         id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987', | ||||
|                       }, | ||||
|  | @ -2158,6 +2200,8 @@ export const mockWorkItemNotesByIidResponse = { | |||
|                             lastEditedBy: null, | ||||
|                             system: true, | ||||
|                             internal: false, | ||||
|                             maxAccessLevelOfAuthor: null, | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234', | ||||
|  | @ -2209,6 +2253,8 @@ export const mockWorkItemNotesByIidResponse = { | |||
|                             lastEditedBy: null, | ||||
|                             system: true, | ||||
|                             internal: false, | ||||
|                             maxAccessLevelOfAuthor: null, | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723568765', | ||||
|  | @ -2261,6 +2307,8 @@ export const mockWorkItemNotesByIidResponse = { | |||
|                             lastEditedBy: null, | ||||
|                             system: true, | ||||
|                             internal: false, | ||||
|                             maxAccessLevelOfAuthor: null, | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876', | ||||
|  | @ -2371,6 +2419,8 @@ export const mockMoreWorkItemNotesResponse = { | |||
|                             lastEditedBy: null, | ||||
|                             system: true, | ||||
|                             internal: false, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e', | ||||
|  | @ -2422,6 +2472,8 @@ export const mockMoreWorkItemNotesResponse = { | |||
|                             lastEditedBy: null, | ||||
|                             system: true, | ||||
|                             internal: false, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e', | ||||
|  | @ -2471,6 +2523,8 @@ export const mockMoreWorkItemNotesResponse = { | |||
|                             lastEditedBy: null, | ||||
|                             system: true, | ||||
|                             internal: false, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876', | ||||
|  | @ -2539,6 +2593,8 @@ export const createWorkItemNoteResponse = { | |||
|                 lastEditedAt: null, | ||||
|                 url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', | ||||
|                 lastEditedBy: null, | ||||
|                 maxAccessLevelOfAuthor: 'Owner', | ||||
|                 authorIsContributor: false, | ||||
|                 discussion: { | ||||
|                   id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122', | ||||
|                   __typename: 'Discussion', | ||||
|  | @ -2590,6 +2646,8 @@ export const mockWorkItemCommentNote = { | |||
|   lastEditedBy: null, | ||||
|   system: false, | ||||
|   internal: false, | ||||
|   maxAccessLevelOfAuthor: 'Owner', | ||||
|   authorIsContributor: false, | ||||
|   discussion: { | ||||
|     id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876', | ||||
|   }, | ||||
|  | @ -2613,6 +2671,16 @@ export const mockWorkItemCommentNote = { | |||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export const mockWorkItemCommentNoteByContributor = { | ||||
|   ...mockWorkItemCommentNote, | ||||
|   authorIsContributor: true, | ||||
| }; | ||||
| 
 | ||||
| export const mockWorkItemCommentByMaintainer = { | ||||
|   ...mockWorkItemCommentNote, | ||||
|   maxAccessLevelOfAuthor: 'Maintainer', | ||||
| }; | ||||
| 
 | ||||
| export const mockWorkItemNotesResponseWithComments = { | ||||
|   data: { | ||||
|     workspace: { | ||||
|  | @ -2674,6 +2742,8 @@ export const mockWorkItemNotesResponseWithComments = { | |||
|                             url: | ||||
|                               'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', | ||||
|                             lastEditedBy: null, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3', | ||||
|  | @ -2712,6 +2782,8 @@ export const mockWorkItemNotesResponseWithComments = { | |||
|                             url: | ||||
|                               'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191', | ||||
|                             lastEditedBy: null, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3', | ||||
|  | @ -2759,6 +2831,8 @@ export const mockWorkItemNotesResponseWithComments = { | |||
|                             lastEditedBy: null, | ||||
|                             system: false, | ||||
|                             internal: false, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987', | ||||
|  | @ -2831,6 +2905,8 @@ export const workItemNotesCreateSubscriptionResponse = { | |||
|               lastEditedBy: null, | ||||
|               system: true, | ||||
|               internal: false, | ||||
|               maxAccessLevelOfAuthor: 'Owner', | ||||
|               authorIsContributor: false, | ||||
|               discussion: { | ||||
|                 id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987', | ||||
|               }, | ||||
|  | @ -2901,6 +2977,8 @@ export const workItemNotesUpdateSubscriptionResponse = { | |||
|       lastEditedBy: null, | ||||
|       system: true, | ||||
|       internal: false, | ||||
|       maxAccessLevelOfAuthor: 'Owner', | ||||
|       authorIsContributor: false, | ||||
|       discussion: { | ||||
|         id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987', | ||||
|       }, | ||||
|  | @ -2952,6 +3030,8 @@ export const workItemSystemNoteWithMetadata = { | |||
|   lastEditedAt: '2023-05-05T07:19:37Z', | ||||
|   url: 'https://gdk.test:3443/flightjs/Flight/-/work_items/46#note_1651', | ||||
|   lastEditedBy: null, | ||||
|   maxAccessLevelOfAuthor: 'Owner', | ||||
|   authorIsContributor: false, | ||||
|   discussion: { | ||||
|     id: 'gid://gitlab/Discussion/7d4a46ea0525e2eeed451f7b718b0ebe73205374', | ||||
|     __typename: 'Discussion', | ||||
|  | @ -3044,6 +3124,8 @@ export const workItemNotesWithSystemNotesWithChangedDescription = { | |||
|                             lastEditedAt: '2023-05-10T05:21:01Z', | ||||
|                             url: 'https://gdk.test:3443/gnuwget/Wget2/-/work_items/79#note_1687', | ||||
|                             lastEditedBy: null, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/aa72f4c2f3eef66afa6d79a805178801ce4bd89f', | ||||
|  | @ -3104,6 +3186,8 @@ export const workItemNotesWithSystemNotesWithChangedDescription = { | |||
|                             lastEditedAt: '2023-05-10T05:21:05Z', | ||||
|                             url: 'https://gdk.test:3443/gnuwget/Wget2/-/work_items/79#note_1688', | ||||
|                             lastEditedBy: null, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/a7d3cf7bd72f7a98f802845f538af65cb11a02cc', | ||||
|  | @ -3165,6 +3249,8 @@ export const workItemNotesWithSystemNotesWithChangedDescription = { | |||
|                             lastEditedAt: '2023-05-10T05:21:08Z', | ||||
|                             url: 'https://gdk.test:3443/gnuwget/Wget2/-/work_items/79#note_1689', | ||||
|                             lastEditedBy: null, | ||||
|                             maxAccessLevelOfAuthor: 'Owner', | ||||
|                             authorIsContributor: false, | ||||
|                             discussion: { | ||||
|                               id: | ||||
|                                 'gid://gitlab/Discussion/391eed1ee0a258cc966a51dde900424f3b51b95d', | ||||
|  |  | |||
|  | @ -24,11 +24,42 @@ RSpec.describe Gitlab::Checks::DiffCheck, feature_category: :source_code_managem | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when commits is not empty' do | ||||
|     context 'when commits include merge commit' do | ||||
|       before do | ||||
|         allow(project.repository).to receive(:new_commits).and_return( | ||||
|           project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51') | ||||
|         ) | ||||
|         allow(project.repository).to receive(:new_commits).and_return([project.repository.commit(merge_commit)]) | ||||
|         allow(subject).to receive(:should_run_validations?).and_return(true) | ||||
|         allow(subject).to receive(:validate_path) | ||||
|         allow(subject).to receive(:validate_file_paths) | ||||
|         subject.validate! | ||||
|       end | ||||
| 
 | ||||
|       context 'when merge commit does not include additional changes' do | ||||
|         let(:merge_commit) { '2b298117a741cdb06eb48df2c33f1390cf89f7e8' } | ||||
| 
 | ||||
|         it 'checks the additional changes' do | ||||
|           expect(subject).to have_received(:validate_file_paths).with([]) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when merge commit includes additional changes' do | ||||
|         let(:merge_commit) { '1ada92f78a19f27cb442a0a205f1c451a3a15432' } | ||||
|         let(:file_paths) { ['files/locked/baz.lfs'] } | ||||
| 
 | ||||
|         it 'checks the additional changes' do | ||||
|           expect(subject).to have_received(:validate_file_paths).with(file_paths) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when commits is not empty' do | ||||
|       let(:new_commits) do | ||||
|         from = 'be93687618e4b132087f430a4d8fc3a609c9b77c' | ||||
|         to = '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' | ||||
|         project.repository.commits_between(from, to) | ||||
|       end | ||||
| 
 | ||||
|       before do | ||||
|         allow(project.repository).to receive(:new_commits).and_return(new_commits) | ||||
|       end | ||||
| 
 | ||||
|       context 'when deletion is true' do | ||||
|  | @ -74,6 +105,52 @@ RSpec.describe Gitlab::Checks::DiffCheck, feature_category: :source_code_managem | |||
|             expect { subject.validate! }.not_to raise_error | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when a merge commit merged a file locked by another user' do | ||||
|           let(:new_commits) do | ||||
|             project.repository.commits_by(oids: %w[ | ||||
|               760c58db5a6f3b64ad7e3ff6b3c4a009da7d9b33 | ||||
|               2b298117a741cdb06eb48df2c33f1390cf89f7e8 | ||||
|             ]) | ||||
|           end | ||||
| 
 | ||||
|           before do | ||||
|             create(:lfs_file_lock, user: owner, project: project, path: 'files/locked/foo.lfs') | ||||
|             create(:lfs_file_lock, user: user, project: project, path: 'files/locked/bar.lfs') | ||||
|           end | ||||
| 
 | ||||
|           it "doesn't raise any error" do | ||||
|             expect { subject.validate! }.not_to raise_error | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         context 'when a merge commit includes additional file locked by another user' do | ||||
|           # e.g. when merging the user added an additional change. | ||||
|           # This merge commit: https://gitlab.com/gitlab-org/gitlab-test/-/commit/1ada92f | ||||
|           # merges `files/locked/bar.lfs` and also adds a new file | ||||
|           # `files/locked/baz.lfs`. In this case we ignore `files/locked/bar.lfs` | ||||
|           # as it is already detected in the commit c41e12c, however, we do | ||||
|           # detect the new `files/locked/baz.lfs` file. | ||||
|           # | ||||
|           let(:new_commits) do | ||||
|             project.repository.commits_by(oids: %w[ | ||||
|               760c58db5a6f3b64ad7e3ff6b3c4a009da7d9b33 | ||||
|               2b298117a741cdb06eb48df2c33f1390cf89f7e8 | ||||
|               c41e12c387b4e0e41bfc17208252d6a6430f2fcd | ||||
|               1ada92f78a19f27cb442a0a205f1c451a3a15432 | ||||
|             ]) | ||||
|           end | ||||
| 
 | ||||
|           before do | ||||
|             create(:lfs_file_lock, user: owner, project: project, path: 'files/locked/foo.lfs') | ||||
|             create(:lfs_file_lock, user: user, project: project, path: 'files/locked/bar.lfs') | ||||
|             create(:lfs_file_lock, user: owner, project: project, path: 'files/locked/baz.lfs') | ||||
|           end | ||||
| 
 | ||||
|           it "does raise an error" do | ||||
|             expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'files/locked/baz.lfs' is locked in Git LFS by #{owner.name}") | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -15,6 +15,28 @@ RSpec.describe Gitlab::Ci::Config::Entry::IdToken do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when given `aud` is a variable' do | ||||
|     it 'is valid' do | ||||
|       config = { aud: '$WATHEVER' } | ||||
|       id_token = described_class.new(config) | ||||
| 
 | ||||
|       id_token.compose! | ||||
| 
 | ||||
|       expect(id_token).to be_valid | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when given `aud` includes a variable' do | ||||
|     it 'is valid' do | ||||
|       config = { aud: 'blah-$WATHEVER' } | ||||
|       id_token = described_class.new(config) | ||||
| 
 | ||||
|       id_token.compose! | ||||
| 
 | ||||
|       expect(id_token).to be_valid | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when given `aud` as an array' do | ||||
|     it 'is valid and concatenates the values' do | ||||
|       config = { aud: ['https://gitlab.com', 'https://aws.com'] } | ||||
|  | @ -27,6 +49,17 @@ RSpec.describe Gitlab::Ci::Config::Entry::IdToken do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when given `aud` as an array with variables' do | ||||
|     it 'is valid and concatenates the values' do | ||||
|       config = { aud: ['$WATHEVER', 'blah-$WATHEVER'] } | ||||
|       id_token = described_class.new(config) | ||||
| 
 | ||||
|       id_token.compose! | ||||
| 
 | ||||
|       expect(id_token).to be_valid | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when not given an `aud`' do | ||||
|     it 'is invalid' do | ||||
|       config = {} | ||||
|  |  | |||
|  | @ -3856,6 +3856,80 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def | |||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when ID tokens are defined with variables' do | ||||
|       let(:ci_server_url) { Gitlab.config.gitlab.url } | ||||
| 
 | ||||
|       let(:ci_server_host) { Gitlab.config.gitlab.host } | ||||
| 
 | ||||
|       before do | ||||
|         rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s | ||||
|         stub_application_setting(ci_jwt_signing_key: rsa_key) | ||||
|         build.metadata.update!(id_tokens: { | ||||
|                                  'ID_TOKEN_1' => { aud: '$CI_SERVER_URL' }, | ||||
|                                  'ID_TOKEN_2' => { aud: 'https://$CI_SERVER_HOST' }, | ||||
|                                  'ID_TOKEN_3' => { aud: ['developers', '$CI_SERVER_URL', 'https://$CI_SERVER_HOST'] } | ||||
|                                }) | ||||
|         build.runner = build_stubbed(:ci_runner) | ||||
|       end | ||||
| 
 | ||||
|       subject(:runner_vars) { build.variables.to_runner_variables } | ||||
| 
 | ||||
|       it 'includes the ID token variables with expanded aud values' do | ||||
|         expect(runner_vars).to include( | ||||
|           a_hash_including(key: 'ID_TOKEN_1', public: false, masked: true), | ||||
|           a_hash_including(key: 'ID_TOKEN_2', public: false, masked: true), | ||||
|           a_hash_including(key: 'ID_TOKEN_3', public: false, masked: true) | ||||
|         ) | ||||
| 
 | ||||
|         id_token_var_1 = runner_vars.find { |var| var[:key] == 'ID_TOKEN_1' } | ||||
|         id_token_var_2 = runner_vars.find { |var| var[:key] == 'ID_TOKEN_2' } | ||||
|         id_token_var_3 = runner_vars.find { |var| var[:key] == 'ID_TOKEN_3' } | ||||
|         id_token_1 = JWT.decode(id_token_var_1[:value], nil, false).first | ||||
|         id_token_2 = JWT.decode(id_token_var_2[:value], nil, false).first | ||||
|         id_token_3 = JWT.decode(id_token_var_3[:value], nil, false).first | ||||
|         expect(id_token_1['aud']).to eq(ci_server_url) | ||||
|         expect(id_token_2['aud']).to eq("https://#{ci_server_host}") | ||||
|         expect(id_token_3['aud']).to match_array(['developers', ci_server_url, "https://#{ci_server_host}"]) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when ID tokens are defined with variables of an environment' do | ||||
|       let!(:envprod) do | ||||
|         create(:environment, project: build.project, name: 'production') | ||||
|       end | ||||
| 
 | ||||
|       let!(:varprod) do | ||||
|         create(:ci_variable, project: build.project, key: 'ENVIRONMENT_SCOPED_VAR', value: 'https://prod', environment_scope: 'prod*') | ||||
|       end | ||||
| 
 | ||||
|       before do | ||||
|         build.update!(environment: 'production') | ||||
|         rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s | ||||
|         stub_application_setting(ci_jwt_signing_key: rsa_key) | ||||
|         build.metadata.update!(id_tokens: { | ||||
|                                  'ID_TOKEN_1' => { aud: '$ENVIRONMENT_SCOPED_VAR' }, | ||||
|                                  'ID_TOKEN_2' => { aud: ['$CI_ENVIRONMENT_NAME', '$ENVIRONMENT_SCOPED_VAR'] } | ||||
|                                }) | ||||
|         build.runner = build_stubbed(:ci_runner) | ||||
|       end | ||||
| 
 | ||||
|       subject(:runner_vars) { build.variables.to_runner_variables } | ||||
| 
 | ||||
|       it 'includes the ID token variables with expanded aud values' do | ||||
|         expect(runner_vars).to include( | ||||
|           a_hash_including(key: 'ID_TOKEN_1', public: false, masked: true), | ||||
|           a_hash_including(key: 'ID_TOKEN_2', public: false, masked: true) | ||||
|         ) | ||||
| 
 | ||||
|         id_token_var_1 = runner_vars.find { |var| var[:key] == 'ID_TOKEN_1' } | ||||
|         id_token_var_2 = runner_vars.find { |var| var[:key] == 'ID_TOKEN_2' } | ||||
|         id_token_1 = JWT.decode(id_token_var_1[:value], nil, false).first | ||||
|         id_token_2 = JWT.decode(id_token_var_2[:value], nil, false).first | ||||
|         expect(id_token_1['aud']).to eq('https://prod') | ||||
|         expect(id_token_2['aud']).to match_array(['production', 'https://prod']) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#scoped_variables' do | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue