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,
|
||||
},
|
||||
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