diff --git a/.projections.json.example b/.projections.json.example index dc1b42b917a..5a5c87704c1 100644 --- a/.projections.json.example +++ b/.projections.json.example @@ -1,5 +1,7 @@ { "ee/*": { "type": "ee" }, + "app/*": { "type": "ce" }, + "lib/*": { "type": "ce" }, "config/initializers/*.rb": { "alternate": "spec/initializers/{}_spec.rb", "type": "source" @@ -57,37 +59,40 @@ "type": "source" }, "app/presenters/*.rb": { + "alternate": "spec/app/presenters/{}_spec.rb", "related": "ee/app/presenters/ee/{}.rb", "type": "source" }, "app/serializers/*.rb": { + "alternate": "spec/app/serializers/{}_spec.rb", "related": "ee/app/serializers/ee/{}.rb", "type": "source" }, "app/services/*.rb": { + "alternate": "spec/app/services/{}_spec.rb", "related": "ee/app/services/ee/{}.rb", "type": "source" }, "app/uploaders/*.rb": { + "alternate": "spec/app/uploaders/{}_spec.rb", "related": "ee/app/uploaders/ee/{}.rb", "type": "source" }, "app/validators/*.rb": { + "alternate": "spec/app/validators/{}_spec.rb", "related": "ee/app/validators/ee/{}.rb", "type": "source" }, "app/views/*.rb": { + "alternate": "spec/app/views/{}_spec.rb", "related": "ee/app/views/ee/{}.rb", "type": "source" }, "app/workers/*.rb": { + "alternate": "spec/app/workers/{}_spec.rb", "related": "ee/app/workers/ee/{}.rb", "type": "source" }, - "app/*.rb": { - "alternate": "spec/{}_spec.rb", - "type": "source" - }, "spec/*_spec.rb": { "alternate": "app/{}.rb", "type": "test" @@ -124,8 +129,79 @@ "alternate": "ee/lib/api/{}.rb", "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": { "alternate": "ee/spec/{}_spec.rb", + "related": "app/{}.rb", "type": "source" }, "ee/spec/*_spec.rb": { @@ -136,6 +212,11 @@ "alternate": "ee/spec/lib/{}_spec.rb", "type": "source" }, + "ee/lib/ee/*.rb": { + "alternate": "ee/spec/lib/{}_spec.rb", + "related": "lib/{}.rb", + "type": "source" + }, "ee/spec/lib/*_spec.rb": { "alternate": "ee/lib/{}.rb", "type": "test" @@ -154,16 +235,18 @@ }, "ee/app/assets/javascripts/*.js": { "alternate": "ee/spec/frontend/{}_spec.js", + "related": "app/assets/javascripts/{}.js", "type": "source" }, "ee/app/assets/javascripts/*.vue": { "alternate": "ee/spec/frontend/{}_spec.js", + "related": "app/assets/javascripts/{}.vue", "type": "source" }, "ee/spec/frontend/*_spec.js": { "alternate": ["ee/app/assets/javascripts/{}.vue", "ee/app/assets/javascripts/{}.js"], "type": "test" }, - "*.rb": {"dispatch": "bundle exec rubocop {file}"}, - "*_spec.rb": {"dispatch": "bundle exec rspec {file}"} + "*.rb": { "dispatch": "bundle exec rubocop {file}" }, + "*_spec.rb": { "dispatch": "bundle exec rspec {file}" } } diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue index 043db617452..987b7044272 100644 --- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue +++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue @@ -42,7 +42,7 @@ export default { computed: { isReference() { - return this.nodeType === 'reference'; + return this.nodeType.startsWith('reference'); }, isCommand() { @@ -96,7 +96,7 @@ export default { getText(item) { if (this.isEmoji) return item.e; - switch (this.nodeType === 'reference' && this.nodeProps.referenceType) { + switch (this.isReference && this.nodeProps.referenceType) { case 'user': return `${this.char}${item.username}`; case 'issue': @@ -105,12 +105,13 @@ export default { case 'snippet': return `${this.char}${item.id}`; case 'milestone': - case 'label': return `${this.char}${item.title}`; + case 'label': + return item.title; case 'command': - return `${this.char}${item.name} `; + return `${this.char}${item.name}`; case 'epic': - return `${item.reference}`; + return item.reference; case 'vulnerability': return `[vulnerability:${item.id}]`; default: @@ -119,17 +120,35 @@ export default { }, getProps(item) { + const props = {}; + if (this.isEmoji) { - return { + Object.assign(props, { name: item.name, unicodeVersion: item.u, title: item.d, 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 }) { diff --git a/app/assets/javascripts/content_editor/components/wrappers/label.vue b/app/assets/javascripts/content_editor/components/wrappers/label.vue new file mode 100644 index 00000000000..4206c866032 --- /dev/null +++ b/app/assets/javascripts/content_editor/components/wrappers/label.vue @@ -0,0 +1,34 @@ + + diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js index 9ac8129335e..707beaf1231 100644 --- a/app/assets/javascripts/content_editor/extensions/reference.js +++ b/app/assets/javascripts/content_editor/extensions/reference.js @@ -46,22 +46,10 @@ export default Node.create({ tag: 'a.gfm:not([data-link=true])', priority: PARSE_HTML_PRIORITY_HIGHEST, }, - { - tag: 'span.gl-label', - }, ]; }, renderHTML({ node }) { - return [ - 'a', - { - class: node.attrs.className, - href: '#', - 'data-reference-type': node.attrs.referenceType, - 'data-original': node.attrs.originalText, - }, - node.attrs.text, - ]; + return ['a', { href: '#' }, node.attrs.text]; }, }); diff --git a/app/assets/javascripts/content_editor/extensions/reference_label.js b/app/assets/javascripts/content_editor/extensions/reference_label.js new file mode 100644 index 00000000000..716e191c3d5 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/reference_label.js @@ -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); + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js index b6db7f9d358..8976b9cafee 100644 --- a/app/assets/javascripts/content_editor/extensions/suggestions.js +++ b/app/assets/javascripts/content_editor/extensions/suggestions.js @@ -34,7 +34,10 @@ function createSuggestionPlugin({ tiptapEditor .chain() .focus() - .insertContentAt(range, [{ type: nodeType, attrs: props }]) + .insertContentAt(range, [ + { type: nodeType, attrs: props }, + { type: 'text', text: ' ' }, + ]) .run(); }, @@ -82,7 +85,7 @@ function createSuggestionPlugin({ }, onUpdate(props) { - component.updateProps(props); + component?.updateProps(props); if (!props.clientRect) { return; @@ -100,12 +103,12 @@ function createSuggestionPlugin({ return true; } - return component.ref?.onKeyDown(props); + return component?.ref?.onKeyDown(props); }, onExit() { popup?.[0].destroy(); - component.destroy(); + component?.destroy(); }, }; }, @@ -151,7 +154,7 @@ export default Node.create({ editor: this.editor, char: '~', dataSource: gl.GfmAutoComplete?.dataSources.labels, - nodeType: 'reference', + nodeType: 'reference_label', nodeProps: { referenceType: 'label', }, diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index b02ea8f6b18..0d78390e769 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -43,6 +43,7 @@ import OrderedList from '../extensions/ordered_list'; import Paragraph from '../extensions/paragraph'; import PasteMarkdown from '../extensions/paste_markdown'; import Reference from '../extensions/reference'; +import ReferenceLabel from '../extensions/reference_label'; import ReferenceDefinition from '../extensions/reference_definition'; import Sourcemap from '../extensions/sourcemap'; import Strike from '../extensions/strike'; @@ -132,6 +133,7 @@ export const createContentEditor = ({ Paragraph, PasteMarkdown.configure({ eventHub, renderMarkdown }), Reference, + ReferenceLabel, ReferenceDefinition, Sourcemap, Strike, diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index ba0cad6c91c..c990f6cf0b3 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -33,6 +33,7 @@ import MathInline from '../extensions/math_inline'; import OrderedList from '../extensions/ordered_list'; import Paragraph from '../extensions/paragraph'; import Reference from '../extensions/reference'; +import ReferenceLabel from '../extensions/reference_label'; import ReferenceDefinition from '../extensions/reference_definition'; import Strike from '../extensions/strike'; import Subscript from '../extensions/subscript'; @@ -61,6 +62,7 @@ import { renderHTMLNode, renderContent, renderBulletList, + renderReference, preserveUnchanged, bold, italic, @@ -184,9 +186,8 @@ const defaultSerializerConfig = { [ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item), [OrderedList.name]: preserveUnchanged(renderOrderedList), [Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph), - [Reference.name]: (state, node) => { - state.write(node.attrs.originalText || node.attrs.text); - }, + [Reference.name]: renderReference, + [ReferenceLabel.name]: renderReference, [ReferenceDefinition.name]: preserveUnchanged({ render: (state, node, parent, index, same, sourceMarkdown) => { const nextSibling = parent.maybeChild(index + 1); diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index 2f5b3bced57..5c0cb21075a 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -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) => { return (_, mark) => { const type = /^(\*\*|__|') -group = Group.find_by_name("") -parent_group = Group.find_by(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.? - - if state - puts "#{p.name} has already enabled. Skipping..." - else - puts "#{p.name} didn't have 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 ### Close a merge request diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md index cb5eb1b1370..1fc29d2a654 100644 --- a/doc/api/pipeline_triggers.md +++ b/doc/api/pipeline_triggers.md @@ -153,7 +153,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: " "https://git 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. -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, which is visible on the [pipeline graph](../ci/pipelines/downstream_pipelines.md#view-multi-project-pipelines-in-pipeline-graphs). diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md index fd4bf71bb49..115f6909d2d 100644 --- a/doc/architecture/blueprints/ci_pipeline_components/index.md +++ b/doc/architecture/blueprints/ci_pipeline_components/index.md @@ -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. - **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, isolated, reusable and resolvable. @@ -118,7 +129,7 @@ isolated, reusable and resolvable. - **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. 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 diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index b488a14e874..7282ebb0909 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -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). - [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts). - [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). - [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 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)** > `CI_JOB_TOKEN` for artifacts download with the API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346) in GitLab 9.5. diff --git a/doc/ci/pipelines/downstream_pipelines.md b/doc/ci/pipelines/downstream_pipelines.md index 2c04fe42bec..0b1963e1874 100644 --- a/doc/ci/pipelines/downstream_pipelines.md +++ b/doc/ci/pipelines/downstream_pipelines.md @@ -7,33 +7,59 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Downstream pipelines **(FREE)** 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 - in the same project as the first pipeline. -- A [multi-project pipeline](#multi-project-pipelines), which is a downstream pipeline triggered - in a different project than the first pipeline. +- A [parent-child pipeline](downstream_pipelines.md#parent-child-pipelines) is a downstream pipeline + triggered in the *same* project as the first pipeline. +- A [multi-project pipeline](#multi-project-pipelines) is a downstream pipeline triggered + in a *different* project than the first pipeline. -Parent-child pipelines and multi-project pipelines can sometimes be used for similar purposes, -but there are some key differences. +You can sometimes use parent-child pipelines and multi-project pipelines for similar purposes, +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. -- 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". - 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). - Are automatically canceled if the pipeline is configured with [`interruptible`](../yaml/index.md#interruptible) when a new pipeline is created for the same ref. -- Display only the parent pipelines in the pipeline index page. Child pipelines are - visible when visiting 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`). +- Are not displayed in the pipeline index page. You can only view child pipelines on + their parent pipeline's page. + +### 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. + + +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: -- 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 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 @@ -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. - 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 -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. - - -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 name of the downstream project. - The status of the pipeline. -If you have a public project that can trigger downstream pipelines in a private project, -make sure there are no confidentiality problems. +## Trigger a downstream pipeline from a job in the `.gitlab-ci.yml` file -### 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 -pipeline in your `.gitlab-ci.yml` file, you create what is called a *trigger job*. For example: +For example: + +::Tabs + +:::TabTitle Multi-project pipeline ```yaml -rspec: - stage: test - script: bundle exec rspec - -staging: - variables: - ENVIRONMENT: staging - stage: deploy - trigger: my/deployment +trigger_job: + trigger: + project: project-group/my-downstream-project ``` -In this example, after the `rspec` job succeeds in the `test` stage, -the `staging` trigger job starts. The initial status of this -job is `pending`. +:::TabTitle Parent-child pipeline -GitLab then creates a downstream pipeline in the -`my/deployment` project and, as soon as the pipeline is created, the -`staging` job succeeds. The full path to the project is `my/deployment`. +```yaml +trigger_job: + trigger: + include: + - local: path/to/child-pipeline.yml +``` -You can view the status for the pipeline, or you can display -[the downstream pipeline's status instead](#mirror-the-status-of-a-downstream-pipeline-in-the-trigger-job). +::EndTabs -The user that creates the upstream pipeline must be able to create pipelines in the -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_. +### Use `rules` to control downstream pipeline jobs -#### 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. -GitLab uses the commit on the head of the branch to -create the downstream pipeline. +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) +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 rspec: @@ -137,112 +174,11 @@ Use: In [GitLab 12.4 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/10126), variable expansion is supported. -Pipelines triggered on a protected branch in a downstream project use the [role](../../user/permissions.md) -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 a child pipeline configuration file in a different project -#### 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 -[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. - - -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 +You can use [`include:file`](../yaml/index.md#includefile) to trigger child pipelines with a configuration file in a different project: ```yaml @@ -254,119 +190,150 @@ microservice_a: 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 - -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: +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: ```yaml -# parent .gitlab-ci.yml microservice_a: trigger: - include: path/to/microservice_a.yml - rules: - - if: $CI_MERGE_REQUEST_ID + include: + - local: path/to/microservice_a.yml + - 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 > [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 -your own script to generate a YAML file, which is then used to trigger a child pipeline. +You can trigger a child pipeline from a YAML file generated in a job, instead of a +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 -build a matrix of targets and architectures. +The artifact containing the generated YAML file must not be [larger than 5MB](https://gitlab.com/gitlab-org/gitlab/-/issues/249140). For an overview, see [Create child pipelines using dynamically generated configurations](https://youtu.be/nMdfus2JWHM). -We also have an example project using -[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. -You could use a similar process for other templating languages like +For an example project that generates a dynamic child pipeline, see +[Dynamic Child Pipelines with Jsonnet](https://gitlab.com/gitlab-org/project-templates/jsonnet). +This project shows how to use a data templating language to generate your `.gitlab-ci.yml` at runtime. +You can use a similar process for other templating languages like [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 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 -configuration for jobs, like scripts, that use the Windows runner would use `\`. +runner for testing, the path separator for the trigger job is `/`. Other CI/CD +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 -generate-config: - stage: build - script: generate-ci-config > generated-config.yml - artifacts: - paths: - - generated-config.yml - -child-pipeline: - stage: test - trigger: - include: - - artifact: generated-config.yml - job: generate-config +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 ``` -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. - - -For an overview, see [Nested Dynamic Pipelines](https://youtu.be/C5j3ju9je2M). - ## 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 -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 @@ -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 -> - [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 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. 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` -job inherits variables declared in global variables blocks, and then we pass these variables to a downstream pipeline. +that is created when the `trigger-downstream` job is queued. This behavior is because `trigger-downstream` +job inherits variables declared in [global `variables`](../yaml/index.md#variables) blocks, +and then GitLab passes these variables to the downstream pipeline. ```yaml variables: @@ -562,7 +527,7 @@ trigger-downstream: 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). 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 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. diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md index 9d7d6351ac3..e36eb24055f 100644 --- a/doc/ci/pipelines/pipeline_architectures.md +++ b/doc/ci/pipelines/pipeline_architectures.md @@ -10,15 +10,21 @@ type: reference Pipelines are the fundamental building blocks for CI/CD in GitLab. This page documents 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: - [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. -- [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 -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/). + + + For an overview, see the [Multi-project pipelines demo](https://www.youtube.com/watch?v=g_PIwBM1J84). ## Basic Pipelines @@ -163,12 +169,29 @@ deploy_b: 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. -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 -into multiple files, keeping things very simple. You can also combine this with: +As pipelines grow more complex, a few related problems start to emerge: + +- The staged structure, where all steps in a stage must complete before the first + 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 when there are changes to that area. diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 5a7f6c7d509..e06abe1dc69 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3978,7 +3978,7 @@ trigger-multi-project-pipeline: **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 authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md). The trigger token is different than the `trigger` keyword. @@ -4006,7 +4006,7 @@ trigger-child-pipeline: **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` @@ -4042,7 +4042,7 @@ trigger-multi-project-pipeline: **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 authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md). The trigger token is different than the `trigger` keyword. diff --git a/doc/user/group/manage.md b/doc/user/group/manage.md index 8c71169d6ba..f11d9035a52 100644 --- a/doc/user/group/manage.md +++ b/doc/user/group/manage.md @@ -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'") => [#, #] ``` + +### 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('') +group = Group.find_by_name("") +## Set parent_group = nil to make the subgroup a top-level group +parent_group = Group.find_by(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) +``` diff --git a/doc/user/project/remote_development/index.md b/doc/user/project/remote_development/index.md new file mode 100644 index 00000000000..879978f550a --- /dev/null +++ b/doc/user/project/remote_development/index.md @@ -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). diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index accf86611aa..705e49df039 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -559,3 +559,33 @@ If this fails, display why it doesn't work with: project = Project.find_by_full_path('') 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 with the appropriate feature name in all instances + state = p. + + if state != 0 + puts "#{p.name} has already enabled. Skipping..." + else + puts "#{p.name} didn't have enabled. Enabling..." + p.project_feature.update!(: 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). diff --git a/qa/Gemfile b/qa/Gemfile index c3eb348ece8..12e5d66fc6b 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,10 +2,10 @@ 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 'allure-rspec', '~> 2.18.0' -gem 'capybara', '~> 3.35.0' +gem 'capybara', '~> 3.37.1' gem 'capybara-screenshot', '~> 1.0.26' gem 'rake', '~> 13' gem 'rspec', '~> 3.11' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index bc41490dcf1..23f82f553f1 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -27,8 +27,9 @@ GEM binding_ninja (0.2.3) builder (3.2.4) byebug (11.1.3) - capybara (3.35.3) + capybara (3.37.1) addressable + matrix mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) @@ -99,14 +100,15 @@ GEM gitlab (4.18.0) httparty (~> 0.18) terminal-table (>= 1.5.1) - gitlab-qa (8.7.0) + gitlab-qa (8.8.0) activesupport (~> 6.1) gitlab (~> 4.18.0) http (~> 5.0) nokogiri (~> 1.10) - rainbow (~> 3.0.0) + rainbow (>= 3, < 4) table_print (= 1.5.7) - zeitwerk (~> 2.4) + toxiproxy (~> 2.0.2) + zeitwerk (>= 2, < 3) google-apis-compute_v1 (0.51.0) google-apis-core (>= 0.7.2, < 2.a) google-apis-core (0.9.0) @@ -165,6 +167,7 @@ GEM rake (~> 13.0) macaddr (1.7.2) systemu (~> 2.6.5) + matrix (0.4.2) memoist (0.16.2) method_source (1.0.0) mime-types (3.4.1) @@ -266,6 +269,7 @@ GEM terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) timecop (0.9.5) + toxiproxy (2.0.2) trailblazer-option (0.1.2) tzinfo (2.0.5) concurrent-ruby (~> 1.0) @@ -300,7 +304,7 @@ DEPENDENCIES activesupport (~> 6.1.4.7) airborne (~> 0.3.7) allure-rspec (~> 2.18.0) - capybara (~> 3.35.0) + capybara (~> 3.37.1) capybara-screenshot (~> 1.0.26) chemlab (~> 0.10) chemlab-library-www-gitlab-com (~> 0.1) @@ -310,7 +314,7 @@ DEPENDENCIES faraday-retry (~> 2.0) fog-core (= 2.1.0) fog-google (~> 1.19) - gitlab-qa (~> 8, >= 8.7.0) + gitlab-qa (~> 8, >= 8.8.0) influxdb-client (~> 1.17) knapsack (~> 4.0) nokogiri (~> 1.13, >= 1.13.9) diff --git a/spec/fixtures/markdown/markdown_golden_master_examples.yml b/spec/fixtures/markdown/markdown_golden_master_examples.yml index 1c10f4fb9e0..6a1e75348cf 100644 --- a/spec/fixtures/markdown/markdown_golden_master_examples.yml +++ b/spec/fixtures/markdown/markdown_golden_master_examples.yml @@ -770,18 +770,18 @@ # responsibility of unit tests. These tests are about the structure of the HTML. uri_substitution: *uri_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' text_attribute_substitution: - - regex: '(title)(=")(.+?)(")' + - regex: '(title)(=")([^"]*)(")' replacement: '\1\2TEXT\4' path_attribute_id_substitution: - regex: '(group|project)(\d+)' replacement: '\1ID' 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: |- -

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

- name: strike markdown: |- ~~del~~ diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js index fbf00b8dd41..e72eb892e74 100644 --- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js +++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js @@ -21,7 +21,9 @@ describe('~/content_editor/components/suggestions_dropdown', () => { const exampleUser = { username: 'root', avatar_url: 'root_avatar.png', type: 'User' }; const exampleIssue = { iid: 123, title: 'Test Issue' }; 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 = { name: 'due', description: 'Set due date', @@ -32,7 +34,19 @@ describe('~/content_editor/components/suggestions_dropdown', () => { title: '❓ Remote Development | Solution validation', 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', color: '#E44D2A', type: 'GroupLabel', @@ -67,10 +81,13 @@ describe('~/content_editor/components/suggestions_dropdown', () => { ${'reference'} | ${'user'} | ${'@'} | ${exampleUser} | ${`@root`} | ${{}} ${'reference'} | ${'issue'} | ${'#'} | ${exampleIssue} | ${`#123`} | ${{}} ${'reference'} | ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${`!224`} | ${{}} - ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone} | ${`%1.3`} | ${{}} - ${'reference'} | ${'command'} | ${'/'} | ${exampleCommand} | ${'/due '} | ${{}} + ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${`%13`} | ${{}} + ${'reference'} | ${'milestone'} | ${'%'} | ${exampleMilestone2} | ${`%Milestone with spaces`} | ${{ originalText: '%"Milestone with spaces"' }} + ${'reference'} | ${'command'} | ${'/'} | ${exampleCommand} | ${'/due'} | ${{}} ${'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'} | ${'snippet'} | ${'$'} | ${exampleSnippet} | ${`$2420859`} | ${{}} ${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps} @@ -130,7 +147,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => { referenceType | char | reference | displaysID ${'issue'} | ${'#'} | ${exampleIssue} | ${true} ${'merge_request'} | ${'!'} | ${exampleMergeRequest} | ${true} - ${'milestone'} | ${'%'} | ${exampleMilestone} | ${false} + ${'milestone'} | ${'%'} | ${exampleMilestone1} | ${false} `('rendering $referenceType references', ({ referenceType, char, reference, displaysID }) => { it(`displays ${referenceType} ID and title`, () => { buildWrapper({ @@ -172,20 +189,26 @@ describe('~/content_editor/components/suggestions_dropdown', () => { }); 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({ propsData: { char: '~', nodeProps: { 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( - `background-color: rgb(228, 77, 42);`, // #E44D2A + `background-color: ${displayedColor};`, ); }); }); diff --git a/spec/frontend/content_editor/components/wrappers/label_spec.js b/spec/frontend/content_editor/components/wrappers/label_spec.js new file mode 100644 index 00000000000..9e58669b0ea --- /dev/null +++ b/spec/frontend/content_editor/components/wrappers/label_spec.js @@ -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); + }); +}); diff --git a/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb index 168aef0f174..72e23e6d5fa 100644 --- a/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb +++ b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb @@ -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(: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(:issue) { create(:issue, project: project) } let_it_be(:merge_request) { create(:merge_request, source_project: project) }