Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-19 12:11:07 +00:00
parent 687bfbe932
commit f6e2d0776a
26 changed files with 828 additions and 488 deletions

View file

@ -1,5 +1,7 @@
{ {
"ee/*": { "type": "ee" }, "ee/*": { "type": "ee" },
"app/*": { "type": "ce" },
"lib/*": { "type": "ce" },
"config/initializers/*.rb": { "config/initializers/*.rb": {
"alternate": "spec/initializers/{}_spec.rb", "alternate": "spec/initializers/{}_spec.rb",
"type": "source" "type": "source"
@ -57,37 +59,40 @@
"type": "source" "type": "source"
}, },
"app/presenters/*.rb": { "app/presenters/*.rb": {
"alternate": "spec/app/presenters/{}_spec.rb",
"related": "ee/app/presenters/ee/{}.rb", "related": "ee/app/presenters/ee/{}.rb",
"type": "source" "type": "source"
}, },
"app/serializers/*.rb": { "app/serializers/*.rb": {
"alternate": "spec/app/serializers/{}_spec.rb",
"related": "ee/app/serializers/ee/{}.rb", "related": "ee/app/serializers/ee/{}.rb",
"type": "source" "type": "source"
}, },
"app/services/*.rb": { "app/services/*.rb": {
"alternate": "spec/app/services/{}_spec.rb",
"related": "ee/app/services/ee/{}.rb", "related": "ee/app/services/ee/{}.rb",
"type": "source" "type": "source"
}, },
"app/uploaders/*.rb": { "app/uploaders/*.rb": {
"alternate": "spec/app/uploaders/{}_spec.rb",
"related": "ee/app/uploaders/ee/{}.rb", "related": "ee/app/uploaders/ee/{}.rb",
"type": "source" "type": "source"
}, },
"app/validators/*.rb": { "app/validators/*.rb": {
"alternate": "spec/app/validators/{}_spec.rb",
"related": "ee/app/validators/ee/{}.rb", "related": "ee/app/validators/ee/{}.rb",
"type": "source" "type": "source"
}, },
"app/views/*.rb": { "app/views/*.rb": {
"alternate": "spec/app/views/{}_spec.rb",
"related": "ee/app/views/ee/{}.rb", "related": "ee/app/views/ee/{}.rb",
"type": "source" "type": "source"
}, },
"app/workers/*.rb": { "app/workers/*.rb": {
"alternate": "spec/app/workers/{}_spec.rb",
"related": "ee/app/workers/ee/{}.rb", "related": "ee/app/workers/ee/{}.rb",
"type": "source" "type": "source"
}, },
"app/*.rb": {
"alternate": "spec/{}_spec.rb",
"type": "source"
},
"spec/*_spec.rb": { "spec/*_spec.rb": {
"alternate": "app/{}.rb", "alternate": "app/{}.rb",
"type": "test" "type": "test"
@ -124,8 +129,79 @@
"alternate": "ee/lib/api/{}.rb", "alternate": "ee/lib/api/{}.rb",
"type": "test" "type": "test"
}, },
"ee/app/controllers/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/controllers/{}.rb",
"type": "source"
},
"ee/app/finders/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/finders/{}.rb",
"type": "source"
},
"ee/app/graphql/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/graphql/{}.rb",
"type": "source"
},
"ee/app/helpers/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/helpers/{}.rb",
"type": "source"
},
"ee/app/mailers/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/mailers/{}.rb",
"type": "source"
},
"ee/app/models/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/models/{}.rb",
"type": "source"
},
"ee/app/policies/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/policies/{}.rb",
"type": "source"
},
"ee/app/presenters/ee/*.rb": {
"alternate": "ee/spec/{}_spec.rb",
"related": "app/presenters/{}.rb",
"type": "source"
},
"ee/app/serializers/ee/*.rb": {
"alternate": "spec/app/serializers/{}_spec.rb",
"related": "app/serializers/{}.rb",
"type": "source"
},
"ee/app/services/ee/*.rb": {
"alternate": "spec/app/services/{}_spec.rb",
"related": "app/services/{}.rb",
"type": "source"
},
"ee/app/uploaders/ee/*.rb": {
"alternate": "spec/app/uploaders/{}_spec.rb",
"related": "app/uploaders/{}.rb",
"type": "source"
},
"ee/app/validators/ee/*.rb": {
"alternate": "spec/app/validators/{}_spec.rb",
"related": "app/validators/{}.rb",
"type": "source"
},
"ee/app/views/ee/*.rb": {
"alternate": "spec/app/views/{}_spec.rb",
"related": "app/views/{}.rb",
"type": "source"
},
"ee/app/workers/ee/*.rb": {
"alternate": "spec/app/workers/{}_spec.rb",
"related": "app/workers/{}.rb",
"type": "source"
},
"ee/app/*.rb": { "ee/app/*.rb": {
"alternate": "ee/spec/{}_spec.rb", "alternate": "ee/spec/{}_spec.rb",
"related": "app/{}.rb",
"type": "source" "type": "source"
}, },
"ee/spec/*_spec.rb": { "ee/spec/*_spec.rb": {
@ -136,6 +212,11 @@
"alternate": "ee/spec/lib/{}_spec.rb", "alternate": "ee/spec/lib/{}_spec.rb",
"type": "source" "type": "source"
}, },
"ee/lib/ee/*.rb": {
"alternate": "ee/spec/lib/{}_spec.rb",
"related": "lib/{}.rb",
"type": "source"
},
"ee/spec/lib/*_spec.rb": { "ee/spec/lib/*_spec.rb": {
"alternate": "ee/lib/{}.rb", "alternate": "ee/lib/{}.rb",
"type": "test" "type": "test"
@ -154,16 +235,18 @@
}, },
"ee/app/assets/javascripts/*.js": { "ee/app/assets/javascripts/*.js": {
"alternate": "ee/spec/frontend/{}_spec.js", "alternate": "ee/spec/frontend/{}_spec.js",
"related": "app/assets/javascripts/{}.js",
"type": "source" "type": "source"
}, },
"ee/app/assets/javascripts/*.vue": { "ee/app/assets/javascripts/*.vue": {
"alternate": "ee/spec/frontend/{}_spec.js", "alternate": "ee/spec/frontend/{}_spec.js",
"related": "app/assets/javascripts/{}.vue",
"type": "source" "type": "source"
}, },
"ee/spec/frontend/*_spec.js": { "ee/spec/frontend/*_spec.js": {
"alternate": ["ee/app/assets/javascripts/{}.vue", "ee/app/assets/javascripts/{}.js"], "alternate": ["ee/app/assets/javascripts/{}.vue", "ee/app/assets/javascripts/{}.js"],
"type": "test" "type": "test"
}, },
"*.rb": {"dispatch": "bundle exec rubocop {file}"}, "*.rb": { "dispatch": "bundle exec rubocop {file}" },
"*_spec.rb": {"dispatch": "bundle exec rspec {file}"} "*_spec.rb": { "dispatch": "bundle exec rspec {file}" }
} }

View file

