Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-19 12:09:51 +00:00
parent 021a832cb8
commit eef2437c0a
36 changed files with 708 additions and 45 deletions

View File

@ -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:

View File

@ -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

View File

@ -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}')">

View File

@ -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>

View File

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

View File

@ -1,6 +1,7 @@
fragment CiRunnerManagerShared on CiRunnerManager {
id
systemId
status
version
revision
executorName

View File

@ -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)"

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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';

View File

@ -10,6 +10,8 @@ fragment WorkItemNote on Note {
createdAt
lastEditedAt
url
authorIsContributor
maxAccessLevelOfAuthor
lastEditedBy {
...User
webPath

View File

@ -11,10 +11,13 @@ fragment WorkItem on WorkItem {
createdAt
updatedAt
closedAt
reference(full: true)
createNoteEmail
project {
id
fullPath
archived
name
}
author {
...Author

View File

@ -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');

View File

@ -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],

View File

@ -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:

View File

@ -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: []
---

View File

@ -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). |

View File

@ -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`**:

View File

@ -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.

View File

@ -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.

View File

@ -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).
![GeoJSON file rendered as a map](img/geo_json_file_rendered_v16_1.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -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**.

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

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

View File

@ -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,
},
});

View File

@ -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.',
);
});
});
});
});

View File

@ -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,
);
});
});
});
});
});

View File

@ -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');
});
});
});

View File

@ -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',

View File

@ -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

View File

@ -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 = {}

View File

@ -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