@ -42,7 +42,7 @@ export default {
computed: { computed: {
isReference() { isReference() {
return this.nodeType === 'reference'; return this.nodeType.startsWith('reference');
}, },
isCommand() { isCommand() {
@ -96,7 +96,7 @@ export default {
getText(item) { getText(item) {
if (this.isEmoji) return item.e; if (this.isEmoji) return item.e;
switch (this.nodeType === 'reference' && this.nodeProps.referenceType) { switch (this.isReference && this.nodeProps.referenceType) {
case 'user': case 'user':
return `${this.char}${item.username}`; return `${this.char}${item.username}`;
case 'issue': case 'issue':
@ -105,12 +105,13 @@ export default {
case 'snippet': case 'snippet':
return `${this.char}${item.id}`; return `${this.char}${item.id}`;
case 'milestone': case 'milestone':
case 'label':
return `${this.char}${item.title}`; return `${this.char}${item.title}`;
case 'label':
return item.title;
case 'command': case 'command':
return `${this.char}${item.name} `; return `${this.char}${item.name}`;
case 'epic': case 'epic':
return `${item.reference}`; return item.reference;
case 'vulnerability': case 'vulnerability':
return `[vulnerability:${item.id}]`; return `[vulnerability:${item.id}]`;
default: default:
@ -119,17 +120,35 @@ export default {
}, },
getProps(item) { getProps(item) {
const props = {};
if (this.isEmoji) { if (this.isEmoji) {
return { Object.assign(props, {
name: item.name, name: item.name,
unicodeVersion: item.u, unicodeVersion: item.u,
title: item.d, title: item.d,
moji: item.e, moji: item.e,
...this.nodeProps, });
};
} }
return this.nodeProps; if (this.isLabel || this.isMilestone) {
Object.assign(props, {
originalText: `${this.char}${
/\W/.test(item.title) ? JSON.stringify(item.title) : item.title
}`,
});
}
if (this.isLabel) {
Object.assign(props, {
text: item.title,
color: item.color,
});
}
Object.assign(props, this.nodeProps);
return props;
}, },
onKeyDown({ event }) { onKeyDown({ event }) {

View file

@ -0,0 +1,34 @@
<script>
import { NodeViewWrapper } from '@tiptap/vue-2';
import { GlLabel } from '@gitlab/ui';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
name: 'DetailsWrapper',
components: {
NodeViewWrapper,
GlLabel,
},
props: {
node: {
type: Object,
required: true,
},
},
computed: {
isScopedLabel() {
return isScopedLabel({ title: this.node.attrs.originalText });
},
},
};
</script>
<template>
<node-view-wrapper class="gl-display-inline-block">
<gl-label
size="sm"
:scoped="isScopedLabel"
:background-color="node.attrs.color"
:title="node.attrs.text"
/>
</node-view-wrapper>
</template>

View file

@ -46,22 +46,10 @@ export default Node.create({
tag: 'a.gfm:not([data-link=true])', tag: 'a.gfm:not([data-link=true])',
priority: PARSE_HTML_PRIORITY_HIGHEST, priority: PARSE_HTML_PRIORITY_HIGHEST,
}, },
{
tag: 'span.gl-label',
},
]; ];
}, },
renderHTML({ node }) { renderHTML({ node }) {
return [ return ['a', { href: '#' }, node.attrs.text];
'a',
{
class: node.attrs.className,
href: '#',
'data-reference-type': node.attrs.referenceType,
'data-original': node.attrs.originalText,
},
node.attrs.text,
];
}, },
}); });

View file

@ -0,0 +1,35 @@
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import LabelWrapper from '../components/wrappers/label.vue';
import Reference from './reference';
export default Reference.extend({
name: 'reference_label',
addAttributes() {
return {
...this.parent(),
text: {
default: null,
parseHTML: (element) => {
const text = element.querySelector('.gl-label-text').textContent;
const scopedText = element.querySelector('.gl-label-text-scoped')?.textContent;
if (!scopedText) return text;
return `${text}${SCOPED_LABEL_DELIMITER}${scopedText}`;
},
},
color: {
default: null,
parseHTML: (element) => element.querySelector('.gl-label-text').style.backgroundColor,
},
};
},
parseHTML() {
return [{ tag: 'span.gl-label' }];
},
addNodeView() {
return new VueNodeViewRenderer(LabelWrapper);
},
});

View file

@ -34,7 +34,10 @@ function createSuggestionPlugin({
tiptapEditor tiptapEditor
.chain() .chain()
.focus() .focus()
.insertContentAt(range, [{ type: nodeType, attrs: props }]) .insertContentAt(range, [
{ type: nodeType, attrs: props },
{ type: 'text', text: ' ' },
])
.run(); .run();
}, },
@ -82,7 +85,7 @@ function createSuggestionPlugin({
}, },
onUpdate(props) { onUpdate(props) {
component.updateProps(props); component?.updateProps(props);
if (!props.clientRect) { if (!props.clientRect) {
return; return;
@ -100,12 +103,12 @@ function createSuggestionPlugin({
return true; return true;
} }
return component.ref?.onKeyDown(props); return component?.ref?.onKeyDown(props);
}, },
onExit() { onExit() {
popup?.[0].destroy(); popup?.[0].destroy();
component.destroy(); component?.destroy();
}, },
}; };
}, },
@ -151,7 +154,7 @@ export default Node.create({
editor: this.editor, editor: this.editor,
char: '~', char: '~',
dataSource: gl.GfmAutoComplete?.dataSources.labels, dataSource: gl.GfmAutoComplete?.dataSources.labels,
nodeType: 'reference', nodeType: 'reference_label',
nodeProps: { nodeProps: {
referenceType: 'label', referenceType: 'label',
}, },

View file

@ -43,6 +43,7 @@ import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph'; import Paragraph from '../extensions/paragraph';
import PasteMarkdown from '../extensions/paste_markdown'; import PasteMarkdown from '../extensions/paste_markdown';
import Reference from '../extensions/reference'; import Reference from '../extensions/reference';
import ReferenceLabel from '../extensions/reference_label';
import ReferenceDefinition from '../extensions/reference_definition'; import ReferenceDefinition from '../extensions/reference_definition';
import Sourcemap from '../extensions/sourcemap'; import Sourcemap from '../extensions/sourcemap';
import Strike from '../extensions/strike'; import Strike from '../extensions/strike';
@ -132,6 +133,7 @@ export const createContentEditor = ({
Paragraph, Paragraph,
PasteMarkdown.configure({ eventHub, renderMarkdown }), PasteMarkdown.configure({ eventHub, renderMarkdown }),
Reference, Reference,
ReferenceLabel,
ReferenceDefinition, ReferenceDefinition,
Sourcemap, Sourcemap,
Strike, Strike,

View file

@ -33,6 +33,7 @@ import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list'; import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph'; import Paragraph from '../extensions/paragraph';
import Reference from '../extensions/reference'; import Reference from '../extensions/reference';
import ReferenceLabel from '../extensions/reference_label';
import ReferenceDefinition from '../extensions/reference_definition'; import ReferenceDefinition from '../extensions/reference_definition';
import Strike from '../extensions/strike'; import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript'; import Subscript from '../extensions/subscript';
@ -61,6 +62,7 @@ import {
renderHTMLNode, renderHTMLNode,
renderContent, renderContent,
renderBulletList, renderBulletList,
renderReference,
preserveUnchanged, preserveUnchanged,
bold, bold,
italic, italic,
@ -184,9 +186,8 @@ const defaultSerializerConfig = {
[ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item), [ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item),
[OrderedList.name]: preserveUnchanged(renderOrderedList), [OrderedList.name]: preserveUnchanged(renderOrderedList),
[Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph), [Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph),
[Reference.name]: (state, node) => { [Reference.name]: renderReference,
state.write(node.attrs.originalText || node.attrs.text); [ReferenceLabel.name]: renderReference,
},
[ReferenceDefinition.name]: preserveUnchanged({ [ReferenceDefinition.name]: preserveUnchanged({
render: (state, node, parent, index, same, sourceMarkdown) => { render: (state, node, parent, index, same, sourceMarkdown) => {
const nextSibling = parent.maybeChild(index + 1); const nextSibling = parent.maybeChild(index + 1);

View file

@ -423,6 +423,10 @@ export function renderOrderedList(state, node) {
}); });
} }
export function renderReference(state, node) {
state.write(node.attrs.originalText || node.attrs.text);
}
const generateBoldTags = (wrapTagName = openTag) => { const generateBoldTags = (wrapTagName = openTag) => {
return (_, mark) => { return (_, mark) => {
const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1]; const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];

View file

@ -866,3 +866,36 @@ To enable debug output in the rails console, [enter the rails console](#rails-co
```ruby ```ruby
Rails.logger.level = Logger::DEBUG Rails.logger.level = Logger::DEBUG
``` ```
#### Get all error messages associated with groups, subgroups, members, and requesters
Collect error messages associated with groups, subgroups, members, and requesters. This
captures error messages that may not appear in the Web interface. This can be especially helpful
for troubleshooting issues with [LDAP group sync](ldap_synchronization.md#group-sync)
and unexpected behavior with users and their membership in groups and subgroups.
```ruby
# Find the group and subgroup
group = Group.find_by_full_path("parent_group")
subgroup = Group.find_by_full_path("parent_group/child_group")
# Group and subgroup errors
group.valid?
group.errors.map(&:full_messages)
subgroup.valid?
subgroup.errors.map(&:full_messages)
# Group and subgroup errors for the members AND requesters
group.requesters.map(&:valid?)
group.requesters.map(&:errors).map(&:full_messages)
group.members.map(&:valid?)
group.members.map(&:errors).map(&:full_messages)
group.members_and_requesters.map(&:errors).map(&:full_messages)
subgroup.requesters.map(&:valid?)
subgroup.requesters.map(&:errors).map(&:full_messages)
subgroup.members.map(&:valid?)
subgroup.members.map(&:errors).map(&:full_messages)
subgroup.members_and_requesters.map(&:errors).map(&:full_messages)
```

View file

@ -265,135 +265,6 @@ group = Group.find_by_full_path 'group'
user.max_member_access_for_group group.id user.max_member_access_for_group group.id
``` ```
## Groups
### Transfer group to another location
```ruby
user = User.find_by_username('<username>')
group = Group.find_by_name("<group_name>")
parent_group = Group.find_by(id: "<group_id>")
service = ::Groups::TransferService.new(group, user)
service.execute(parent_group)
```
### Count unique users in a group and subgroups
```ruby
group = Group.find_by_path_or_name("groupname")
members = []
for member in group.members_with_descendants
members.push(member.user_name)
end
members.uniq.length
```
```ruby
group = Group.find_by_path_or_name("groupname")
# Count users from subgroup and up (inherited)
group.members_with_parents.count
# Count users from the parent group and down (specific grants)
parent.members_with_descendants.count
```
### Find groups that are pending deletion
```ruby
#
# This section lists all the groups which are pending deletion
#
Group.all.each do |g|
if g.marked_for_deletion?
puts "Group ID: #{g.id}"
puts "Group name: #{g.name}"
puts "Group path: #{g.full_path}"
end
end
```
### Delete a group
```ruby
GroupDestroyWorker.perform_async(group_id, user_id)
```
### Modify group project creation
```ruby
# Project creation levels: 0 - No one, 1 - Maintainers, 2 - Developers + Maintainers
group = Group.find_by_path_or_name('group-name')
group.project_creation_level=0
```
### Modify group - disable 2FA requirement
WARNING:
When disabling the 2FA Requirement on a subgroup, the whole parent group (including all subgroups) is affected by this change.
```ruby
group = Group.find_by_path_or_name('group-name')
group.require_two_factor_authentication=false
group.save
```
### Check and toggle a feature for all projects in a group
```ruby
projects = Group.find_by_name('_group_name').projects
projects.each do |p|
state = p.<feature-name>?
if state
puts "#{p.name} has <feature-name> already enabled. Skipping..."
else
puts "#{p.name} didn't have <feature-name> enabled. Enabling..."
p.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
end
end
```
To find features that can be toggled, run `pp p.project_feature`.
Available permission levels are listed in
[concerns/featurable.rb](https://gitlab.com/gitlab-org/gitlab/blob/master/app/models/concerns/featurable.rb).
### Get all error messages associated with groups, subgroups, members, and requesters
Collect error messages associated with groups, subgroups, members, and requesters. This
captures error messages that may not appear in the Web interface. This can be especially helpful
for troubleshooting issues with [LDAP group sync](../auth/ldap/ldap_synchronization.md#group-sync)
and unexpected behavior with users and their membership in groups and subgroups.
```ruby
# Find the group and subgroup
group = Group.find_by_full_path("parent_group")
subgroup = Group.find_by_full_path("parent_group/child_group")
# Group and subgroup errors
group.valid?
group.errors.map(&:full_messages)
subgroup.valid?
subgroup.errors.map(&:full_messages)
# Group and subgroup errors for the members AND requesters
group.requesters.map(&:valid?)
group.requesters.map(&:errors).map(&:full_messages)
group.members.map(&:valid?)
group.members.map(&:errors).map(&:full_messages)
group.members_and_requesters.map(&:errors).map(&:full_messages)
subgroup.requesters.map(&:valid?)
subgroup.requesters.map(&:errors).map(&:full_messages)
subgroup.members.map(&:valid?)
subgroup.members.map(&:errors).map(&:full_messages)
subgroup.members_and_requesters.map(&:errors).map(&:full_messages)
```
## Routes
## Merge requests ## Merge requests
### Close a merge request ### Close a merge request

View file

@ -153,7 +153,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
Trigger a pipeline by using a pipeline [trigger token](../ci/triggers/index.md#create-a-trigger-token) Trigger a pipeline by using a pipeline [trigger token](../ci/triggers/index.md#create-a-trigger-token)
or a [CI/CD job token](../ci/jobs/ci_job_token.md) for authentication. or a [CI/CD job token](../ci/jobs/ci_job_token.md) for authentication.
With a CI/CD job token, the [triggered pipeline is a multi-project pipeline](../ci/jobs/ci_job_token.md#trigger-a-multi-project-pipeline-by-using-a-cicd-job-token). With a CI/CD job token, the [triggered pipeline is a multi-project pipeline](../ci/pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api).
The job that authenticates the request becomes associated with the upstream pipeline, The job that authenticates the request becomes associated with the upstream pipeline,
which is visible on the [pipeline graph](../ci/pipelines/downstream_pipelines.md#view-multi-project-pipelines-in-pipeline-graphs). which is visible on the [pipeline graph](../ci/pipelines/downstream_pipelines.md#view-multi-project-pipelines-in-pipeline-graphs).

View file

@ -107,7 +107,18 @@ identifying abstract concepts and are subject to changes as we refine the design
- **Catalog** is the collection of projects that are set to contain components. - **Catalog** is the collection of projects that are set to contain components.
- **Version** is the release name of a tag in the project, which allows components to be pinned to a specific revision. - **Version** is the release name of a tag in the project, which allows components to be pinned to a specific revision.
## Characteristics of a component ## Definition of pipeline component
A pipeline component is a reusable single-purpose building block that abstracts away a single pipeline configuration unit. Components are used to compose a part or entire pipeline configuration.
It can optionally take input parameters and set output data to be adaptable and reusable in different pipeline contexts,
while encapsulating and isolating implementation details.
Components allow a pipeline to be assembled by using abstractions instead of having all the details defined in one place.
When using a component in a pipeline, a user shouldn't need to know the implementation details of the component and should
only rely on the provided interface. The interface will have a version / revision, so that users understand which revision they are interfacing with.
A pipeline component defines its type which indicates in which context of the pipeline configuration the component can be used.
For example, a component of type X can only be used according to the type X use-case.
For best experience with any systems made of components it's fundamental that components are single purpose, For best experience with any systems made of components it's fundamental that components are single purpose,
isolated, reusable and resolvable. isolated, reusable and resolvable.
@ -118,7 +129,7 @@ isolated, reusable and resolvable.
- **Reusability:** a component is designed to be used in different pipelines. - **Reusability:** a component is designed to be used in different pipelines.
Depending on the assumptions it's built on a component can be more or less generic. Depending on the assumptions it's built on a component can be more or less generic.
Generic components are more reusable but may require more customization. Generic components are more reusable but may require more customization.
- **Resolvable:** When a component depends on another component, this dependency needs to be explicit and trackable. Hidden dependencies can lead to myriads of problems. - **Resolvable:** When a component depends on another component, this dependency must be explicit and trackable.
## Proposal ## Proposal

View file

@ -20,7 +20,8 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints
(scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled). (scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled).
- [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts). - [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts).
- [Get job token's job](../../api/jobs.md#get-job-tokens-job). - [Get job token's job](../../api/jobs.md#get-job-tokens-job).
- [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter. - [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter
to [trigger a multi-project pipeline](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api).
- [Releases](../../api/releases/index.md) and [Release links](../../api/releases/links.md). - [Releases](../../api/releases/index.md) and [Release links](../../api/releases/links.md).
- [Terraform plan](../../user/infrastructure/index.md). - [Terraform plan](../../user/infrastructure/index.md).
@ -99,28 +100,6 @@ The job token scope is only for controlling access to private projects.
There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559) to improve There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559) to improve
the feature with more strategic control of the access permissions. the feature with more strategic control of the access permissions.
## Trigger a multi-project pipeline by using a CI/CD job token
> `CI_JOB_TOKEN` for multi-project pipelines was [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/31573) from GitLab Premium to GitLab Free in 12.4.
You can use the `CI_JOB_TOKEN` to [trigger multi-project pipelines](../../api/pipeline_triggers.md#trigger-a-pipeline-with-a-token)
from a CI/CD job.
For example:
```yaml
trigger_pipeline:
stage: deploy
script:
- curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=main "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
rules:
- if: $CI_COMMIT_TAG
environment: production
```
If you use the `CI_PIPELINE_SOURCE` [predefined CI/CD variable](../variables/predefined_variables.md)
in a pipeline triggered this way, [the value is `pipeline` (not `triggered`)](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines).
## Download an artifact from a different pipeline **(PREMIUM)** ## Download an artifact from a different pipeline **(PREMIUM)**
> `CI_JOB_TOKEN` for artifacts download with the API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) in GitLab 9.5. > `CI_JOB_TOKEN` for artifacts download with the API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) in GitLab 9.5.

View file

@ -7,33 +7,59 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Downstream pipelines **(FREE)** # Downstream pipelines **(FREE)**
A downstream pipeline is any GitLab CI/CD pipeline triggered by another pipeline. A downstream pipeline is any GitLab CI/CD pipeline triggered by another pipeline.
A downstream pipeline can be either: Downstream pipelines run independently and concurrently to the upstream pipeline
that triggered them.
- A [parent-child pipeline](downstream_pipelines.md#parent-child-pipelines), which is a downstream pipeline triggered - A [parent-child pipeline](downstream_pipelines.md#parent-child-pipelines) is a downstream pipeline
in the same project as the first pipeline. triggered in the *same* project as the first pipeline.
- A [multi-project pipeline](#multi-project-pipelines), which is a downstream pipeline triggered - A [multi-project pipeline](#multi-project-pipelines) is a downstream pipeline triggered
in a different project than the first pipeline. in a *different* project than the first pipeline.
Parent-child pipelines and multi-project pipelines can sometimes be used for similar purposes, You can sometimes use parent-child pipelines and multi-project pipelines for similar purposes,
but there are some key differences. but there are [key differences](pipeline_architectures.md).
Parent-child pipelines: ## Parent-child pipelines
A parent pipeline is one that triggers a downstream pipeline in the same project.
The downstream pipeline is called a child pipeline. Child pipelines:
- Run under the same project, ref, and commit SHA as the parent pipeline. - Run under the same project, ref, and commit SHA as the parent pipeline.
- Affect the overall status of the ref the pipeline runs against. For example, - Do not directly affect the overall status of the ref the pipeline runs against. For example,
if a pipeline fails for the main branch, it's common to say that "main is broken". if a pipeline fails for the main branch, it's common to say that "main is broken".
The status of child pipelines don't directly affect the status of the ref, unless the child The status of child pipelines only affects the status of the ref if the child
pipeline is triggered with [`strategy:depend`](../yaml/index.md#triggerstrategy). pipeline is triggered with [`strategy:depend`](../yaml/index.md#triggerstrategy).
- Are automatically canceled if the pipeline is configured with [`interruptible`](../yaml/index.md#interruptible) - Are automatically canceled if the pipeline is configured with [`interruptible`](../yaml/index.md#interruptible)
when a new pipeline is created for the same ref. when a new pipeline is created for the same ref.
- Display only the parent pipelines in the pipeline index page. Child pipelines are - Are not displayed in the pipeline index page. You can only view child pipelines on
visible when visiting their parent pipeline's page. their parent pipeline's page.
- Are limited to 2 levels of nesting. A parent pipeline can trigger multiple child pipelines,
and those child pipeline can trigger multiple child pipelines (`A -> B -> C`). ### Nested child pipelines
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29651) in GitLab 13.4.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243747) in GitLab 13.5.
Parent and child pipelines were introduced with a maximum depth of one level of child
pipelines, which was later increased to two. A parent pipeline can trigger many child
pipelines, and these child pipelines can trigger their own child pipelines. It's not
possible to trigger another level of child pipelines.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Nested Dynamic Pipelines](https://youtu.be/C5j3ju9je2M).
## Multi-project pipelines
A pipeline in one project can trigger downstream pipelines in another project,
called multi-project pipelines. The user triggering the upstream pipeline must be able to
start pipelines in the downstream project, otherwise [the downstream pipeline fails to start](#trigger-job-fails-and-does-not-create-multi-project-pipeline).
For example, you might deploy your web application from three different GitLab projects.
With multi-project pipelines you can trigger a pipeline in each project, where each
has its own build, test, and deploy process. You can visualize the connected pipelines
in one place, including all cross-project interdependencies.
Multi-project pipelines: Multi-project pipelines:
- Are triggered from another pipeline, but the upstream (triggering) pipeline does - Are triggered from another project's pipeline, but the upstream (triggering) pipeline does
not have much control over the downstream (triggered) pipeline. However, it can not have much control over the downstream (triggered) pipeline. However, it can
choose the ref of the downstream pipeline, and pass CI/CD variables to it. choose the ref of the downstream pipeline, and pass CI/CD variables to it.
- Affect the overall status of the ref of the project it runs in, but does not - Affect the overall status of the ref of the project it runs in, but does not
@ -46,75 +72,86 @@ Multi-project pipelines:
that happened to be triggered by an external project. They are all visible on the pipeline index page. that happened to be triggered by an external project. They are all visible on the pipeline index page.
- Are independent, so there are no nesting limits. - Are independent, so there are no nesting limits.
## Multi-project pipelines Learn more in the "Cross-project Pipeline Triggering and Visualization" demo at
[GitLab@learn](https://about.gitlab.com/learn/), in the Continuous Integration section.
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8. If you use a public project to trigger downstream pipelines in a private project,
make sure there are no confidentiality problems. The upstream project's pipelines page
always displays:
You can set up [GitLab CI/CD](../index.md) across multiple projects, so that a pipeline - The name of the downstream project.
in one project can trigger a downstream pipeline in another project. You can visualize the entire pipeline
in one place, including all cross-project interdependencies.
For example, you might deploy your web application from three different projects in GitLab.
Each project has its own build, test, and deploy process. With multi-project pipelines you can
visualize the entire pipeline, including all build and test stages for all three projects.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see the [Multi-project pipelines demo](https://www.youtube.com/watch?v=g_PIwBM1J84).
Multi-project pipelines are also useful for larger products that require cross-project interdependencies, like those
with a [microservices architecture](https://about.gitlab.com/blog/2016/08/16/trends-in-version-control-land-microservices/).
Learn more in the [Cross-project Pipeline Triggering and Visualization demo](https://about.gitlab.com/learn/)
at GitLab@learn, in the Continuous Integration section.
If you trigger a pipeline in a downstream private project, on the upstream project's pipelines page,
you can view:
- The name of the project.
- The status of the pipeline. - The status of the pipeline.
If you have a public project that can trigger downstream pipelines in a private project, ## Trigger a downstream pipeline from a job in the `.gitlab-ci.yml` file
make sure there are no confidentiality problems.
### Trigger a multi-project pipeline from a job in your `.gitlab-ci.yml` file Use the [`trigger`](../yaml/index.md#trigger) keyword in your `.gitlab-ci.yml` file
to create a job that triggers a downstream pipeline. This job is called a trigger job.
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8. After the trigger job starts, the initial status of the job is `pending` while GitLab
attempts to create the downstream pipeline. If the downstream pipeline is created,
GitLab marks the job as passed, otherwise the job failed. Alternatively,
you can [set the trigger job to show the downstream pipeline's status](#mirror-the-status-of-a-downstream-pipeline-in-the-trigger-job)
instead.
When you use the [`trigger`](../yaml/index.md#trigger) keyword to create a multi-project For example:
pipeline in your `.gitlab-ci.yml` file, you create what is called a *trigger job*. For example:
::Tabs
:::TabTitle Multi-project pipeline
```yaml ```yaml
rspec: trigger_job:
stage: test trigger:
script: bundle exec rspec project: project-group/my-downstream-project
staging:
variables:
ENVIRONMENT: staging
stage: deploy
trigger: my/deployment
``` ```
In this example, after the `rspec` job succeeds in the `test` stage, :::TabTitle Parent-child pipeline
the `staging` trigger job starts. The initial status of this
job is `pending`.
GitLab then creates a downstream pipeline in the ```yaml
`my/deployment` project and, as soon as the pipeline is created, the trigger_job:
`staging` job succeeds. The full path to the project is `my/deployment`. trigger:
include:
- local: path/to/child-pipeline.yml
```
You can view the status for the pipeline, or you can display ::EndTabs
[the downstream pipeline's status instead](#mirror-the-status-of-a-downstream-pipeline-in-the-trigger-job).
The user that creates the upstream pipeline must be able to create pipelines in the ### Use `rules` to control downstream pipeline jobs
downstream project (`my/deployment`) too. If the downstream project is not found,
or the user does not have [permission](../../user/permissions.md) to create a pipeline there,
the `staging` job is marked as _failed_.
#### Specify a downstream pipeline branch You can use CI/CD variables or the [`rules`](../yaml/index.md#rulesif) keyword to
[control job behavior](../jobs/job_control.md) for downstream pipelines.
You can specify a branch name for the downstream pipeline to use. When a downstream pipeline is triggered with the [`trigger`](../yaml/index.md#trigger) keyword,
GitLab uses the commit on the head of the branch to the value of the [`$CI_PIPELINE_SOURCE` predefined variable](../variables/predefined_variables.md)
create the downstream pipeline. for all jobs is:
- `pipeline` for multi-project pipelines.
- `parent` for parent-child pipelines.
For example, with a multi-project pipeline:
```yaml
job1:
rules:
- if: $CI_PIPELINE_SOURCE == "pipeline"
script: echo "This job runs in multi-project pipelines only"
job2:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script: echo "This job runs in merge request pipelines only"
job3:
rules:
- if: $CI_PIPELINE_SOURCE == "pipeline"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script: echo "This job runs in both multi-project and merge request pipelines"
```
### Specify a branch for multi-project pipelines
You can specify a branch name for a multi-project pipeline to use. GitLab uses
the commit on the head of the branch to create the downstream pipeline:
```yaml ```yaml
rspec: rspec:
@ -137,112 +174,11 @@ Use:
In [GitLab 12.4 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/10126), variable expansion is In [GitLab 12.4 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/10126), variable expansion is
supported. supported.
Pipelines triggered on a protected branch in a downstream project use the [role](../../user/permissions.md) ### Use a child pipeline configuration file in a different project
of the user that ran the trigger job in the upstream project. If the user does not
have permission to run CI/CD pipelines against the protected branch, the pipeline fails. See
[pipeline security for protected branches](index.md#pipeline-security-on-protected-branches).
#### Use `rules` or `only`/`except` with multi-project pipelines > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205157) in GitLab 13.5.
You can use CI/CD variables or the [`rules`](../yaml/index.md#rulesif) keyword to You can use [`include:file`](../yaml/index.md#includefile) to trigger child pipelines
[control job behavior](../jobs/job_control.md) for multi-project pipelines. When a
downstream pipeline is triggered with the [`trigger`](../yaml/index.md#trigger) keyword,
the value of the [`$CI_PIPELINE_SOURCE` predefined variable](../variables/predefined_variables.md)
is `pipeline` for all its jobs.
If you use [`only/except`](../yaml/index.md#only--except) to control job behavior, use the
[`pipelines`](../yaml/index.md#onlyrefs--exceptrefs) keyword.
### Trigger a multi-project pipeline by using the API
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/31573) to GitLab Free in 12.4.
When you use the [`CI_JOB_TOKEN` to trigger pipelines](../jobs/ci_job_token.md),
GitLab recognizes the source of the job token. The pipelines become related,
so you can visualize their relationships on pipeline graphs.
These relationships are displayed in the pipeline graph by showing inbound and
outbound connections for upstream and downstream pipeline dependencies.
When using:
- CI/CD variables or [`rules`](../yaml/index.md#rulesif) to control job behavior, the value of
the [`$CI_PIPELINE_SOURCE` predefined variable](../variables/predefined_variables.md) is
`pipeline` for multi-project pipeline triggered through the API with `CI_JOB_TOKEN`.
- [`only/except`](../yaml/index.md#only--except) to control job behavior, use the
`pipelines` keyword.
## Parent-child pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16094) in GitLab 12.7.
As pipelines grow more complex, a few related problems start to emerge:
- The staged structure, where all steps in a stage must be completed before the first
job in next stage begins, causes arbitrary waits, slowing things down.
- Configuration for the single global pipeline becomes very long and complicated,
making it hard to manage.
- Imports with [`include`](../yaml/index.md#include) increase the complexity of the configuration, and create the potential
for namespace collisions where jobs are unintentionally duplicated.
- Pipeline UX can become unwieldy with so many jobs and stages to work with.
Additionally, sometimes the behavior of a pipeline needs to be more dynamic. The ability
to choose to start sub-pipelines (or not) is a powerful ability, especially if the
YAML is dynamically generated.
![Parent pipeline graph expanded](img/parent_pipeline_graph_expanded_v14_3.png)
Similarly to [multi-project pipelines](#multi-project-pipelines), a pipeline can trigger a
set of concurrently running downstream child pipelines, but in the same project:
- Child pipelines still execute each of their jobs according to a stage sequence, but
would be free to continue forward through their stages without waiting for unrelated
jobs in the parent pipeline to finish.
- The configuration is split up into smaller child pipeline configurations. Each child pipeline contains only relevant steps which are
easier to understand. This reduces the cognitive load to understand the overall configuration.
- Imports are done at the child pipeline level, reducing the likelihood of collisions.
Child pipelines work well with other GitLab CI/CD features:
- Use [`rules: changes`](../yaml/index.md#ruleschanges) to trigger pipelines only when
certain files change. This is useful for monorepos, for example.
- Since the parent pipeline in `.gitlab-ci.yml` and the child pipeline run as normal
pipelines, they can have their own behaviors and sequencing in relation to triggers.
See the [`trigger`](../yaml/index.md#trigger) keyword documentation for full details on how to
include the child pipeline configuration.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Parent-Child Pipelines feature demo](https://youtu.be/n8KpBSqZNbk).
NOTE:
The artifact containing the generated YAML file must not be [larger than 5MB](https://gitlab.com/gitlab-org/gitlab/-/issues/249140).
### Trigger a parent-child pipeline
The simplest case is [triggering a child pipeline](../yaml/index.md#trigger) using a
local YAML file to define the pipeline configuration. In this case, the parent pipeline
triggers the child pipeline, and continues without waiting:
```yaml
microservice_a:
trigger:
include: path/to/microservice_a.yml
```
You can include multiple files when defining a child pipeline. The child pipeline's
configuration is composed of all configuration files merged together:
```yaml
microservice_a:
trigger:
include:
- local: path/to/microservice_a.yml
- template: Security/SAST.gitlab-ci.yml
```
In [GitLab 13.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/205157),
you can use [`include:file`](../yaml/index.md#includefile) to trigger child pipelines
with a configuration file in a different project: with a configuration file in a different project:
```yaml ```yaml
@ -254,119 +190,150 @@ microservice_a:
file: '/path/to/child-pipeline.yml' file: '/path/to/child-pipeline.yml'
``` ```
The maximum number of entries that are accepted for `trigger:include` is three. ### Combine multiple child pipeline configuration files
### Merge request child pipelines You can include up to three configuration files when defining a child pipeline. The child pipeline's
configuration is composed of all configuration files merged together:
To trigger a child pipeline as a [merge request pipeline](merge_request_pipelines.md) we need to:
- Set the trigger job to run on merge requests:
```yaml ```yaml
# parent .gitlab-ci.yml
microservice_a: microservice_a:
trigger: trigger:
include: path/to/microservice_a.yml include:
rules: - local: path/to/microservice_a.yml
- if: $CI_MERGE_REQUEST_ID - template: Security/SAST.gitlab-ci.yml
- project: 'my-group/my-pipeline-library'
ref: 'main'
file: '/path/to/child-pipeline.yml'
``` ```
- Configure the child pipeline by either:
- Setting all jobs in the child pipeline to evaluate in the context of a merge request:
```yaml
# child path/to/microservice_a.yml
workflow:
rules:
- if: $CI_MERGE_REQUEST_ID
job1:
script: ...
job2:
script: ...
```
- Alternatively, setting the rule per job. For example, to create only `job1` in
the context of merge request pipelines:
```yaml
# child path/to/microservice_a.yml
job1:
script: ...
rules:
- if: $CI_MERGE_REQUEST_ID
job2:
script: ...
```
### Dynamic child pipelines ### Dynamic child pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35632) in GitLab 12.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35632) in GitLab 12.9.
Instead of running a child pipeline from a static YAML file, you can define a job that runs You can trigger a child pipeline from a YAML file generated in a job, instead of a
your own script to generate a YAML file, which is then used to trigger a child pipeline. static file saved in your project. This technique can be very powerful for generating pipelines
targeting content that changed or to build a matrix of targets and architectures.
This technique can be very powerful in generating pipelines targeting content that changed or to The artifact containing the generated YAML file must not be [larger than 5MB](https://gitlab.com/gitlab-org/gitlab/-/issues/249140).
build a matrix of targets and architectures.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Create child pipelines using dynamically generated configurations](https://youtu.be/nMdfus2JWHM). For an overview, see [Create child pipelines using dynamically generated configurations](https://youtu.be/nMdfus2JWHM).
We also have an example project using For an example project that generates a dynamic child pipeline, see
[Dynamic Child Pipelines with Jsonnet](https://gitlab.com/gitlab-org/project-templates/jsonnet) [Dynamic Child Pipelines with Jsonnet](https://gitlab.com/gitlab-org/project-templates/jsonnet).
which shows how to use a data templating language to generate your `.gitlab-ci.yml` at runtime. This project shows how to use a data templating language to generate your `.gitlab-ci.yml` at runtime.
You could use a similar process for other templating languages like You can use a similar process for other templating languages like
[Dhall](https://dhall-lang.org/) or [ytt](https://get-ytt.io/). [Dhall](https://dhall-lang.org/) or [ytt](https://get-ytt.io/).
#### Trigger a dynamic child pipeline
To trigger a child pipeline from a dynamically generated configuration file:
1. Generate the configuration file in a job and save it as an [artifact](../yaml/index.md#artifactspaths):
```yaml
generate-config:
stage: build
script: generate-ci-config > generated-config.yml
artifacts:
paths:
- generated-config.yml
```
1. Configure the trigger job to run after the job that generated the configuration file,
and set `include: artifact` to the generated artifact:
```yaml
child-pipeline:
stage: test
trigger:
include:
- artifact: generated-config.yml
job: generate-config
```
In this example, `generated-config.yml` is extracted from the artifacts and used as the configuration
for triggering the child pipeline.
The artifact path is parsed by GitLab, not the runner, so the path must match the The artifact path is parsed by GitLab, not the runner, so the path must match the
syntax for the OS running GitLab. If GitLab is running on Linux but using a Windows syntax for the OS running GitLab. If GitLab is running on Linux but using a Windows
runner for testing, the path separator for the trigger job would be `/`. Other CI/CD runner for testing, the path separator for the trigger job is `/`. Other CI/CD
configuration for jobs, like scripts, that use the Windows runner would use `\`. configuration for jobs that use the Windows runner, like scripts, use `\`.
For example, to trigger a child pipeline from a dynamically generated configuration file: ### Run child pipelines with merge request pipelines
To trigger a child pipeline as a [merge request pipeline](merge_request_pipelines.md):
1. Set the trigger job to run on merge requests:
```yaml
# parent .gitlab-ci.yml
microservice_a:
trigger:
include: path/to/microservice_a.yml
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```
1. Configure the child pipeline jobs to run in merge request pipelines:
- With [`workflow:rules`](../yaml/index.md#workflowrules):
```yaml
# child path/to/microservice_a.yml
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
job1:
script: ...
job2:
script: ...
```
- By configuring [rules](../yaml/index.md#rules) for each job:
```yaml
# child path/to/microservice_a.yml
job1:
script: ...
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
job2:
script: ...
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```
## Trigger a multi-project pipeline by using the API
You can use the [CI/CD job token (`CI_JOB_TOKEN`)](../jobs/ci_job_token.md) with the
[pipeline trigger API endpoint](../../api/pipeline_triggers.md#trigger-a-pipeline-with-a-token)
to trigger multi-project pipelines from a CI/CD job. GitLab recognizes the source of the job token
and marks the pipelines as related. In the pipeline graph, the relationships are displayed
as inbound and outbound connections for upstream and downstream pipeline dependencies.
For example:
```yaml ```yaml
generate-config: trigger_pipeline:
stage: build stage: deploy
script: generate-ci-config > generated-config.yml script:
artifacts: - curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=main "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
paths: rules:
- generated-config.yml - if: $CI_COMMIT_TAG
environment: production
child-pipeline:
stage: test
trigger:
include:
- artifact: generated-config.yml
job: generate-config
``` ```
The `generated-config.yml` is extracted from the artifacts and used as the configuration
for triggering the child pipeline.
In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail.
This is [resolved](https://gitlab.com/gitlab-org/gitlab/-/issues/209070) in GitLab 12.10.
### Nested child pipelines
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29651) in GitLab 13.4.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243747) in GitLab 13.5.
Parent and child pipelines were introduced with a maximum depth of one level of child
pipelines, which was later increased to two. A parent pipeline can trigger many child
pipelines, and these child pipelines can trigger their own child pipelines. It's not
possible to trigger another level of child pipelines.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Nested Dynamic Pipelines](https://youtu.be/C5j3ju9je2M).
## View a downstream pipeline ## View a downstream pipeline
> Hover behavior for pipeline cards [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/197140/) in GitLab 13.2.
In the [pipeline graph view](index.md#view-full-pipeline-graph), downstream pipelines display In the [pipeline graph view](index.md#view-full-pipeline-graph), downstream pipelines display
as a list of cards on the right of the graph. as a list of cards on the right of the graph. Hover over the pipeline's card to view
which job triggered the downstream pipeline.
### Retry a downstream pipeline ### Retry a downstream pipeline
@ -390,9 +357,6 @@ To cancel a downstream pipeline that is still running, select **Cancel** (**{can
### Mirror the status of a downstream pipeline in the trigger job ### Mirror the status of a downstream pipeline in the trigger job
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in GitLab Premium 12.3.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8.
You can mirror the pipeline status from the triggered pipeline to the source trigger job You can mirror the pipeline status from the triggered pipeline to the source trigger job
by using [`strategy: depend`](../yaml/index.md#triggerstrategy): by using [`strategy: depend`](../yaml/index.md#triggerstrategy):
@ -549,8 +513,9 @@ The `ENVIRONMENT` variable is passed to every job defined in a downstream
pipeline. It is available as a variable when GitLab Runner picks a job. pipeline. It is available as a variable when GitLab Runner picks a job.
In the following configuration, the `MY_VARIABLE` variable is passed to the downstream pipeline In the following configuration, the `MY_VARIABLE` variable is passed to the downstream pipeline
that is created when the `trigger-downstream` job is queued. This is because `trigger-downstream` that is created when the `trigger-downstream` job is queued. This behavior is because `trigger-downstream`
job inherits variables declared in global variables blocks, and then we pass these variables to a downstream pipeline. job inherits variables declared in [global `variables`](../yaml/index.md#variables) blocks,
and then GitLab passes these variables to the downstream pipeline.
```yaml ```yaml
variables: variables:
@ -562,7 +527,7 @@ trigger-downstream:
trigger: my/project trigger: my/project
``` ```
### Prevent global variables from being passed #### Prevent global variables from being passed
You can stop global variables from reaching the downstream pipeline by using the [`inherit:variables` keyword](../yaml/index.md#inheritvariables). You can stop global variables from reaching the downstream pipeline by using the [`inherit:variables` keyword](../yaml/index.md#inheritvariables).
For example, in a [multi-project pipeline](#multi-project-pipelines): For example, in a [multi-project pipeline](#multi-project-pipelines):
@ -645,3 +610,16 @@ For example, in a [multi-project pipeline](#multi-project-pipelines):
ref: master ref: master
artifacts: true artifacts: true
``` ```
## Troubleshooting
### Trigger job fails and does not create multi-project pipeline
With multi-project pipelines, the trigger job fails and does not create the downstream pipeline if:
- The downstream project is not found.
- The user that creates the upstream pipeline does not have [permission](../../user/permissions.md)
to create pipelines in the downstream project.
- The downstream pipeline targets a protected branch and the user does not have permission
to run pipelines against the protected branch. See [pipeline security for protected branches](index.md#pipeline-security-on-protected-branches)
for more information.

View file

@ -10,15 +10,21 @@ type: reference
Pipelines are the fundamental building blocks for CI/CD in GitLab. This page documents Pipelines are the fundamental building blocks for CI/CD in GitLab. This page documents
some of the important concepts related to them. some of the important concepts related to them.
There are three main ways to structure your pipelines, each with their You can structure your pipelines with different methods, each with their
own advantages. These methods can be mixed and matched if needed: own advantages. These methods can be mixed and matched if needed:
- [Basic](#basic-pipelines): Good for straightforward projects where all the configuration is in one easy to find place. - [Basic](#basic-pipelines): Good for straightforward projects where all the configuration is in one easy to find place.
- [Directed Acyclic Graph](#directed-acyclic-graph-pipelines): Good for large, complex projects that need efficient execution. - [Directed Acyclic Graph](#directed-acyclic-graph-pipelines): Good for large, complex projects that need efficient execution.
- [Child/Parent Pipelines](#child--parent-pipelines): Good for monorepos and projects with lots of independently defined components. - [Parent-child pipelines](#parent-child-pipelines): Good for monorepos and projects with lots of independently defined components.
For more details about <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
any of the keywords used below, check out our [CI YAML reference](../yaml/index.md) for details. For an overview, see the [Parent-Child Pipelines feature demo](https://youtu.be/n8KpBSqZNbk).
- [Multi-project pipelines](downstream_pipelines.md#multi-project-pipelines): Good for larger products that require cross-project interdependencies,
like those with a [microservices architecture](https://about.gitlab.com/blog/2016/08/16/trends-in-version-control-land-microservices/).
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see the [Multi-project pipelines demo](https://www.youtube.com/watch?v=g_PIwBM1J84).
## Basic Pipelines ## Basic Pipelines
@ -163,12 +169,29 @@ deploy_b:
environment: production environment: production
``` ```
## Child / Parent Pipelines ## Parent-child pipelines
In the examples above, it's clear we've got two types of things that could be built independently. As pipelines grow more complex, a few related problems start to emerge:
This is an ideal case for using [Child / Parent Pipelines](downstream_pipelines.md#parent-child-pipelines)) via
the [`trigger` keyword](../yaml/index.md#trigger). It separates out the configuration - The staged structure, where all steps in a stage must complete before the first
into multiple files, keeping things very simple. You can also combine this with: job in next stage begins, causes waits that slow things down.
- Configuration for the single global pipeline becomes
hard to manage.
- Imports with [`include`](../yaml/index.md#include) increase the complexity of the configuration, and can cause
namespace collisions where jobs are unintentionally duplicated.
- Pipeline UX has too many jobs and stages to work with.
Additionally, sometimes the behavior of a pipeline needs to be more dynamic. The ability
to choose to start sub-pipelines (or not) is a powerful ability, especially if the
YAML is dynamically generated.
![Parent pipeline graph expanded](img/parent_pipeline_graph_expanded_v14_3.png)
In the [basic pipeline](#basic-pipelines) and [directed acyclic graph](#directed-acyclic-graph-pipelines)
examples above, there are two packages that could be built independently.
These cases are ideal for using [parent-child pipelines](downstream_pipelines.md#parent-child-pipelines).
It separates out the configuration into multiple files, keeping things simpler.
You can combine parent-child pipelines with:
- The [`rules` keyword](../yaml/index.md#rules): For example, have the child pipelines triggered only - The [`rules` keyword](../yaml/index.md#rules): For example, have the child pipelines triggered only
when there are changes to that area. when there are changes to that area.

View file

@ -3978,7 +3978,7 @@ trigger-multi-project-pipeline:
**Related topics**: **Related topics**:
- [Multi-project pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-from-a-job-in-your-gitlab-ciyml-file). - [Multi-project pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-downstream-pipeline-from-a-job-in-the-gitlab-ciyml-file).
- To run a pipeline for a specific branch, tag, or commit, you can use a [trigger token](../triggers/index.md) - To run a pipeline for a specific branch, tag, or commit, you can use a [trigger token](../triggers/index.md)
to authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md). to authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md).
The trigger token is different than the `trigger` keyword. The trigger token is different than the `trigger` keyword.
@ -4006,7 +4006,7 @@ trigger-child-pipeline:
**Related topics**: **Related topics**:
- [Child pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-parent-child-pipeline). - [Child pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-downstream-pipeline-from-a-job-in-the-gitlab-ciyml-file).
#### `trigger:project` #### `trigger:project`
@ -4042,7 +4042,7 @@ trigger-multi-project-pipeline:
**Related topics**: **Related topics**:
- [Multi-project pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-from-a-job-in-your-gitlab-ciyml-file). - [Multi-project pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-downstream-pipeline-from-a-job-in-the-gitlab-ciyml-file).
- To run a pipeline for a specific branch, tag, or commit, you can also use a [trigger token](../triggers/index.md) - To run a pipeline for a specific branch, tag, or commit, you can also use a [trigger token](../triggers/index.md)
to authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md). to authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md).
The trigger token is different than the `trigger` keyword. The trigger token is different than the `trigger` keyword.

View file

@ -805,3 +805,45 @@ To find and store an array of groups based on an SQL query in the [rails console
Group.find_by_sql("SELECT * FROM namespaces WHERE name LIKE '%oup'") Group.find_by_sql("SELECT * FROM namespaces WHERE name LIKE '%oup'")
=> [#<Group id:3 @test-group>, #<Group id:4 @template-group/template-subgroup>] => [#<Group id:3 @test-group>, #<Group id:4 @template-group/template-subgroup>]
``` ```
### Transfer subgroup to another location using Rails console
If transferring a group doesn't work through the UI or API, you may want to attempt the transfer in a [Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session):
WARNING:
Any command that changes data directly could be damaging if not run correctly, or under the right conditions. We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
```ruby
user = User.find_by_username('<username>')
group = Group.find_by_name("<group_name>")
## Set parent_group = nil to make the subgroup a top-level group
parent_group = Group.find_by(id: "<group_id>")
service = ::Groups::TransferService.new(group, user)
service.execute(parent_group)
```
### Find groups pending deletion using Rails console
If you need to find all the groups that are pending deletion, you can use the following command in a [Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session):
```ruby
Group.all.each do |g|
if g.marked_for_deletion?
puts "Group ID: #{g.id}"
puts "Group name: #{g.name}"
puts "Group path: #{g.full_path}"
end
end
```
### Delete a group using Rails console
At times, a group deletion may get stuck. If needed, in a [Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session),
you can attempt to delete a group using the following command:
WARNING:
Any command that changes data directly could be damaging if not run correctly, or under the right conditions. We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
```ruby
GroupDestroyWorker.new.perform(group_id, user_id)
```

View file

@ -0,0 +1,139 @@
---
stage: Create
group: Editor
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
---
# Remote Development **(FREE)**
DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
Please do not rely on this information for purchasing or planning purposes.
As with all projects, the items mentioned on this page are subject to change or delay.
The development, release, and timing of any products, features, or functionality remain at the
sole discretion of GitLab Inc.
You can use the [Web IDE](../web_ide/index.md) to commit changes to a project directly from your web browser without installing any dependencies or cloning any repositories. The Web IDE, however, lacks a native runtime environment on which you would compile code, run tests, or generate real-time feedback in the IDE. For a more complete IDE experience, you can pair the Web IDE with a Remote Development environment that has been properly configured to run as a host.
## Connect a remote machine to the Web IDE
Prerequisites:
- A remote virtual machine with root access
- A domain address resolving to that machine
- Docker installation
To connect a remote machine to the Web IDE, you must:
1. [Generate Let's Encrypt certificates](#generate-lets-encrypt-certificates).
1. [Connect a development environment to the Web IDE](#connect-a-development-environment-to-the-web-ide).
### Generate Let's Encrypt certificates
To generate Let's Encrypt certificates:
1. [Point a domain to your remote machine](#point-a-domain-to-your-remote-machine).
1. [Install Certbot](#install-certbot).
1. [Generate the certificates](#generate-the-certificates).
#### Point a domain to your remote machine
To point a domain to your remote machine, create an `A` record from `example.remote.gitlab.dev` to `1.2.3.4`.
#### Install Certbot
[Certbot](https://certbot.eff.org/) is a free and open-source software tool that automatically uses Let's Encrypt certificates on manually administrated websites to enable HTTPS.
To install Certbot, run the following command:
```shell
sudo apt-get update
sudo apt-get install certbot
```
#### Generate the certificates
```shell
export EMAIL="YOUR_EMAIL@example.com"
export DOMAIN="example.remote.gitlab.dev"
certbot -d "${DOMAIN}" \
-m "${EMAIL}" \
--config-dir ~/.certbot/config \
--logs-dir ~/.certbot/logs \
--work-dir ~/.certbot/work \
--manual \
--preferred-challenges dns certonly
```
### Connect a development environment to the Web IDE
To connect a development environment to the Web IDE:
1. [Create a development environment](#manage-a-development-environment).
1. [Fetch a token](#fetch-a-token).
1. [Connect to the Web IDE](#connect-to-the-web-ide).
#### Manage a development environment
**Create a development environment**
```shell
export CERTS_DIR="/home/ubuntu/.certbot/config/live/${DOMAIN}"
export PROJECTS_DIR="/home/ubuntu"
docker run -d \
--name my-environment \
-p 3443:3443 \
-v "${CERTS_DIR}/fullchain.pem:/gitlab-rd-web-ide/certs/fullchain.pem" \
-v "${CERTS_DIR}/privkey.pem:/gitlab-rd-web-ide/certs/privkey.pem" \
-v "${PROJECTS_DIR}:/projects" \
registry.gitlab.com/gitlab-com/create-stage/editor-poc/remote-development/gitlab-rd-web-ide-docker:0.1 \
--log-level warn --domain "${DOMAIN}" --ignore-version-mismatch
```
The new development environment starts automatically.
**Stop a development environment**
```shell
docker container stop my-environment
```
**Start a development environment**
```shell
docker container start my-environment
```
The token changes every time you restart the development environment.
**Remove a development environment**
To remove a development environment:
1. Stop the development environment.
1. Run the following command:
```shell
docker container rm my-environment
```
#### Fetch a token
```shell
docker exec my-environment cat TOKEN
```
#### Connect to the Web IDE
To connect to the Web IDE:
1. Run the following command:
```shell
echo "https://gitlab-org.gitlab.io/gitlab-web-ide?remoteHost=${DOMAIN}:3443&hostPath=/projects"
```
1. Go to that URL and enter the [token you fetched](#fetch-a-token).

View file

@ -559,3 +559,33 @@ If this fails, display why it doesn't work with:
project = Project.find_by_full_path('<project_path>') project = Project.find_by_full_path('<project_path>')
project.delete_error project.delete_error
``` ```
### Toggle a feature for all projects within a group
While toggling a feature in a project can be done through the [projects API](../../api/projects.md),
you may need to do this for a large number of projects.
To toggle a specific feature, you can [start a Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session)
and run the following function:
WARNING:
Any command that changes data directly could be damaging if not run correctly, or under the right conditions. We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case.
```ruby
projects = Group.find_by_name('_group_name').projects
projects.each do |p|
## replace <feature-name> with the appropriate feature name in all instances
state = p.<feature-name>
if state != 0
puts "#{p.name} has <feature-name> already enabled. Skipping..."
else
puts "#{p.name} didn't have <feature-name> enabled. Enabling..."
p.project_feature.update!(<feature-name>: ProjectFeature::PRIVATE)
end
end
```
To find features that can be toggled, run `pp p.project_feature`.
Available permission levels are listed in
[concerns/featurable.rb](https://gitlab.com/gitlab-org/gitlab/blob/master/app/models/concerns/featurable.rb).

View file

@ -2,10 +2,10 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'gitlab-qa', '~> 8', '>= 8.7.0', require: 'gitlab/qa' gem 'gitlab-qa', '~> 8', '>= 8.8.0', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.18.0' gem 'allure-rspec', '~> 2.18.0'
gem 'capybara', '~> 3.35.0' gem 'capybara', '~> 3.37.1'
gem 'capybara-screenshot', '~> 1.0.26' gem 'capybara-screenshot', '~> 1.0.26'
gem 'rake', '~> 13' gem 'rake', '~> 13'
gem 'rspec', '~> 3.11' gem 'rspec', '~> 3.11'

View file

@ -27,8 +27,9 @@ GEM
binding_ninja (0.2.3) binding_ninja (0.2.3)
builder (3.2.4) builder (3.2.4)
byebug (11.1.3) byebug (11.1.3)
capybara (3.35.3) capybara (3.37.1)
addressable addressable
matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
rack (>= 1.6.0) rack (>= 1.6.0)
@ -99,14 +100,15 @@ GEM
gitlab (4.18.0) gitlab (4.18.0)
httparty (~> 0.18) httparty (~> 0.18)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
gitlab-qa (8.7.0) gitlab-qa (8.8.0)
activesupport (~> 6.1) activesupport (~> 6.1)
gitlab (~> 4.18.0) gitlab (~> 4.18.0)
http (~> 5.0) http (~> 5.0)
nokogiri (~> 1.10) nokogiri (~> 1.10)
rainbow (~> 3.0.0) rainbow (>= 3, < 4)
table_print (= 1.5.7) table_print (= 1.5.7)
zeitwerk (~> 2.4) toxiproxy (~> 2.0.2)
zeitwerk (>= 2, < 3)
google-apis-compute_v1 (0.51.0) google-apis-compute_v1 (0.51.0)
google-apis-core (>= 0.7.2, < 2.a) google-apis-core (>= 0.7.2, < 2.a)
google-apis-core (0.9.0) google-apis-core (0.9.0)
@ -165,6 +167,7 @@ GEM
rake (~> 13.0) rake (~> 13.0)
macaddr (1.7.2) macaddr (1.7.2)
systemu (~> 2.6.5) systemu (~> 2.6.5)
matrix (0.4.2)
memoist (0.16.2) memoist (0.16.2)
method_source (1.0.0) method_source (1.0.0)
mime-types (3.4.1) mime-types (3.4.1)
@ -266,6 +269,7 @@ GEM
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
timecop (0.9.5) timecop (0.9.5)
toxiproxy (2.0.2)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
tzinfo (2.0.5) tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@ -300,7 +304,7 @@ DEPENDENCIES
activesupport (~> 6.1.4.7) activesupport (~> 6.1.4.7)
airborne (~> 0.3.7) airborne (~> 0.3.7)
allure-rspec (~> 2.18.0) allure-rspec (~> 2.18.0)
capybara (~> 3.35.0) capybara (~> 3.37.1)
capybara-screenshot (~> 1.0.26) capybara-screenshot (~> 1.0.26)
chemlab (~> 0.10) chemlab (~> 0.10)
chemlab-library-www-gitlab-com (~> 0.1) chemlab-library-www-gitlab-com (~> 0.1)
@ -310,7 +314,7 @@ DEPENDENCIES
faraday-retry (~> 2.0) faraday-retry (~> 2.0)
fog-core (= 2.1.0) fog-core (= 2.1.0)
fog-google (~> 1.19) fog-google (~> 1.19)
gitlab-qa (~> 8, >= 8.7.0) gitlab-qa (~> 8, >= 8.8.0)
influxdb-client (~> 1.17) influxdb-client (~> 1.17)
knapsack (~> 4.0) knapsack (~> 4.0)
nokogiri (~> 1.13, >= 1.13.9) nokogiri (~> 1.13, >= 1.13.9)

View file

@ -770,18 +770,18 @@
# responsibility of unit tests. These tests are about the structure of the HTML. # responsibility of unit tests. These tests are about the structure of the HTML.
uri_substitution: *uri_substitution uri_substitution: *uri_substitution
data_attribute_id_substitution: data_attribute_id_substitution:
- regex: '(data-user|data-project|data-issue|data-iid|data-merge-request|data-milestone)(=")(\d+?)(")' - regex: '(data-user|data-project|data-issue|data-iid|data-merge-request|data-milestone|data-label)(=")(\d+?)(")'
replacement: '\1\2ID\4' replacement: '\1\2ID\4'
text_attribute_substitution: text_attribute_substitution:
- regex: '(title)(=")(.+?)(")' - regex: '(title)(=")([^"]*)(")'
replacement: '\1\2TEXT\4' replacement: '\1\2TEXT\4'
path_attribute_id_substitution: path_attribute_id_substitution:
- regex: '(group|project)(\d+)' - regex: '(group|project)(\d+)'
replacement: '\1ID' replacement: '\1ID'
markdown: |- markdown: |-
Hi @gfm_user - thank you for reporting this bug (#1) we hope to fix it in %1.1 as part of !1 Hi @gfm_user - thank you for reporting this ~"UX bug" (#1) we hope to fix it in %1.1 as part of !1
html: |- html: |-
<p data-sourcepos="1:1-1:92" dir="auto">Hi <a href="/gfm_user" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="John Doe1">@gfm_user</a> - thank you for reporting this bug (<a href="/group1/project1/-/issues/1" data-reference-type="issue" data-original="#1" data-link="false" data-link-reference="false" data-project="11" data-issue="11" data-project-path="group1/project1" data-iid="1" data-issue-type="issue" data-container="body" data-placement="top" title="My title 1" class="gfm gfm-issue">#1</a>) we hope to fix it in <a href="/group1/project1/-/milestones/1" data-reference-type="milestone" data-original="%1.1" data-link="false" data-link-reference="false" data-project="11" data-milestone="11" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%1.1</a> as part of <a href="/group1/project1/-/merge_requests/1" data-reference-type="merge_request" data-original="!1" data-link="false" data-link-reference="false" data-project="11" data-merge-request="11" data-project-path="group1/project1" data-iid="1" data-container="body" data-placement="top" title="My title 2" class="gfm gfm-merge_request">!1</a></p> <p data-sourcepos="1:1-1:98" dir="auto">Hi <a href="/gfm_user" data-reference-type="user" data-user="1" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="John Doe1">@gfm_user</a> - thank you for reporting this <span class="gl-label gl-label-sm"><a href="/groupID/projectID/-/issues?label_name=UX+bug" data-reference-type="label" data-original='~"UX bug"' data-link="false" data-link-reference="false" data-project="ID" data-label="2" data-container="body" data-placement="top" title="TEXT" class="gfm gfm-label has-tooltip gl-link gl-label-link"><span class="gl-label-text gl-label-text-light" data-container="body" data-html="true" style="background-color: #990000">UX bug</span></a></span> (<a href="/group1/project1/-/issues/1" data-reference-type="issue" data-original="#1" data-link="false" data-link-reference="false" data-project="11" data-issue="11" data-project-path="group1/project1" data-iid="1" data-issue-type="issue" data-container="body" data-placement="top" title="My title 1" class="gfm gfm-issue">#1</a>) we hope to fix it in <a href="/group1/project1/-/milestones/1" data-reference-type="milestone" data-original="%1.1" data-link="false" data-link-reference="false" data-project="11" data-milestone="11" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%1.1</a> as part of <a href="/group1/project1/-/merge_requests/1" data-reference-type="merge_request" data-original="!1" data-link="false" data-link-reference="false" data-project="11" data-merge-request="11" data-project-path="group1/project1" data-iid="1" data-container="body" data-placement="top" title="My title 2" class="gfm gfm-merge_request">!1</a></p>
- name: strike - name: strike
markdown: |- markdown: |-
~~del~~ ~~del~~

View file

@ -21,7 +21,9 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' }; const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' };
const exampleIssue = { iid: 123, title: 'Test Issue' }; const exampleIssue = { iid: 123, title: 'Test Issue' };
const exampleMergeRequest = { iid: 224, title: 'Test MR' }; const exampleMergeRequest = { iid: 224, title: 'Test MR' };
const exampleMilestone = { iid: 21, title: '1.3' }; const exampleMilestone1 = { iid: 21, title: '13' };
const exampleMilestone2 = { iid: 24, title: 'Milestone with spaces' };
const exampleCommand = { const exampleCommand = {
name: 'due', name: 'due',
description: 'Set due date', description: 'Set due date',
@ -32,7 +34,19 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
title: '❓ Remote Development | Solution validation', title: '❓ Remote Development | Solution validation',
reference: 'gitlab-org&8884', reference: 'gitlab-org&8884',
}; };
const exampleLabel = { const exampleLabel1 = {
title: 'Create',
color: '#E44D2A',
type: 'GroupLabel',
textColor: '#FFFFFF',
};
const exampleLabel2 = {
title: 'Weekly Team Announcement',
color: '#E44D2A',
type: 'GroupLabel',
textColor: '#FFFFFF',
};
const exampleLabel3 = {
title: 'devops::create', title: 'devops::create',
color: '#E44D2A', color: '#E44D2A',
type: 'GroupLabel', type: 'GroupLabel',
@ -67,10 +81,13 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
${'reference'} | ${'user'} | ${'@'} | ${exampleUser} | ${`@root`} | ${{}} ${'reference'} | ${'user'} | ${'@'} | ${exampleUser} | ${`@root`} | ${{}}
${'reference'} | ${'issue'} | ${'#'} | ${exampleIssue} | ${`#123`} | ${{}} ${'reference'} | ${'issue'} | ${'#'} | ${exampleIssue} | ${`#123`} | ${{}}
${'reference'} | ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${`!224`} | ${{}} ${'reference'} | ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${`!224`} | ${{}}
${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone} | ${`%1.3`} | ${{}} ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${`%13`} | ${{}}
${'reference'} | ${'command'} | ${'/'} | ${exampleCommand} | ${'/due '} | ${{}} ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone2} | ${`%Milestone with spaces`} | ${{ originalText: '%"Milestone with spaces"' }}
${'reference'} | ${'command'} | ${'/'} | ${exampleCommand} | ${'/due'} | ${{}}
${'reference'} | ${'epic'} | ${'&'} | ${exampleEpic} | ${`gitlab-org&8884`} | ${{}} ${'reference'} | ${'epic'} | ${'&'} | ${exampleEpic} | ${`gitlab-org&8884`} | ${{}}
${'reference'} | ${'label'} | ${'~'} | ${exampleLabel} | ${`~devops::create`} | ${{}} ${'reference'} | ${'label'} | ${'~'} | ${exampleLabel1} | ${`Create`} | ${{}}
${'reference'} | ${'label'} | ${'~'} | ${exampleLabel2} | ${`Weekly Team Announcement`} | ${{ originalText: '~"Weekly Team Announcement"' }}
${'reference'} | ${'label'} | ${'~'} | ${exampleLabel3} | ${`devops::create`} | ${{ originalText: '~"devops::create"', text: 'devops::create' }}
${'reference'} | ${'vulnerability'} | ${'[vulnerability:'} | ${exampleVulnerability} | ${`[vulnerability:60850147]`} | ${{}} ${'reference'} | ${'vulnerability'} | ${'[vulnerability:'} | ${exampleVulnerability} | ${`[vulnerability:60850147]`} | ${{}}
${'reference'} | ${'snippet'} | ${'$'} | ${exampleSnippet} | ${`$2420859`} | ${{}} ${'reference'} | ${'snippet'} | ${'$'} | ${exampleSnippet} | ${`$2420859`} | ${{}}
${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps} ${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps}
@ -130,7 +147,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
referenceType | char | reference | displaysID referenceType | char | reference | displaysID
${'issue'} | ${'#'} | ${exampleIssue} | ${true} ${'issue'} | ${'#'} | ${exampleIssue} | ${true}
${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${true} ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${true}
${'milestone'} | ${'%'} | ${exampleMilestone} | ${false} ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${false}
`('rendering $referenceType references', ({ referenceType, char, reference, displaysID }) => { `('rendering $referenceType references', ({ referenceType, char, reference, displaysID }) => {
it(`displays ${referenceType} ID and title`, () => { it(`displays ${referenceType} ID and title`, () => {
buildWrapper({ buildWrapper({
@ -172,20 +189,26 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
}); });
describe('rendering label references', () => { describe('rendering label references', () => {
it('displays label title and color', () => { it.each`
label | displayedTitle | displayedColor
${exampleLabel1} | ${'Create'} | ${'rgb(228, 77, 42)' /* #E44D2A */}
${exampleLabel2} | ${'Weekly Team Announcement'} | ${'rgb(228, 77, 42)' /* #E44D2A */}
${exampleLabel3} | ${'devops::create'} | ${'rgb(228, 77, 42)' /* #E44D2A */}
`('displays label title and color', ({ label, displayedTitle, displayedColor }) => {
buildWrapper({ buildWrapper({
propsData: { propsData: {
char: '~', char: '~',
nodeProps: { nodeProps: {
referenceType: 'label', referenceType: 'label',
}, },
items: [exampleLabel], items: [label],
}, },
}); });
expect(wrapper.text()).toContain(`${exampleLabel.title}`); expect(wrapper.text()).toContain(displayedTitle);
expect(wrapper.text()).not.toContain('"'); // no quotes in the dropdown list
expect(wrapper.findByTestId('label-color-box').attributes().style).toEqual( expect(wrapper.findByTestId('label-color-box').attributes().style).toEqual(
`background-color: rgb(228, 77, 42);`, // #E44D2A `background-color: ${displayedColor};`,
); );
}); });
}); });

View file

@ -0,0 +1,36 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import LabelWrapper from '~/content_editor/components/wrappers/label.vue';
describe('content/components/wrappers/label', () => {
let wrapper;
const createWrapper = async (node = {}) => {
wrapper = shallowMountExtended(LabelWrapper, {
propsData: { node },
});
};
afterEach(() => {
wrapper.destroy();
});
it("renders a GlLabel with the node's text and color", () => {
createWrapper({ attrs: { color: '#ff0000', text: 'foo bar', originalText: '~"foo bar"' } });
const glLabel = wrapper.findComponent(GlLabel);
expect(glLabel.props()).toMatchObject(
expect.objectContaining({
title: 'foo bar',
backgroundColor: '#ff0000',
}),
);
});
it('renders a scoped label if there is a "::" in the label', () => {
createWrapper({ attrs: { color: '#ff0000', text: 'foo::bar', originalText: '~"foo::bar"' } });
expect(wrapper.findComponent(GlLabel).props().scoped).toBe(true);
});
});

View file

@ -13,6 +13,8 @@ RSpec.shared_context 'API::Markdown Golden Master shared context' do |markdown_y
let_it_be(:project) { create(:project, :public, :repository, group: group) } let_it_be(:project) { create(:project, :public, :repository, group: group) }
let_it_be(:label) { create(:label, project: project, title: 'bug') } let_it_be(:label) { create(:label, project: project, title: 'bug') }
let_it_be(:label2) { create(:label, project: project, title: 'UX bug') }
let_it_be(:milestone) { create(:milestone, project: project, title: '1.1') } let_it_be(:milestone) { create(:milestone, project: project, title: '1.1') }
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) } let_it_be(:merge_request) { create(:merge_request, source_project: project) }