Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue10
-rw-r--r--app/assets/javascripts/jira_connect/index.js4
-rw-r--r--app/assets/javascripts/lib/utils/grammar.js6
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue69
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue78
-rw-r--r--app/assets/javascripts/notes/components/email_participants_warning.vue70
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue82
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js11
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue20
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js)24
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue137
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue86
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js2
-rw-r--r--app/assets/javascripts/pipelines/utils.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss9
-rw-r--r--app/assets/stylesheets/pages/note_form.scss8
-rw-r--r--app/graphql/resolvers/package_details_resolver.rb18
-rw-r--r--app/graphql/resolvers/packages_resolver.rb2
-rw-r--r--app/graphql/types/package_type.rb16
-rw-r--r--app/graphql/types/package_type_enum.rb15
-rw-r--r--app/graphql/types/packages/composer/details_type.rb16
-rw-r--r--app/graphql/types/packages/composer/json_type.rb18
-rw-r--r--app/graphql/types/packages/composer/metadatum_type.rb17
-rw-r--r--app/graphql/types/packages/package_tag_type.rb16
-rw-r--r--app/graphql/types/packages/package_type.rb27
-rw-r--r--app/graphql/types/packages/package_type_enum.rb17
-rw-r--r--app/graphql/types/project_type.rb2
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/models/ci/bridge.rb6
-rw-r--r--app/models/project.rb2
-rw-r--r--app/policies/packages/composer/metadatum_policy.rb8
-rw-r--r--app/policies/packages/tag_policy.rb6
-rw-r--r--app/policies/project_policy.rb8
-rw-r--r--app/serializers/issue_entity.rb4
-rw-r--r--app/services/ci/create_downstream_pipeline_service.rb4
-rw-r--r--app/services/ci/pipeline_trigger_service.rb8
-rw-r--r--app/services/ci/play_build_service.rb4
-rw-r--r--app/views/jira_connect/subscriptions/index.html.haml5
-rw-r--r--changelogs/unreleased/13019-improve-error-messages-when-adding-namespaces-in-jira-connect-app.yml5
-rw-r--r--changelogs/unreleased/285467-package-registry-graphql-api.yml5
-rw-r--r--changelogs/unreleased/297011-add-additional-aliases-for-reviewer-quick-commands.yml5
-rw-r--r--changelogs/unreleased/ajk-297358-fix-preloaded-pagination.yml5
-rw-r--r--changelogs/unreleased/ci-setting-prevent-user-defined-ci-variables.yml5
-rw-r--r--changelogs/unreleased/fix-admin-project-overview-badge-alignment.yml5
-rw-r--r--changelogs/unreleased/webide-markdown-center.yml5
-rw-r--r--db/migrate/20201223114050_add_restrict_user_defined_variables_to_project_settings.rb19
-rw-r--r--db/schema_migrations/202012231140501
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/postgresql/replication_and_failover.md56
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql329
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json855
-rw-r--r--doc/api/graphql/reference/index.md67
-rw-r--r--doc/api/projects.md14
-rw-r--r--doc/ci/variables/README.md30
-rw-r--r--doc/install/README.md17
-rw-r--r--doc/user/group/roadmap/img/roadmap_filters_v13_7.pngbin21615 -> 0 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_filters_v13_8.pngbin0 -> 20766 bytes
-rw-r--r--doc/user/group/roadmap/index.md3
-rw-r--r--lib/api/entities/project.rb1
-rw-r--r--lib/api/helpers/projects_helpers.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb30
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb4
-rw-r--r--lib/gitlab/graphql/pagination/keyset/connection.rb17
-rw-r--r--lib/gitlab/graphql/pagination/keyset/query_builder.rb5
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb29
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb16
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/features/projects/issues/email_participants_spec.rb34
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_composer_details.json12
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json36
-rw-r--r--spec/frontend/jira_connect/components/app_spec.js33
-rw-r--r--spec/frontend/notes/components/comment_field_layout_spec.js137
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js15
-rw-r--r--spec/frontend/notes/components/email_participants_warning_spec.js70
-rw-r--r--spec/frontend/notes/components/note_form_spec.js4
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js9
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js66
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js2
-rw-r--r--spec/frontend/pipelines/shared/links_layer_spec.js99
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js8
-rw-r--r--spec/graphql/resolvers/package_details_resolver_spec.rb21
-rw-r--r--spec/graphql/types/packages/composer/details_type_spec.rb23
-rw-r--r--spec/graphql/types/packages/composer/json_type_spec.rb15
-rw-r--r--spec/graphql/types/packages/composer/metadatum_type_spec.rb15
-rw-r--r--spec/graphql/types/packages/package_type_enum_spec.rb (renamed from spec/graphql/types/package_type_enum_spec.rb)0
-rw-r--r--spec/graphql/types/packages/package_type_spec.rb (renamed from spec/graphql/types/package_type_spec.rb)2
-rw-r--r--spec/graphql/types/packages/tag_type_spec.rb15
-rw-r--r--spec/graphql/types/query_type_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb93
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb26
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb41
-rw-r--r--spec/policies/project_policy_spec.rb42
-rw-r--r--spec/requests/api/graphql/packages/package_composer_details_spec.rb39
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb37
-rw-r--r--spec/requests/api/projects_spec.rb14
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb47
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb14
-rw-r--r--spec/services/ci/play_build_service_spec.rb25
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb162
111 files changed, 3313 insertions, 397 deletions
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue
index 275a66d5956..e08918a6720 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/components/app.vue
@@ -1,9 +1,13 @@
<script>
import { mapState } from 'vuex';
+import { GlAlert } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'JiraConnectApp',
+ components: {
+ GlAlert,
+ },
mixins: [glFeatureFlagsMixin()],
computed: {
...mapState(['errorMessage']),
@@ -16,6 +20,12 @@ export default {
<template>
<div>
+ <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" :dismissible="false">
+ {{ errorMessage }}
+ </gl-alert>
+
+ <h1>GitLab for Jira Configuration</h1>
+
<div v-if="showNewUi">
<h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3>
</div>
diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js
index 05f13de6f53..0dbcb778a6c 100644
--- a/app/assets/javascripts/jira_connect/index.js
+++ b/app/assets/javascripts/jira_connect/index.js
@@ -23,11 +23,9 @@ const initJiraFormHandlers = () => {
};
const reqFailed = (res, fallbackErrorMessage) => {
- const { responseJSON: { error = fallbackErrorMessage } = {} } = res || {};
+ const { error = fallbackErrorMessage } = res || {};
store.commit(SET_ERROR_MESSAGE, error);
- // eslint-disable-next-line no-alert
- alert(error);
};
if (typeof AP.getLocation === 'function') {
diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js
index f68767b9acd..6d6361d19b6 100644
--- a/app/assets/javascripts/lib/utils/grammar.js
+++ b/app/assets/javascripts/lib/utils/grammar.js
@@ -16,12 +16,12 @@ import { sprintf, s__ } from '~/locale';
*
* @param {String[]} items
*/
-export const toNounSeriesText = (items) => {
+export const toNounSeriesText = (items, { onlyCommas = false } = {}) => {
if (items.length === 0) {
return '';
} else if (items.length === 1) {
return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false);
- } else if (items.length === 2) {
+ } else if (items.length === 2 && !onlyCommas) {
return sprintf(
s__('nounSeries|%{firstItem} and %{lastItem}'),
{
@@ -33,7 +33,7 @@ export const toNounSeriesText = (items) => {
}
return items.reduce((item, nextItem, idx) =>
- idx === items.length - 1
+ idx === items.length - 1 && !onlyCommas
? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }, false)
: sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }, false),
);
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
new file mode 100644
index 00000000000..aaf64702ffd
--- /dev/null
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -0,0 +1,69 @@
+<script>
+import EmailParticipantsWarning from './email_participants_warning.vue';
+import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
+
+const DEFAULT_NOTEABLE_TYPE = 'Issue';
+
+export default {
+ components: {
+ EmailParticipantsWarning,
+ NoteableWarning,
+ },
+ props: {
+ noteableData: {
+ type: Object,
+ required: true,
+ },
+ noteableType: {
+ type: String,
+ required: false,
+ default: DEFAULT_NOTEABLE_TYPE,
+ },
+ withAlertContainer: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ isLocked() {
+ return Boolean(this.noteableData.discussion_locked);
+ },
+ isConfidential() {
+ return Boolean(this.noteableData.confidential);
+ },
+ hasWarning() {
+ return this.isConfidential || this.isLocked;
+ },
+ emailParticipants() {
+ return this.noteableData.issue_email_participants?.map(({ email }) => email) || [];
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-base gl-border-gray-100"
+ >
+ <div
+ v-if="withAlertContainer"
+ class="error-alert"
+ data-testid="comment-field-alert-container"
+ ></div>
+ <noteable-warning
+ v-if="hasWarning"
+ class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100 gl-rounded-base gl-rounded-bottom-left-none gl-rounded-bottom-right-none"
+ :is-locked="isLocked"
+ :is-confidential="isConfidential"
+ :noteable-type="noteableType"
+ :locked-noteable-docs-path="noteableData.locked_discussion_docs_path"
+ :confidential-noteable-docs-path="noteableData.confidential_issues_docs_path"
+ />
+ <slot></slot>
+ <email-participants-warning
+ v-if="emailParticipants.length"
+ class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100 gl-rounded-base gl-rounded-top-left-none! gl-rounded-top-right-none!"
+ :emails="emailParticipants"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 67e9b8b2c19..111af977ec5 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -17,18 +17,17 @@ import {
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import * as constants from '../constants';
import eventHub from '../event_hub';
-import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
+import CommentFieldLayout from './comment_field_layout.vue';
export default {
name: 'CommentForm',
components: {
- NoteableWarning,
noteSignedOutWidget,
discussionLockedWidget,
markdownField,
@@ -36,6 +35,7 @@ export default {
GlButton,
TimelineEntryItem,
GlIcon,
+ CommentFieldLayout,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin],
props: {
@@ -287,6 +287,9 @@ export default {
Autosize.update(this.$refs.textarea);
});
},
+ hasEmailParticipants() {
+ return this.getNoteableData.issue_email_participants?.length;
+ },
},
};
</script>
@@ -309,46 +312,41 @@ export default {
</div>
<div class="timeline-content timeline-content-form">
<form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
- <div class="error-alert"></div>
-
- <noteable-warning
- v-if="hasWarning(getNoteableData)"
- :is-locked="isLocked(getNoteableData)"
- :is-confidential="isConfidential(getNoteableData)"
+ <comment-field-layout
+ :with-alert-container="true"
+ :noteable-data="getNoteableData"
:noteable-type="noteableType"
- :locked-noteable-docs-path="lockedIssueDocsPath"
- :confidential-noteable-docs-path="confidentialIssueDocsPath"
- />
-
- <markdown-field
- ref="markdownField"
- :is-submitting="isSubmitting"
- :markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
- :add-spacing-classes="false"
- :textarea-value="note"
>
- <textarea
- id="note-body"
- ref="textarea"
- slot="textarea"
- v-model="note"
- dir="auto"
- :disabled="isSubmitting"
- name="note[note]"
- class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area"
- data-qa-selector="comment_field"
- data-testid="comment-field"
- :data-supports-quick-actions="!glFeatures.tributeAutocomplete"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment or drag your files here…')"
- @keydown.up="editCurrentUserLastNote()"
- @keydown.meta.enter="handleSave()"
- @keydown.ctrl.enter="handleSave()"
- ></textarea>
- </markdown-field>
-
+ <markdown-field
+ ref="markdownField"
+ :is-submitting="isSubmitting"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :add-spacing-classes="false"
+ :textarea-value="note"
+ >
+ <template #textarea>
+ <textarea
+ id="note-body"
+ ref="textarea"
+ v-model="note"
+ dir="auto"
+ :disabled="isSubmitting"
+ name="note[note]"
+ class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area"
+ data-qa-selector="comment_field"
+ data-testid="comment-field"
+ :data-supports-quick-actions="!glFeatures.tributeAutocomplete"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ @keydown.up="editCurrentUserLastNote()"
+ @keydown.meta.enter="handleSave()"
+ @keydown.ctrl.enter="handleSave()"
+ ></textarea>
+ </template>
+ </markdown-field>
+ </comment-field-layout>
<div class="note-form-actions">
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue
new file mode 100644
index 00000000000..bb1ff58120a
--- /dev/null
+++ b/app/assets/javascripts/notes/components/email_participants_warning.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { toNounSeriesText } from '~/lib/utils/grammar';
+
+export default {
+ components: {
+ GlSprintf,
+ },
+ props: {
+ emails: {
+ type: Array,
+ required: true,
+ },
+ numberOfLessParticipants: {
+ type: Number,
+ required: false,
+ default: 3,
+ },
+ },
+ data() {
+ return {
+ isShowingMoreParticipants: false,
+ };
+ },
+ computed: {
+ title() {
+ return this.moreParticipantsAvailable
+ ? toNounSeriesText(this.lessParticipants, { onlyCommas: true })
+ : toNounSeriesText(this.emails);
+ },
+ lessParticipants() {
+ return this.emails.slice(0, this.numberOfLessParticipants);
+ },
+ moreLabel() {
+ return sprintf(s__('EmailParticipantsWarning|and %{moreCount} more'), {
+ moreCount: this.emails.length - this.numberOfLessParticipants,
+ });
+ },
+ moreParticipantsAvailable() {
+ return !this.isShowingMoreParticipants && this.emails.length > this.numberOfLessParticipants;
+ },
+ message() {
+ return this.moreParticipantsAvailable
+ ? s__('EmailParticipantsWarning|%{emails}, %{andMore} will be notified of your comment.')
+ : s__('EmailParticipantsWarning|%{emails} will be notified of your comment.');
+ },
+ },
+ methods: {
+ showMoreParticipants() {
+ this.isShowingMoreParticipants = true;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-note-warning" data-testid="email-participants-warning">
+ <gl-sprintf :message="message">
+ <template #andMore>
+ <button type="button" class="btn-transparent btn-link" @click="showMoreParticipants">
+ {{ moreLabel }}
+ </button>
+ </template>
+ <template #emails>
+ <span>{{ title }}</span>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 47202d0e241..9acb837c27f 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -3,19 +3,19 @@
import { mapGetters, mapActions, mapState } from 'vuex';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
-import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import markdownField from '~/vue_shared/components/markdown/field.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
import { __, sprintf } from '~/locale';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
+import CommentFieldLayout from './comment_field_layout.vue';
export default {
name: 'NoteForm',
components: {
- NoteableWarning,
markdownField,
+ CommentFieldLayout,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable],
props: {
@@ -303,6 +303,9 @@ export default {
this.$emit('handleFormUpdateAddToReview', this.updatedNoteBody, shouldResolve);
},
+ hasEmailParticipants() {
+ return this.getNoteableData.issue_email_participants?.length;
+ },
},
};
</script>
@@ -316,46 +319,41 @@ export default {
></div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
- <noteable-warning
- v-if="hasWarning(getNoteableData)"
- :is-locked="isLocked(getNoteableData)"
- :is-confidential="isConfidential(getNoteableData)"
- :locked-noteable-docs-path="lockedIssueDocsPath"
- :confidential-noteable-docs-path="confidentialIssueDocsPath"
- />
-
- <markdown-field
- :markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
- :line="line"
- :note="discussionNote"
- :can-suggest="canSuggest"
- :add-spacing-classes="false"
- :help-page-path="helpPagePath"
- :show-suggest-popover="showSuggestPopover"
- :textarea-value="updatedNoteBody"
- @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
- >
- <textarea
- id="note_note"
- ref="textarea"
- slot="textarea"
- v-model="updatedNoteBody"
- :data-supports-quick-actions="!isEditing && !glFeatures.tributeAutocomplete"
- name="note[note]"
- class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form"
- data-qa-selector="reply_field"
- dir="auto"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment or drag your files here…')"
- @keydown.meta.enter="handleKeySubmit()"
- @keydown.ctrl.enter="handleKeySubmit()"
- @keydown.exact.up="editMyLastNote()"
- @keydown.exact.esc="cancelHandler(true)"
- @input="onInput"
- ></textarea>
- </markdown-field>
+ <comment-field-layout :noteable-data="getNoteableData">
+ <markdown-field
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :line="line"
+ :note="discussionNote"
+ :can-suggest="canSuggest"
+ :add-spacing-classes="false"
+ :help-page-path="helpPagePath"
+ :show-suggest-popover="showSuggestPopover"
+ :textarea-value="updatedNoteBody"
+ @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
+ >
+ <template #textarea>
+ <textarea
+ id="note_note"
+ ref="textarea"
+ v-model="updatedNoteBody"
+ :data-supports-quick-actions="!isEditing && !glFeatures.tributeAutocomplete"
+ name="note[note]"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form"
+ data-qa-selector="reply_field"
+ dir="auto"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ @keydown.meta.enter="handleKeySubmit()"
+ @keydown.ctrl.enter="handleKeySubmit()"
+ @keydown.exact.up="editMyLastNote()"
+ @keydown.exact.esc="cancelHandler(true)"
+ @input="onInput"
+ ></textarea>
+ </template>
+ </markdown-field>
+ </comment-field-layout>
<div class="note-form-actions clearfix">
<template v-if="showBatchCommentsActions">
<p v-if="showResolveDiscussionToggle">
diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js
index 0ca8c8c98a3..52b67764b70 100644
--- a/app/assets/javascripts/notes/mixins/issuable_state.js
+++ b/app/assets/javascripts/notes/mixins/issuable_state.js
@@ -12,21 +12,10 @@ export default {
lockedIssueDocsPath() {
return this.getNoteableDataByProp('locked_discussion_docs_path');
},
- confidentialIssueDocsPath() {
- return this.getNoteableDataByProp('confidential_issues_docs_path');
- },
},
methods: {
- isConfidential(issue) {
- return Boolean(issue.confidential);
- },
-
isLocked(issue) {
return Boolean(issue.discussion_locked);
},
-
- hasWarning(issue) {
- return this.isConfidential(issue) || this.isLocked(issue);
- },
},
};
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue
index e2529613844..ef2be2a5fba 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue
@@ -44,13 +44,21 @@ export default {
<template>
<div data-testid="ci-lint-value">
- <pre v-if="scripts.beforeScript.show" data-testid="ci-lint-before-script">{{
- scripts.beforeScript.content
- }}</pre>
- <pre v-if="scripts.script.show" data-testid="ci-lint-script">{{ scripts.script.content }}</pre>
- <pre v-if="scripts.afterScript.show" data-testid="ci-lint-after-script">{{
- scripts.afterScript.content
+ <pre
+ v-if="scripts.beforeScript.show"
+ class="gl-white-space-pre-wrap"
+ data-testid="ci-lint-before-script"
+ >{{ scripts.beforeScript.content }}</pre
+ >
+ <pre v-if="scripts.script.show" class="gl-white-space-pre-wrap" data-testid="ci-lint-script">{{
+ scripts.script.content
}}</pre>
+ <pre
+ v-if="scripts.afterScript.show"
+ class="gl-white-space-pre-wrap"
+ data-testid="ci-lint-after-script"
+ >{{ scripts.afterScript.content }}</pre
+ >
<ul class="gl-list-style-none gl-pl-0 gl-mb-0">
<li v-if="tagList">
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 5958d198be2..96a674e342f 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,5 +1,6 @@
<script>
import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
+import LinksLayer from '../graph_shared/links_layer.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
@@ -8,6 +9,7 @@ import { reportToSentry } from './utils';
export default {
name: 'PipelineGraph',
components: {
+ LinksLayer,
LinkedGraphWrapper,
LinkedPipelinesColumn,
StageColumnComponent,
@@ -32,9 +34,15 @@ export default {
DOWNSTREAM,
UPSTREAM,
},
+ CONTAINER_REF: 'PIPELINE_LINKS_CONTAINER_REF',
+ BASE_CONTAINER_ID: 'pipeline-links-container',
data() {
return {
hoveredJobName: '',
+ measurements: {
+ width: 0,
+ height: 0,
+ },
pipelineExpanded: {
jobName: '',
expanded: false,
@@ -42,6 +50,9 @@ export default {
};
},
computed: {
+ containerId() {
+ return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`;
+ },
downstreamPipelines() {
return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
},
@@ -54,12 +65,13 @@ export default {
hasUpstreamPipelines() {
return Boolean(this.pipeline?.upstream?.length > 0);
},
- // The two show checks prevent upstream / downstream from showing redundant linked columns
+ // The show downstream check prevents showing redundant linked columns
showDownstreamPipelines() {
return (
this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM
);
},
+ // The show upstream check prevents showing redundant linked columns
showUpstreamPipelines() {
return (
this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM
@@ -72,7 +84,19 @@ export default {
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
+ mounted() {
+ this.measurements = this.getMeasurements();
+ },
methods: {
+ getMeasurements() {
+ return {
+ width: this.$refs[this.containerId].scrollWidth,
+ height: this.$refs[this.containerId].scrollHeight,
+ };
+ },
+ onError(errorType) {
+ this.$emit('error', errorType);
+ },
setJob(jobName) {
this.hoveredJobName = jobName;
},
@@ -88,43 +112,57 @@ export default {
<template>
<div class="js-pipeline-graph">
<div
+ :id="containerId"
+ :ref="containerId"
class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
:class="{ 'gl-py-5': !isLinkedPipeline }"
>
- <linked-graph-wrapper>
- <template #upstream>
- <linked-pipelines-column
- v-if="showUpstreamPipelines"
- :linked-pipelines="upstreamPipelines"
- :column-title="__('Upstream')"
- :type="$options.pipelineTypeConstants.UPSTREAM"
- @error="emit('error', errorType)"
- />
- </template>
- <template #main>
- <stage-column-component
- v-for="stage in graph"
- :key="stage.name"
- :title="stage.name"
- :groups="stage.groups"
- :action="stage.status.action"
- :job-hovered="hoveredJobName"
- :pipeline-expanded="pipelineExpanded"
- @refreshPipelineGraph="$emit('refreshPipelineGraph')"
- />
- </template>
- <template #downstream>
- <linked-pipelines-column
- v-if="showDownstreamPipelines"
- :linked-pipelines="downstreamPipelines"
- :column-title="__('Downstream')"
- :type="$options.pipelineTypeConstants.DOWNSTREAM"
- @downstreamHovered="setJob"
- @pipelineExpandToggle="togglePipelineExpanded"
- @error="emit('error', errorType)"
- />
- </template>
- </linked-graph-wrapper>
+ <links-layer
+ :pipeline-data="graph"
+ :pipeline-id="pipeline.id"
+ :container-id="containerId"
+ :container-measurements="measurements"
+ :highlighted-job="hoveredJobName"
+ default-link-color="gl-stroke-transparent"
+ @error="onError"
+ >
+ <linked-graph-wrapper>
+ <template #upstream>
+ <linked-pipelines-column
+ v-if="showUpstreamPipelines"
+ :linked-pipelines="upstreamPipelines"
+ :column-title="__('Upstream')"
+ :type="$options.pipelineTypeConstants.UPSTREAM"
+ @error="onError"
+ />
+ </template>
+ <template #main>
+ <stage-column-component
+ v-for="stage in graph"
+ :key="stage.name"
+ :title="stage.name"
+ :groups="stage.groups"
+ :action="stage.status.action"
+ :job-hovered="hoveredJobName"
+ :pipeline-expanded="pipelineExpanded"
+ :pipeline-id="pipeline.id"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
+ @jobHover="setJob"
+ />
+ </template>
+ <template #downstream>
+ <linked-pipelines-column
+ v-if="showDownstreamPipelines"
+ :linked-pipelines="downstreamPipelines"
+ :column-title="__('Downstream')"
+ :type="$options.pipelineTypeConstants.DOWNSTREAM"
+ @downstreamHovered="setJob"
+ @pipelineExpandToggle="togglePipelineExpanded"
+ @error="onError"
+ />
+ </template>
+ </linked-graph-wrapper>
+ </links-layer>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index b6e756ceaba..08d6162aeb8 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -23,8 +23,16 @@ export default {
type: Object,
required: true,
},
+ pipelineId: {
+ type: Number,
+ required: false,
+ default: -1,
+ },
},
computed: {
+ computedJobId() {
+ return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : '';
+ },
tooltipText() {
const { name, status } = this.group;
return `${name} - ${status.label}`;
@@ -41,7 +49,7 @@ export default {
};
</script>
<template>
- <div class="ci-job-dropdown-container dropdown dropright">
+ <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright">
<button
v-gl-tooltip.hover="{ boundary: 'viewport' }"
:title="tooltipText"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 4f414cbb31f..8262d728a24 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -74,6 +74,11 @@ export default {
required: false,
default: () => ({}),
},
+ pipelineId: {
+ type: Number,
+ required: false,
+ default: -1,
+ },
},
computed: {
boundary() {
@@ -85,6 +90,9 @@ export default {
hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status);
},
+ computedJobId() {
+ return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
+ },
status() {
return this.job && this.job.status ? this.job.status : {};
},
@@ -146,6 +154,7 @@ export default {
</script>
<template>
<div
+ :id="computedJobId"
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
data-qa-selector="job_item_container"
>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 45d183cce97..e65ae318952 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -24,6 +24,10 @@ export default {
type: Array,
required: true,
},
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
action: {
type: Object,
required: false,
@@ -94,16 +98,19 @@ export default {
:key="getGroupId(group)"
data-testid="stage-column-group"
class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
+ @mouseenter="$emit('jobHover', group.name)"
+ @mouseleave="$emit('jobHover', '')"
>
<job-item
v-if="group.size === 1"
:job="group.jobs[0]"
:job-hovered="jobHovered"
:pipeline-expanded="pipelineExpanded"
+ :pipeline-id="pipelineId"
css-class-job-name="gl-build-content"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/>
- <job-group-dropdown v-else :group="group" />
+ <job-group-dropdown v-else :group="group" :pipeline-id="pipelineId" />
</div>
</template>
</main-graph-wrapper>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
index b550c599fda..fe59af12011 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -1,5 +1,7 @@
import * as d3 from 'd3';
-import { createUniqueLinkId } from '../../utils';
+
+export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;
+
/**
* This function expects its first argument data structure
* to be the same shaped as the one generated by `parseData`,
@@ -7,12 +9,11 @@ import { createUniqueLinkId } from '../../utils';
* we find the nodes in the graph, calculate their coordinates and
* trace the lines that represent the needs of each job.
* @param {Object} nodeDict - Resulting object of `parseData` with nodes and links
- * @param {Object} jobs - An object where each key is the job name that contains the job data
- * @param {ref} svg - Reference to the svg we draw in
+ * @param {String} containerID - Id for the svg the links will be draw in
* @returns {Array} Links that contain all the information about them
*/
-export const generateLinksData = ({ links }, containerID) => {
+export const generateLinksData = ({ links }, containerID, modifier = '') => {
const containerEl = document.getElementById(containerID);
return links.map((link) => {
const path = d3.path();
@@ -20,8 +21,11 @@ export const generateLinksData = ({ links }, containerID) => {
const sourceId = link.source;
const targetId = link.target;
- const sourceNodeEl = document.getElementById(sourceId);
- const targetNodeEl = document.getElementById(targetId);
+ const modifiedSourceId = `${sourceId}${modifier}`;
+ const modifiedTargetId = `${targetId}${modifier}`;
+
+ const sourceNodeEl = document.getElementById(modifiedSourceId);
+ const targetNodeEl = document.getElementById(modifiedTargetId);
const sourceNodeCoordinates = sourceNodeEl.getBoundingClientRect();
const targetNodeCoordinates = targetNodeEl.getBoundingClientRect();
@@ -35,11 +39,11 @@ export const generateLinksData = ({ links }, containerID) => {
// from the total to make sure it's aligned properly. We then make the line
// positioned in the center of the job node by adding half the height
// of the job pill.
- const paddingLeft = Number(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-left').replace('px', ''),
+ const paddingLeft = parseFloat(
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-left'),
);
- const paddingTop = Number(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-top').replace('px', ''),
+ const paddingTop = parseFloat(
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-top'),
);
const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft;
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
new file mode 100644
index 00000000000..480cb032e11
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
@@ -0,0 +1,137 @@
+<script>
+import { isEmpty } from 'lodash';
+import { DRAW_FAILURE } from '../../constants';
+import { createJobsHash, generateJobNeedsDict } from '../../utils';
+import { parseData } from '../parsing_utils';
+import { generateLinksData } from './drawing_utils';
+
+export default {
+ name: 'LinksInner',
+ STROKE_WIDTH: 2,
+ props: {
+ containerId: {
+ type: String,
+ required: true,
+ },
+ containerMeasurements: {
+ type: Object,
+ required: true,
+ },
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
+ pipelineData: {
+ type: Array,
+ required: true,
+ },
+ defaultLinkColor: {
+ type: String,
+ required: false,
+ default: 'gl-stroke-gray-200',
+ },
+ highlightedJob: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ links: [],
+ needsObject: null,
+ };
+ },
+ computed: {
+ hasHighlightedJob() {
+ return Boolean(this.highlightedJob);
+ },
+ isPipelineDataEmpty() {
+ return isEmpty(this.pipelineData);
+ },
+ highlightedJobs() {
+ // If you are hovering on a job, then the jobs we want to highlight are:
+ // The job you are currently hovering + all of its needs.
+ return this.hasHighlightedJob
+ ? [this.highlightedJob, ...this.needsObject[this.highlightedJob]]
+ : [];
+ },
+ highlightedLinks() {
+ // If you are hovering on a job, then the links we want to highlight are:
+ // All the links whose `source` and `target` are highlighted jobs.
+ if (this.hasHighlightedJob) {
+ const filteredLinks = this.links.filter((link) => {
+ return (
+ this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target)
+ );
+ });
+
+ return filteredLinks.map((link) => link.ref);
+ }
+
+ return [];
+ },
+ viewBox() {
+ return [0, 0, this.containerMeasurements.width, this.containerMeasurements.height];
+ },
+ },
+ watch: {
+ highlightedJob() {
+ // On first hover, generate the needs reference
+ if (!this.needsObject) {
+ const jobs = createJobsHash(this.pipelineData);
+ this.needsObject = generateJobNeedsDict(jobs) ?? {};
+ }
+ },
+ },
+ mounted() {
+ if (!isEmpty(this.pipelineData)) {
+ this.prepareLinkData();
+ }
+ },
+ methods: {
+ isLinkHighlighted(linkRef) {
+ return this.highlightedLinks.includes(linkRef);
+ },
+ prepareLinkData() {
+ try {
+ const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
+ const parsedData = parseData(arrayOfJobs);
+ this.links = generateLinksData(parsedData, this.containerId, `-${this.pipelineId}`);
+ } catch {
+ this.$emit('error', DRAW_FAILURE);
+ }
+ },
+ getLinkClasses(link) {
+ return [
+ this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : this.defaultLinkColor,
+ { 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
+ ];
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-relative">
+ <svg
+ id="link-svg"
+ class="gl-absolute"
+ :viewBox="viewBox"
+ :width="`${containerMeasurements.width}px`"
+ :height="`${containerMeasurements.height}px`"
+ >
+ <template>
+ <path
+ v-for="link in links"
+ :key="link.path"
+ :ref="link.ref"
+ :d="link.path"
+ class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
+ :class="getLinkClasses(link)"
+ :stroke-width="$options.STROKE_WIDTH"
+ />
+ </template>
+ </svg>
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
new file mode 100644
index 00000000000..0993892a574
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { __ } from '~/locale';
+import LinksInner from './links_inner.vue';
+
+export default {
+ name: 'LinksLayer',
+ components: {
+ GlAlert,
+ LinksInner,
+ },
+ MAX_GROUPS: 200,
+ props: {
+ containerMeasurements: {
+ type: Object,
+ required: true,
+ },
+ pipelineData: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ alertDismissed: false,
+ showLinksOverride: false,
+ };
+ },
+ i18n: {
+ showLinksAnyways: __('Show links anyways'),
+ tooManyJobs: __(
+ 'This graph has a large number of jobs and showing the links between them may have performance implications.',
+ ),
+ },
+ computed: {
+ containerZero() {
+ return !this.containerMeasurements.width || !this.containerMeasurements.height;
+ },
+ numGroups() {
+ return this.pipelineData.reduce((acc, { groups }) => {
+ return acc + Number(groups.length);
+ }, 0);
+ },
+ showAlert() {
+ return !this.showLinkedLayers && !this.alertDismissed;
+ },
+ showLinkedLayers() {
+ return (
+ !this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS)
+ );
+ },
+ },
+ methods: {
+ dismissAlert() {
+ this.alertDismissed = true;
+ },
+ overrideShowLinks() {
+ this.dismissAlert();
+ this.showLinksOverride = true;
+ },
+ },
+};
+</script>
+<template>
+ <links-inner
+ v-if="showLinkedLayers"
+ :container-measurements="containerMeasurements"
+ :pipeline-data="pipelineData"
+ v-bind="$attrs"
+ v-on="$listeners"
+ >
+ <slot></slot>
+ </links-inner>
+ <div v-else>
+ <gl-alert
+ v-if="showAlert"
+ class="gl-w-max-content gl-ml-4"
+ :primary-button-text="$options.i18n.showLinksAnyways"
+ @primaryAction="overrideShowLinks"
+ @dismiss="dismissAlert"
+ >
+ {{ $options.i18n.tooManyJobs }}
+ </gl-alert>
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
index 6c957d09e46..8636808b69e 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
+import { generateLinksData } from '../graph_shared/drawing_utils';
import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
-import { generateLinksData } from './drawing_utils';
import { parseData } from '../parsing_utils';
import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
import { createJobsHash, generateJobNeedsDict } from '../../utils';
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 8c9040dce04..133608b9801 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -158,7 +158,7 @@ export default async function () {
);
const { pipelineProjectPath, pipelineIid } = dataset;
- createPipelinesDetailApp(SELECTORS.PIPELINE_DETAILS, pipelineProjectPath, pipelineIid);
+ createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, pipelineProjectPath, pipelineIid);
} catch {
Flash(__('An error occurred while loading the pipeline.'));
}
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 54fcd5502de..50bb23b7e63 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -6,8 +6,6 @@ export const validateParams = (params) => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
-export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;
-
/**
* This function takes the stages array and transform it
* into a hash where each key is a job name and the job data
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 67be76604a3..24386c90954 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -111,6 +111,6 @@ export default {
<template>
<div ref="markdownPreview" class="md-previewer" data-testid="md-previewer">
<gl-skeleton-loading v-if="isLoading" />
- <div v-else class="md" v-html="previewContent"></div>
+ <div v-else class="md gl-ml-auto gl-mr-auto" v-html="previewContent"></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 4dac72df2b5..a4c5ca28494 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -256,7 +256,7 @@ export default {
return {
...filter,
value: {
- data: stripQuotes(valueString),
+ data: typeof valueString === 'string' ? stripQuotes(valueString) : valueString,
operator: filter.value.operator,
},
};
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 7ba9236b833..e3d02d01496 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -99,6 +99,10 @@
line-height: $list-text-height;
color: $gl-text-color-secondary;
+ @include media-breakpoint-down(xs) {
+ padding-top: $gl-padding-6;
+ }
+
span {
margin-right: 15px;
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 15cc10d1532..f6b9473d235 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -179,6 +179,10 @@ $ide-commit-header-height: 48px;
overflow: auto;
padding: $gl-padding;
background-color: var(--ide-empty-state-background, transparent);
+
+ .md {
+ max-width: $limited-layout-width;
+ }
}
.file-container {
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index ff989b474ad..81e9b04b18e 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -1,11 +1,14 @@
@import 'mixins_and_variables_and_functions';
-// We should only import styles that we actually use.
-// @import '@gitlab/ui/src/scss/gitlab_ui';
-
+/**
+NOTE: We should only import styles that we actually use.
+Ex:
+ @import '@gitlab/ui/src/scss/gitlab_ui';
+*/
@import '@gitlab/ui/src/scss/bootstrap';
@import 'bootstrap-vue/src/index';
@import '@gitlab/ui/src/scss/utilities';
+@import '@gitlab/ui/src/components/base/alert/alert';
$atlaskit-border-color: #dfe1e6;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 0c24ea9ccc6..254ad96bb57 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -100,8 +100,6 @@
color: $orange-600;
background-color: $orange-50;
border-radius: $border-radius-default $border-radius-default 0 0;
- border: 1px solid $border-gray-normal;
- border-bottom: 0;
padding: 3px 12px;
margin: auto;
align-items: center;
@@ -454,3 +452,9 @@ table {
.markdown-selector {
color: $blue-600;
}
+
+.comment-warning-wrapper {
+ .md-area {
+ border: 0;
+ }
+}
diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb
new file mode 100644
index 00000000000..dcf4430e55f
--- /dev/null
+++ b/app/graphql/resolvers/package_details_resolver.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Resolvers
+ # No return types defined because they can be different.
+ # rubocop: disable Graphql/ResolverType
+ class PackageDetailsResolver < BaseResolver
+ argument :id, ::Types::GlobalIDType[::Packages::Package],
+ required: true,
+ description: 'The global ID of the package.'
+
+ def resolve(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ::Types::GlobalIDType[::Packages::Package].coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/packages_resolver.rb b/app/graphql/resolvers/packages_resolver.rb
index 519fb87183e..d19099e94d4 100644
--- a/app/graphql/resolvers/packages_resolver.rb
+++ b/app/graphql/resolvers/packages_resolver.rb
@@ -2,7 +2,7 @@
module Resolvers
class PackagesResolver < BaseResolver
- type Types::PackageType, null: true
+ type Types::Packages::PackageType, null: true
def resolve(**args)
return unless packages_available?
diff --git a/app/graphql/types/package_type.rb b/app/graphql/types/package_type.rb
deleted file mode 100644
index 0604bf827a5..00000000000
--- a/app/graphql/types/package_type.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- class PackageType < BaseObject
- graphql_name 'Package'
- description 'Represents a package'
- authorize :read_package
-
- field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the package'
- field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the package'
- field :created_at, Types::TimeType, null: false, description: 'The created date'
- field :updated_at, Types::TimeType, null: false, description: 'The update date'
- field :version, GraphQL::STRING_TYPE, null: true, description: 'The version of the package'
- field :package_type, Types::PackageTypeEnum, null: false, description: 'The type of the package'
- end
-end
diff --git a/app/graphql/types/package_type_enum.rb b/app/graphql/types/package_type_enum.rb
deleted file mode 100644
index 6f50c166da3..00000000000
--- a/app/graphql/types/package_type_enum.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- class PackageTypeEnum < BaseEnum
- PACKAGE_TYPE_NAMES = {
- pypi: 'PyPI',
- npm: 'NPM'
- }.freeze
-
- ::Packages::Package.package_types.keys.each do |package_type|
- type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
- value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s
- end
- end
-end
diff --git a/app/graphql/types/packages/composer/details_type.rb b/app/graphql/types/packages/composer/details_type.rb
new file mode 100644
index 00000000000..8c6845a6fb3
--- /dev/null
+++ b/app/graphql/types/packages/composer/details_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Composer
+ class DetailsType < Types::Packages::PackageType
+ graphql_name 'PackageComposerDetails'
+ description 'Details of a Composer package'
+
+ authorize :read_package
+
+ field :composer_metadatum, Types::Packages::Composer::MetadatumType, null: false, description: 'The Composer metadatum.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/composer/json_type.rb b/app/graphql/types/packages/composer/json_type.rb
new file mode 100644
index 00000000000..b7aa32f0170
--- /dev/null
+++ b/app/graphql/types/packages/composer/json_type.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Composer
+ # rubocop: disable Graphql/AuthorizeTypes
+ class JsonType < BaseObject
+ graphql_name 'PackageComposerJsonType'
+ description 'Represents a composer JSON file'
+
+ field :name, GraphQL::STRING_TYPE, null: true, description: 'The name set in the Composer JSON file.'
+ field :type, GraphQL::STRING_TYPE, null: true, description: 'The type set in the Composer JSON file.'
+ field :license, GraphQL::STRING_TYPE, null: true, description: 'The license set in the Composer JSON file.'
+ field :version, GraphQL::STRING_TYPE, null: true, description: 'The version set in the Composer JSON file.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/composer/metadatum_type.rb b/app/graphql/types/packages/composer/metadatum_type.rb
new file mode 100644
index 00000000000..a97818b1fb8
--- /dev/null
+++ b/app/graphql/types/packages/composer/metadatum_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Composer
+ class MetadatumType < BaseObject
+ graphql_name 'PackageComposerMetadatumType'
+ description 'Composer metadatum'
+
+ authorize :read_package
+
+ field :target_sha, GraphQL::STRING_TYPE, null: false, description: 'Target SHA of the package.'
+ field :composer_json, Types::Packages::Composer::JsonType, null: false, description: 'Data of the Composer JSON file.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_tag_type.rb b/app/graphql/types/packages/package_tag_type.rb
new file mode 100644
index 00000000000..a05ce03da67
--- /dev/null
+++ b/app/graphql/types/packages/package_tag_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageTagType < BaseObject
+ graphql_name 'PackageTag'
+ description 'Represents a package tag'
+ authorize :read_package
+
+ field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the tag.'
+ field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the tag.'
+ field :created_at, Types::TimeType, null: false, description: 'The created date.'
+ field :updated_at, Types::TimeType, null: false, description: 'The updated date.'
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb
new file mode 100644
index 00000000000..b13d16e91c6
--- /dev/null
+++ b/app/graphql/types/packages/package_type.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageType < BaseObject
+ graphql_name 'Package'
+ description 'Represents a package in the Package Registry'
+
+ authorize :read_package
+
+ field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the package.'
+ field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the package.'
+ field :created_at, Types::TimeType, null: false, description: 'The created date.'
+ field :updated_at, Types::TimeType, null: false, description: 'The updated date.'
+ field :version, GraphQL::STRING_TYPE, null: true, description: 'The version of the package.'
+ field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'The type of the package.'
+ field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'The package tags.'
+ field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
+ field :pipelines, Types::Ci::PipelineType.connection_type, null: true, description: 'Pipelines that built the package.'
+ field :versions, Types::Packages::PackageType.connection_type, null: true, description: 'The other versions of the package.'
+
+ def project
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_type_enum.rb b/app/graphql/types/packages/package_type_enum.rb
new file mode 100644
index 00000000000..9713c9d49b1
--- /dev/null
+++ b/app/graphql/types/packages/package_type_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageTypeEnum < BaseEnum
+ PACKAGE_TYPE_NAMES = {
+ pypi: 'PyPI',
+ npm: 'NPM'
+ }.freeze
+
+ ::Packages::Package.package_types.keys.each do |package_type|
+ type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
+ value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 195da01f41f..f66d8926a9f 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -175,7 +175,7 @@ module Types
description: 'A single issue of the project',
resolver: Resolvers::IssuesResolver.single
- field :packages, Types::PackageType.connection_type, null: true,
+ field :packages, Types::Packages::PackageType.connection_type, null: true,
description: 'Packages of the project',
resolver: Resolvers::PackagesResolver
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 05bb371088c..0e0c060f374 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -58,6 +58,11 @@ module Types
argument :id, ::Types::GlobalIDType[::ContainerRepository], required: true, description: 'The global ID of the container repository'
end
+ field :package_composer_details, Types::Packages::Composer::DetailsType,
+ null: true,
+ description: 'Find a composer package',
+ resolver: Resolvers::PackageDetailsResolver
+
field :user, Types::UserType,
null: true,
description: 'Find a user',
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index f1f99771b40..ef3891908f7 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -210,7 +210,8 @@ module Ci
project: downstream_project,
source: :pipeline,
target_revision: {
- ref: target_ref || downstream_project.default_branch
+ ref: target_ref || downstream_project.default_branch,
+ variables_attributes: downstream_variables
},
execute_params: {
ignore_skip_ci: true,
@@ -230,7 +231,8 @@ module Ci
checkout_sha: parent_pipeline.sha,
before: parent_pipeline.before_sha,
source_sha: parent_pipeline.source_sha,
- target_sha: parent_pipeline.target_sha
+ target_sha: parent_pipeline.target_sha,
+ variables_attributes: downstream_variables
},
execute_params: {
ignore_skip_ci: true,
diff --git a/app/models/project.rb b/app/models/project.rb
index b9911fd308b..b03dcde9f3f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -412,6 +412,8 @@ class Project < ApplicationRecord
delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci
delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, to: :ci_cd_settings, prefix: :ci
+ delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, :restrict_user_defined_variables?,
+ to: :ci_cd_settings
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
:allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?,
diff --git a/app/policies/packages/composer/metadatum_policy.rb b/app/policies/packages/composer/metadatum_policy.rb
new file mode 100644
index 00000000000..66bac31f48f
--- /dev/null
+++ b/app/policies/packages/composer/metadatum_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+module Packages
+ module Composer
+ class MetadatumPolicy < BasePolicy
+ delegate { @subject.package }
+ end
+ end
+end
diff --git a/app/policies/packages/tag_policy.rb b/app/policies/packages/tag_policy.rb
new file mode 100644
index 00000000000..84bad30470a
--- /dev/null
+++ b/app/policies/packages/tag_policy.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+module Packages
+ class TagPolicy < BasePolicy
+ delegate { @subject.package }
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index e9bdf88fa5e..03cb53f55be 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy
::Feature.enabled?(:build_service_proxy, @subject)
end
+ condition(:user_defined_variables_allowed) do
+ !@subject.restrict_user_defined_variables?
+ end
+
with_scope :subject
condition(:packages_disabled) { !@subject.packages_enabled }
@@ -616,6 +620,10 @@ class ProjectPolicy < BasePolicy
enable :admin_resource_access_tokens
end
+ rule { user_defined_variables_allowed | can?(:maintainer_access) }.policy do
+ enable :set_pipeline_variables
+ end
+
private
def user_is_user?
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 7c12e0956f3..647a73495f8 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -71,6 +71,10 @@ class IssueEntity < IssuableEntity
expose :archived_project_docs_path, if: -> (issue) { issue.project.archived? } do |issue|
help_page_path('user/project/settings/index.md', anchor: 'archiving-a-project')
end
+
+ expose :issue_email_participants do |issue|
+ issue.issue_email_participants.map { |x| { email: x.email } }
+ end
end
IssueEntity.prepend_if_ee('::EE::IssueEntity')
diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb
index 86d0cf079fc..629d85b041f 100644
--- a/app/services/ci/create_downstream_pipeline_service.rb
+++ b/app/services/ci/create_downstream_pipeline_service.rb
@@ -33,9 +33,7 @@ module Ci
pipeline_params.fetch(:target_revision))
downstream_pipeline = service.execute(
- pipeline_params.fetch(:source), **pipeline_params[:execute_params]) do |pipeline|
- pipeline.variables.build(@bridge.downstream_variables)
- end
+ pipeline_params.fetch(:source), **pipeline_params[:execute_params])
downstream_pipeline.tap do |pipeline|
update_bridge_status!(@bridge, pipeline)
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index d9f41b7040e..a31f5e9056e 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -21,10 +21,10 @@ module Ci
# this check is to not leak the presence of the project if user cannot read it
return unless trigger.project == project
- pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
+ pipeline = Ci::CreatePipelineService
+ .new(project, trigger.owner, ref: params[:ref], variables_attributes: variables)
.execute(:trigger, ignore_skip_ci: true) do |pipeline|
pipeline.trigger_requests.build(trigger: trigger)
- pipeline.variables.build(variables)
end
if pipeline.persisted?
@@ -44,7 +44,8 @@ module Ci
# this check is to not leak the presence of the project if user cannot read it
return unless can?(job.user, :read_project, project)
- pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref])
+ pipeline = Ci::CreatePipelineService
+ .new(project, job.user, ref: params[:ref], variables_attributes: variables)
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
source = job.sourced_pipelines.build(
source_pipeline: job.pipeline,
@@ -53,7 +54,6 @@ module Ci
project: project)
pipeline.source_pipeline = source
- pipeline.variables.build(variables)
end
if pipeline.persisted?
diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb
index 6adeca624a8..ebc980a9053 100644
--- a/app/services/ci/play_build_service.rb
+++ b/app/services/ci/play_build_service.rb
@@ -5,6 +5,10 @@ module Ci
def execute(build, job_variables_attributes = nil)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, build)
+ if job_variables_attributes.present? && !can?(current_user, :set_pipeline_variables, project)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
# Try to enqueue the build, otherwise create a duplicate.
#
if build.enqueue
diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml
index 90a92f5c6d5..30dad19be37 100644
--- a/app/views/jira_connect/subscriptions/index.html.haml
+++ b/app/views/jira_connect/subscriptions/index.html.haml
@@ -9,10 +9,9 @@
= link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in'
.jira-connect-app
- %h1
- GitLab for Jira Configuration
-
- if current_user.blank? && @subscriptions.empty?
+ %h1
+ GitLab for Jira Configuration
%h2.heading-with-border Sign in to GitLab.com to get started.
.gl-mt-5
diff --git a/changelogs/unreleased/13019-improve-error-messages-when-adding-namespaces-in-jira-connect-app.yml b/changelogs/unreleased/13019-improve-error-messages-when-adding-namespaces-in-jira-connect-app.yml
new file mode 100644
index 00000000000..a6724919107
--- /dev/null
+++ b/changelogs/unreleased/13019-improve-error-messages-when-adding-namespaces-in-jira-connect-app.yml
@@ -0,0 +1,5 @@
+---
+title: Improve error messages when adding namespaces in Jira Connect App
+merge_request: 48651
+author:
+type: changed
diff --git a/changelogs/unreleased/285467-package-registry-graphql-api.yml b/changelogs/unreleased/285467-package-registry-graphql-api.yml
new file mode 100644
index 00000000000..7bb0398e25f
--- /dev/null
+++ b/changelogs/unreleased/285467-package-registry-graphql-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add composer details GraphQL type and query
+merge_request: 51059
+author:
+type: added
diff --git a/changelogs/unreleased/297011-add-additional-aliases-for-reviewer-quick-commands.yml b/changelogs/unreleased/297011-add-additional-aliases-for-reviewer-quick-commands.yml
new file mode 100644
index 00000000000..6234ffe87ec
--- /dev/null
+++ b/changelogs/unreleased/297011-add-additional-aliases-for-reviewer-quick-commands.yml
@@ -0,0 +1,5 @@
+---
+title: Adding /reviewer and /remove_reviewer aliases and specs
+merge_request: 51384
+author:
+type: added
diff --git a/changelogs/unreleased/ajk-297358-fix-preloaded-pagination.yml b/changelogs/unreleased/ajk-297358-fix-preloaded-pagination.yml
new file mode 100644
index 00000000000..a882c0ffd1e
--- /dev/null
+++ b/changelogs/unreleased/ajk-297358-fix-preloaded-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Generate page-info for connections of preloaded associations
+merge_request: 51642
+author:
+type: fixed
diff --git a/changelogs/unreleased/ci-setting-prevent-user-defined-ci-variables.yml b/changelogs/unreleased/ci-setting-prevent-user-defined-ci-variables.yml
new file mode 100644
index 00000000000..3ae1c06e04a
--- /dev/null
+++ b/changelogs/unreleased/ci-setting-prevent-user-defined-ci-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent user-defined variables from being used by non-maintainers
+merge_request: 51682
+author:
+type: security
diff --git a/changelogs/unreleased/fix-admin-project-overview-badge-alignment.yml b/changelogs/unreleased/fix-admin-project-overview-badge-alignment.yml
new file mode 100644
index 00000000000..f88f11bda54
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-project-overview-badge-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix admin project overview badge alignment
+merge_request: 51066
+author: Kev @KevSlashNull
+type: fixed
diff --git a/changelogs/unreleased/webide-markdown-center.yml b/changelogs/unreleased/webide-markdown-center.yml
new file mode 100644
index 00000000000..095f6a81251
--- /dev/null
+++ b/changelogs/unreleased/webide-markdown-center.yml
@@ -0,0 +1,5 @@
+---
+title: Centered Markdown Preview in Web IDE with a set max width to limit the container size
+merge_request: 50291
+author: Mehul Sharma
+type: other
diff --git a/db/migrate/20201223114050_add_restrict_user_defined_variables_to_project_settings.rb b/db/migrate/20201223114050_add_restrict_user_defined_variables_to_project_settings.rb
new file mode 100644
index 00000000000..d04c6981bf9
--- /dev/null
+++ b/db/migrate/20201223114050_add_restrict_user_defined_variables_to_project_settings.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddRestrictUserDefinedVariablesToProjectSettings < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_column :project_ci_cd_settings, :restrict_user_defined_variables, :boolean, default: false, null: false
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :project_ci_cd_settings, :restrict_user_defined_variables
+ end
+ end
+end
diff --git a/db/schema_migrations/20201223114050 b/db/schema_migrations/20201223114050
new file mode 100644
index 00000000000..25ac0eac211
--- /dev/null
+++ b/db/schema_migrations/20201223114050
@@ -0,0 +1 @@
+35acb5bbabfd12f97c988776aafa6ff380e2cbe2267e856b8f439f7102a6fbf2 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3e1723463d0..de4218ed405 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -15572,7 +15572,8 @@ CREATE TABLE project_ci_cd_settings (
forward_deployment_enabled boolean,
merge_trains_enabled boolean DEFAULT false,
auto_rollback_enabled boolean DEFAULT false NOT NULL,
- keep_latest_artifact boolean DEFAULT true NOT NULL
+ keep_latest_artifact boolean DEFAULT true NOT NULL,
+ restrict_user_defined_variables boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE project_ci_cd_settings_id_seq
diff --git a/doc/administration/postgresql/replication_and_failover.md b/doc/administration/postgresql/replication_and_failover.md
index efc5b5ada81..75d0c558962 100644
--- a/doc/administration/postgresql/replication_and_failover.md
+++ b/doc/administration/postgresql/replication_and_failover.md
@@ -714,28 +714,6 @@ consul['configuration'] = {
The manual steps for this configuration are the same as for the [example recommended setup](#example-recommended-setup-manual-steps).
-### Manual failover procedure for Patroni
-
-While Patroni supports automatic failover, you also have the ability to perform
-a manual one, where you have two slightly different options:
-
-- **Failover**: allows you to perform a manual failover when there are no healthy nodes.
- You can perform this action in any PostgreSQL node:
-
- ```shell
- sudo gitlab-ctl patroni failover
- ```
-
-- **Switchover**: only works when the cluster is healthy and allows you to schedule a switchover (it can happen immediately).
- You can perform this action in any PostgreSQL node:
-
- ```shell
- sudo gitlab-ctl patroni switchover
- ```
-
-For further details on this subject, see the
-[Patroni documentation](https://patroni.readthedocs.io/en/latest/rest_api.html#switchover-and-failover-endpoints).
-
## Patroni
NOTE:
@@ -828,6 +806,38 @@ want to signal Patroni to reload its configuration or restart PostgreSQL process
must use the `reload` or `restart` sub-commands of `gitlab-ctl patroni` instead. These two sub-commands are wrappers of
the same `patronictl` commands.
+### Manual failover procedure for Patroni
+
+While Patroni supports automatic failover, you also have the ability to perform
+a manual one, where you have two slightly different options:
+
+- **Failover**: allows you to perform a manual failover when there are no healthy nodes.
+ You can perform this action in any PostgreSQL node:
+
+ ```shell
+ sudo gitlab-ctl patroni failover
+ ```
+
+- **Switchover**: only works when the cluster is healthy and allows you to schedule a switchover (it can happen immediately).
+ You can perform this action in any PostgreSQL node:
+
+ ```shell
+ sudo gitlab-ctl patroni switchover
+ ```
+
+For further details on this subject, see the
+[Patroni documentation](https://patroni.readthedocs.io/en/latest/rest_api.html#switchover-and-failover-endpoints).
+
+#### Geo secondary site considerations
+
+Similar to `repmgr`, when a Geo secondary site is replicating from a primary site that uses `Patroni` and `PgBouncer`, [replicating through PgBouncer is not supported](https://github.com/pgbouncer/pgbouncer/issues/382#issuecomment-517911529) and the secondary must replicate directly from the leader node in the `Patroni` cluster. Therefore, when there is an automatic or manual failover in the `Patroni` cluster, you will need to manually re-point your secondary site to replicate from the new leader with:
+
+```shell
+sudo gitlab-ctl replicate-geo-database --host=<new_leader_ip> --replication-slot=<slot_name>
+```
+
+Otherwise, the replication will not happen anymore, even if the original node gets re-added as a follower node. This will re-sync your secondary site database and may take a long time depending on the amount of data to sync. You may also need to run `gitlab-ctl reconfigure` if replication is still not working after re-syncing.
+
### Recovering the Patroni cluster
To recover the old primary and rejoin it to the cluster as a replica, you can simply start Patroni with:
@@ -1222,7 +1232,7 @@ When a Geo secondary site is replicating from a primary site that uses `repmgr`
sudo gitlab-ctl replicate-geo-database --host=<new_leader_ip> --replication-slot=<slot_name>
```
-Otherwise, the replication will not happen anymore, even if the original node gets re-added as a follower node. This will re-sync your secondary site database and may take a long time depending on the amount of data to sync.
+Otherwise, the replication will not happen anymore, even if the original node gets re-added as a follower node. This will re-sync your secondary site database and may take a long time depending on the amount of data to sync. You may also need to run `gitlab-ctl reconfigure` if replication is still not working after re-syncing.
### Repmgr Restore procedure
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index d71fabdd6d3..8c27a3b72d9 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -16521,38 +16521,278 @@ input OncallUserInputType {
}
"""
-Represents a package
+Represents a package in the Package Registry
"""
type Package {
"""
- The created date
+ The created date.
"""
createdAt: Time!
"""
- The ID of the package
+ The ID of the package.
"""
id: ID!
"""
- The name of the package
+ The name of the package.
"""
name: String!
"""
- The type of the package
+ The type of the package.
"""
packageType: PackageTypeEnum!
"""
- The update date
+ Pipelines that built the package.
+ """
+ pipelines(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PipelineConnection
+
+ """
+ Project where the package is stored.
+ """
+ project: Project!
+
+ """
+ The package tags.
+ """
+ tags(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PackageTagConnection
+
+ """
+ The updated date.
"""
updatedAt: Time!
"""
- The version of the package
+ The version of the package.
"""
version: String
+
+ """
+ The other versions of the package.
+ """
+ versions(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PackageConnection
+}
+
+"""
+Details of a Composer package
+"""
+type PackageComposerDetails {
+ """
+ The Composer metadatum.
+ """
+ composerMetadatum: PackageComposerMetadatumType!
+
+ """
+ The created date.
+ """
+ createdAt: Time!
+
+ """
+ The ID of the package.
+ """
+ id: ID!
+
+ """
+ The name of the package.
+ """
+ name: String!
+
+ """
+ The type of the package.
+ """
+ packageType: PackageTypeEnum!
+
+ """
+ Pipelines that built the package.
+ """
+ pipelines(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PipelineConnection
+
+ """
+ Project where the package is stored.
+ """
+ project: Project!
+
+ """
+ The package tags.
+ """
+ tags(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PackageTagConnection
+
+ """
+ The updated date.
+ """
+ updatedAt: Time!
+
+ """
+ The version of the package.
+ """
+ version: String
+
+ """
+ The other versions of the package.
+ """
+ versions(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PackageConnection
+}
+
+"""
+Represents a composer JSON file
+"""
+type PackageComposerJsonType {
+ """
+ The license set in the Composer JSON file.
+ """
+ license: String
+
+ """
+ The name set in the Composer JSON file.
+ """
+ name: String
+
+ """
+ The type set in the Composer JSON file.
+ """
+ type: String
+
+ """
+ The version set in the Composer JSON file.
+ """
+ version: String
+}
+
+"""
+Composer metadatum
+"""
+type PackageComposerMetadatumType {
+ """
+ Data of the Composer JSON file.
+ """
+ composerJson: PackageComposerJsonType!
+
+ """
+ Target SHA of the package.
+ """
+ targetSha: String!
}
"""
@@ -16686,6 +16926,66 @@ type PackageSettings {
mavenDuplicatesAllowed: Boolean!
}
+"""
+Represents a package tag
+"""
+type PackageTag {
+ """
+ The created date.
+ """
+ createdAt: Time!
+
+ """
+ The ID of the tag.
+ """
+ id: ID!
+
+ """
+ The name of the tag.
+ """
+ name: String!
+
+ """
+ The updated date.
+ """
+ updatedAt: Time!
+}
+
+"""
+The connection type for PackageTag.
+"""
+type PackageTagConnection {
+ """
+ A list of edges.
+ """
+ edges: [PackageTagEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [PackageTag]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type PackageTagEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: PackageTag
+}
+
enum PackageTypeEnum {
"""
Packages from the Composer package manager
@@ -16734,6 +17034,11 @@ enum PackageTypeEnum {
}
"""
+Identifier of Packages::Package
+"""
+scalar PackagesPackageID
+
+"""
Information about pagination in a connection.
"""
type PageInfo {
@@ -19868,6 +20173,16 @@ type Query {
): Namespace
"""
+ Find a composer package
+ """
+ packageComposerDetails(
+ """
+ The global ID of the package.
+ """
+ id: PackagesPackageID!
+ ): PackageComposerDetails
+
+ """
Find a project
"""
project(
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 6792f6cd07e..6ae3f9afda7 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -48921,11 +48921,11 @@
{
"kind": "OBJECT",
"name": "Package",
- "description": "Represents a package",
+ "description": "Represents a package in the Package Registry",
"fields": [
{
"name": "createdAt",
- "description": "The created date",
+ "description": "The created date.",
"args": [
],
@@ -48943,7 +48943,7 @@
},
{
"name": "id",
- "description": "The ID of the package",
+ "description": "The ID of the package.",
"args": [
],
@@ -48961,7 +48961,7 @@
},
{
"name": "name",
- "description": "The name of the package",
+ "description": "The name of the package.",
"args": [
],
@@ -48979,7 +48979,7 @@
},
{
"name": "packageType",
- "description": "The type of the package",
+ "description": "The type of the package.",
"args": [
],
@@ -48996,8 +48996,132 @@
"deprecationReason": null
},
{
+ "name": "pipelines",
+ "description": "Pipelines that built the package.",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PipelineConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "Project where the package is stored.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tags",
+ "description": "The package tags.",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageTagConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "updatedAt",
- "description": "The update date",
+ "description": "The updated date.",
"args": [
],
@@ -49015,7 +49139,7 @@
},
{
"name": "version",
- "description": "The version of the package",
+ "description": "The version of the package.",
"args": [
],
@@ -49026,6 +49150,489 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "versions",
+ "description": "The other versions of the package.",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageComposerDetails",
+ "description": "Details of a Composer package",
+ "fields": [
+ {
+ "name": "composerMetadatum",
+ "description": "The Composer metadatum.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PackageComposerMetadatumType",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": "The created date.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "The ID of the package.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "The name of the package.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "packageType",
+ "description": "The type of the package.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "PackageTypeEnum",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pipelines",
+ "description": "Pipelines that built the package.",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PipelineConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "Project where the package is stored.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tags",
+ "description": "The package tags.",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageTagConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "The updated date.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "version",
+ "description": "The version of the package.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "versions",
+ "description": "The other versions of the package.",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageComposerJsonType",
+ "description": "Represents a composer JSON file",
+ "fields": [
+ {
+ "name": "license",
+ "description": "The license set in the Composer JSON file.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "The name set in the Composer JSON file.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": "The type set in the Composer JSON file.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "version",
+ "description": "The version set in the Composer JSON file.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageComposerMetadatumType",
+ "description": "Composer metadatum",
+ "fields": [
+ {
+ "name": "composerJson",
+ "description": "Data of the Composer JSON file.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PackageComposerJsonType",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "targetSha",
+ "description": "Target SHA of the package.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -49438,6 +50045,203 @@
"possibleTypes": null
},
{
+ "kind": "OBJECT",
+ "name": "PackageTag",
+ "description": "Represents a package tag",
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": "The created date.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "The ID of the tag.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "The name of the tag.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "The updated date.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageTagConnection",
+ "description": "The connection type for PackageTag.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PackageTagEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PackageTag",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageTagEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageTag",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "ENUM",
"name": "PackageTypeEnum",
"description": null,
@@ -49503,6 +50307,16 @@
"possibleTypes": null
},
{
+ "kind": "SCALAR",
+ "name": "PackagesPackageID",
+ "description": "Identifier of Packages::Package",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "PageInfo",
"description": "Information about pagination in a connection.",
@@ -57938,6 +58752,33 @@
"deprecationReason": null
},
{
+ "name": "packageComposerDetails",
+ "description": "Find a composer package",
+ "args": [
+ {
+ "name": "id",
+ "description": "The global ID of the package.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "PackagesPackageID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageComposerDetails",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "project",
"description": "Find a project",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4930b136894..0a732c59832 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2496,16 +2496,58 @@ Autogenerated return type of OncallScheduleUpdate.
### Package
-Represents a package.
+Represents a package in the Package Registry.
| Field | Type | Description |
| ----- | ---- | ----------- |
-| `createdAt` | Time! | The created date |
-| `id` | ID! | The ID of the package |
-| `name` | String! | The name of the package |
-| `packageType` | PackageTypeEnum! | The type of the package |
-| `updatedAt` | Time! | The update date |
-| `version` | String | The version of the package |
+| `createdAt` | Time! | The created date. |
+| `id` | ID! | The ID of the package. |
+| `name` | String! | The name of the package. |
+| `packageType` | PackageTypeEnum! | The type of the package. |
+| `pipelines` | PipelineConnection | Pipelines that built the package. |
+| `project` | Project! | Project where the package is stored. |
+| `tags` | PackageTagConnection | The package tags. |
+| `updatedAt` | Time! | The updated date. |
+| `version` | String | The version of the package. |
+| `versions` | PackageConnection | The other versions of the package. |
+
+### PackageComposerDetails
+
+Details of a Composer package.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `composerMetadatum` | PackageComposerMetadatumType! | The Composer metadatum. |
+| `createdAt` | Time! | The created date. |
+| `id` | ID! | The ID of the package. |
+| `name` | String! | The name of the package. |
+| `packageType` | PackageTypeEnum! | The type of the package. |
+| `pipelines` | PipelineConnection | Pipelines that built the package. |
+| `project` | Project! | Project where the package is stored. |
+| `tags` | PackageTagConnection | The package tags. |
+| `updatedAt` | Time! | The updated date. |
+| `version` | String | The version of the package. |
+| `versions` | PackageConnection | The other versions of the package. |
+
+### PackageComposerJsonType
+
+Represents a composer JSON file.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `license` | String | The license set in the Composer JSON file. |
+| `name` | String | The name set in the Composer JSON file. |
+| `type` | String | The type set in the Composer JSON file. |
+| `version` | String | The version set in the Composer JSON file. |
+
+### PackageComposerMetadatumType
+
+Composer metadatum.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `composerJson` | PackageComposerJsonType! | Data of the Composer JSON file. |
+| `targetSha` | String! | Target SHA of the package. |
### PackageFileRegistry
@@ -2531,6 +2573,17 @@ Namespace-level Package Registry settings.
| `mavenDuplicateExceptionRegex` | UntrustedRegexp | When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect. |
| `mavenDuplicatesAllowed` | Boolean! | Indicates whether duplicate Maven packages are allowed for this namespace. |
+### PackageTag
+
+Represents a package tag.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `createdAt` | Time! | The created date. |
+| `id` | ID! | The ID of the tag. |
+| `name` | String! | The name of the tag. |
+| `updatedAt` | Time! | The updated date. |
+
### PageInfo
Information about pagination in a connection..
diff --git a/doc/api/projects.md b/doc/api/projects.md
index a344f53a2b1..f9a4b3ba55e 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -160,6 +160,7 @@ When the user is authenticated and `simple` is not set this returns something li
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -253,6 +254,7 @@ When the user is authenticated and `simple` is not set this returns something li
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -418,6 +420,7 @@ GET /users/:user_id/projects
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -511,6 +514,7 @@ GET /users/:user_id/projects
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -640,6 +644,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -726,6 +731,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -881,6 +887,7 @@ GET /projects/:id
"repository_storage": "default",
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"printing_merge_requests_link_enabled": true,
@@ -1234,6 +1241,7 @@ PUT /projects/:id
| `packages_enabled` | boolean | **{dotted-circle}** No | Enable or disable packages repository feature. |
| `pages_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled`, or `public`. |
| `requirements_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, `enabled` or `public` |
+| `restrict_user_defined_variables` | boolean | **{dotted-circle}** No | Allow only maintainers to pass user-defined variables when triggering a pipeline. For example when the pipeline is triggered in the UI, with the API, or by a trigger token. |
| `path` | string | **{dotted-circle}** No | Custom repository name for the project. By default generated based on name. |
| `public_builds` | boolean | **{dotted-circle}** No | If `true`, jobs can be viewed by non-project members. |
| `remove_source_branch_after_merge` | boolean | **{dotted-circle}** No | Enable `Delete source branch` option by default for all new merge requests. |
@@ -1356,6 +1364,7 @@ Example responses:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -1449,6 +1458,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -1540,6 +1550,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -1725,6 +1736,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -1837,6 +1849,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": false,
+ "restrict_user_defined_variables": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
@@ -2397,6 +2410,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"allow_merge_on_skipped_pipeline": null,
+ "restrict_user_defined_variables": false,
"request_access_enabled": true,
"only_allow_merge_if_all_discussions_are_resolved": false,
"remove_source_branch_after_merge": true,
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 4d1b21cfe46..3fd388c899d 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -594,7 +594,35 @@ WARNING:
Variables with multi-line values are not supported due to
limitations with the Auto DevOps scripting environment.
-### Override a variable by manually running a pipeline
+### When you can override variables
+
+You can override the value of a variable when:
+
+1. [Manually running](#override-a-variable-by-manually-running-a-pipeline) pipelines in the UI.
+1. Manually creating pipelines [via API](../../api/pipelines.md#create-a-new-pipeline).
+1. Manually playing a job via the UI.
+1. Using [push options](../../user/project/push_options.md#push-options-for-gitlab-cicd).
+1. Manually triggering pipelines with [the API](../triggers/README.md#making-use-of-trigger-variables).
+1. Passing variables to a [downstream pipeline](../multi_project_pipelines.md#passing-variables-to-a-downstream-pipeline).
+
+These pipeline variables declared in these events take [priority over other variables](#priority-of-environment-variables).
+
+#### Restrict who can override variables
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/295234) in GitLab 13.8.
+
+To allow only users with Maintainer role to set these variables, you can use
+[the API](../../api/projects.md#edit-project) to enable the project setting `restrict_user_defined_variables`.
+When a user without Maintainer role tries to run a pipeline with overridden
+variables, an `Insufficient permissions to set pipeline variables` error occurs.
+
+The setting is `disabled` by default.
+
+If you [store your CI configurations in a different repository](../../ci/pipelines/settings.md#custom-ci-configuration-path),
+use this setting for strict control over all aspects of the environment
+the pipeline runs in.
+
+#### Override a variable by manually running a pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/44059) in GitLab 10.8.
diff --git a/doc/install/README.md b/doc/install/README.md
index 7b037da4c4a..7ed478439e0 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -17,7 +17,7 @@ troubleshooting), and the cost of hosting.
Depending on your platform, select from the following available methods to
install GitLab:
-- [_Omnibus GitLab_](#installing-gitlab-using-the-omnibus-gitlab-package-recommended):
+- [_Omnibus GitLab_](#installing-gitlab-on-linux-using-the-omnibus-gitlab-package-recommended):
The official deb/rpm packages that contain a bundle of GitLab and the
components it depends on, including PostgreSQL, Redis, and Sidekiq.
- [_GitLab Helm chart_](#installing-gitlab-on-kubernetes-via-the-gitlab-helm-charts):
@@ -42,16 +42,24 @@ Before you install GitLab, be sure to review the [system requirements](requireme
The system requirements include details about the minimum hardware, software,
database, and additional requirements to support GitLab.
-## Installing GitLab using the Omnibus GitLab package (recommended)
+## Installing GitLab on Linux using the Omnibus GitLab package (recommended)
The Omnibus GitLab package uses our official deb/rpm repositories, and is
recommended for most users.
-If you need additional flexibility and resilience, we recommend deploying
+If you need additional scale or resilience, we recommend deploying
GitLab as described in our [reference architecture documentation](../administration/reference_architectures/index.md).
[**> Install GitLab using the Omnibus GitLab package.**](https://about.gitlab.com/install/)
+### GitLab Environment Toolkit (alpha)
+
+The [GitLab Environment Toolkit](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit) provides a set of automation tools to easily deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers.
+
+It is currently in alpha, and is not recommended for production use.
+
+[**> Install a GitLab reference architecture using the GitLab Environment Toolkit.**](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit#documentation)
+
## Installing GitLab on Kubernetes via the GitLab Helm charts
When installing GitLab on Kubernetes, there are some trade-offs that you
@@ -95,8 +103,7 @@ the above methods, provided the cloud provider supports it.
- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md): Install Omnibus GitLab on a VM in GCP.
- [Install GitLab on Azure](azure/index.md): Install Omnibus GitLab from Azure Marketplace.
- [Install GitLab on OpenShift](https://docs.gitlab.com/charts/installation/cloud/openshift.html): Install GitLab on OpenShift by using the GitLab Helm charts.
-- [Install GitLab on DC/OS](https://d2iq.com/blog/gitlab-dcos): Install GitLab on Mesosphere DC/OS via the [GitLab-Mesosphere integration](https://about.gitlab.com/blog/2016/09/16/announcing-gitlab-and-mesosphere/).
-- [Install GitLab on DigitalOcean](https://about.gitlab.com/blog/2016/04/27/getting-started-with-gitlab-and-digitalocean/): Install Omnibus GitLab on DigitalOcean.
+- [Install GitLab on DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-gitlab-on-ubuntu-18-04): Install Omnibus GitLab on DigitalOcean.
- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md):
Quickly test any version of GitLab on DigitalOcean using Docker Machine.
diff --git a/doc/user/group/roadmap/img/roadmap_filters_v13_7.png b/doc/user/group/roadmap/img/roadmap_filters_v13_7.png
deleted file mode 100644
index 00505a7f34f..00000000000
--- a/doc/user/group/roadmap/img/roadmap_filters_v13_7.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_filters_v13_8.png b/doc/user/group/roadmap/img/roadmap_filters_v13_8.png
new file mode 100644
index 00000000000..d826909b022
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_filters_v13_8.png
Binary files differ
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 6dfa7641dbb..f3b7be536ae 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -67,8 +67,9 @@ You can also filter epics in the Roadmap view by:
- Author
- Label
- Milestone
+- Confidentiality of epics
-![roadmap date range in weeks](img/roadmap_filters_v13_7.png)
+![roadmap date range in weeks](img/roadmap_filters_v13_8.png)
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index 317caefe0a1..6ad6123a20e 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -100,6 +100,7 @@ module API
end
expose :only_allow_merge_if_pipeline_succeeds
expose :allow_merge_on_skipped_pipeline
+ expose :restrict_user_defined_variables
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
expose :remove_source_branch_after_merge
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 3115e968e84..cf2bcace33b 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -87,6 +87,7 @@ module API
params :optional_update_params_ce do
optional :ci_forward_deployment_enabled, type: Boolean, desc: 'Skip older deployment jobs that are still pending'
+ optional :restrict_user_defined_variables, type: Boolean, desc: 'Restrict use of user-defined variables when triggering a pipeline'
end
params :optional_update_params_ee do
@@ -141,6 +142,7 @@ module API
:repository_access_level,
:request_access_enabled,
:resolve_outdated_diff_discussions,
+ :restrict_user_defined_variables,
:shared_runners_enabled,
:snippets_access_level,
:tag_list,
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index c68419c8aa4..f0548284001 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -5,6 +5,9 @@ module Gitlab
module Pipeline
module Chain
class Build < Chain::Base
+ include Gitlab::Allowable
+ include Chain::Helpers
+
def perform!
@pipeline.assign_attributes(
source: @command.source,
@@ -20,13 +23,34 @@ module Gitlab
pipeline_schedule: @command.schedule,
merge_request: @command.merge_request,
external_pull_request: @command.external_pull_request,
- variables_attributes: Array(@command.variables_attributes),
- locked: @command.project.latest_pipeline_locked
+ locked: @command.project.latest_pipeline_locked,
+ variables_attributes: variables_attributes
)
end
def break?
- false
+ @pipeline.errors.any?
+ end
+
+ private
+
+ def variables_attributes
+ variables = Array(@command.variables_attributes)
+
+ # We allow parent pipelines to pass variables to child pipelines since
+ # these variables are coming from internal configurations. We will check
+ # permissions to :set_pipeline_variables when those are injected upstream,
+ # to the parent pipeline.
+ # In other scenarios (e.g. multi-project pipelines or run pipeline via UI)
+ # the variables are provided from the outside and those should be guarded.
+ return variables if @command.creates_child_pipeline?
+
+ if variables.present? && !can?(@command.current_user, :set_pipeline_variables, @command.project)
+ error("Insufficient permissions to set pipeline variables")
+ variables = []
+ end
+
+ variables
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index d05be54267c..815fe6bac6d 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -79,6 +79,10 @@ module Gitlab
bridge&.parent_pipeline
end
+ def creates_child_pipeline?
+ bridge&.triggers_child_pipeline?
+ end
+
def metrics
@metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new
end
diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb
index 2ad8d2f7ab7..f95c91c5706 100644
--- a/lib/gitlab/graphql/pagination/keyset/connection.rb
+++ b/lib/gitlab/graphql/pagination/keyset/connection.rb
@@ -67,9 +67,14 @@ module Gitlab
# next page
true
elsif first
- # If we count the number of requested items plus one (`limit_value + 1`),
- # then if we get `limit_value + 1` then we know there is a next page
- relation_count(set_limit(sliced_nodes, limit_value + 1)) == limit_value + 1
+ case sliced_nodes
+ when Array
+ sliced_nodes.size > limit_value
+ else
+ # If we count the number of requested items plus one (`limit_value + 1`),
+ # then if we get `limit_value + 1` then we know there is a next page
+ relation_count(set_limit(sliced_nodes, limit_value + 1)) == limit_value + 1
+ end
else
false
end
@@ -157,8 +162,8 @@ module Gitlab
list = OrderInfo.build_order_list(items)
- if loaded?(items)
- @order_list = list.presence || [items.primary_key]
+ if loaded?(items) && !before.present? && !after.present?
+ @order_list = list.presence || [OrderInfo.new(items.primary_key)]
# already sorted, or trivially sorted
next items if list.present? || items.size <= 1
@@ -194,7 +199,7 @@ module Gitlab
ordering = { 'id' => node[:id].to_s }
order_list.each do |field|
- field_name = field.attribute_name
+ field_name = field.try(:attribute_name) || field
field_value = node[field_name]
ordering[field_name] = if field_value.is_a?(Time)
field_value.strftime('%Y-%m-%d %H:%M:%S.%N %Z')
diff --git a/lib/gitlab/graphql/pagination/keyset/query_builder.rb b/lib/gitlab/graphql/pagination/keyset/query_builder.rb
index 331981ce723..29169449843 100644
--- a/lib/gitlab/graphql/pagination/keyset/query_builder.rb
+++ b/lib/gitlab/graphql/pagination/keyset/query_builder.rb
@@ -40,7 +40,10 @@ module Gitlab
# "issues"."id" > 500
#
def conditions
- attr_values = order_list.map { |field| decoded_cursor[field.attribute_name] }
+ attr_values = order_list.map do |field|
+ name = field.try(:attribute_name) || field
+ decoded_cursor[name]
+ end
if order_list.count == 1 && attr_values.first.nil?
raise Gitlab::Graphql::Errors::ArgumentError.new('Before/after cursor invalid: `nil` was provided as only sortable value')
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index 31cc3f930c0..b56fd8278a1 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -163,16 +163,17 @@ module Gitlab
end
end
explanation do |users|
+ reviewers = reviewers_to_add(users)
_('Assigns %{reviewer_users_sentence} as %{reviewer_text}.') % { reviewer_users_sentence: reviewer_users_sentence(users),
- reviewer_text: 'reviewer'.pluralize(users.size) }
+ reviewer_text: 'reviewer'.pluralize(reviewers.size) }
end
execution_message do |users = nil|
- if users.blank?
+ reviewers = reviewers_to_add(users)
+ if reviewers.blank?
_("Failed to assign a reviewer because no user was found.")
else
- users = [users.first] unless quick_action_target.allows_multiple_reviewers?
_('Assigned %{reviewer_users_sentence} as %{reviewer_text}.') % { reviewer_users_sentence: reviewer_users_sentence(users),
- reviewer_text: 'reviewer'.pluralize(users.size) }
+ reviewer_text: 'reviewer'.pluralize(reviewers.size) }
end
end
params do
@@ -186,7 +187,7 @@ module Gitlab
parse_params do |reviewer_param|
extract_users(reviewer_param)
end
- command :assign_reviewer do |users|
+ command :assign_reviewer, :reviewer do |users|
next if users.empty?
if quick_action_target.allows_multiple_reviewers?
@@ -228,7 +229,7 @@ module Gitlab
# When multiple users are assigned, all will be unassigned if multiple reviewers are no longer allowed
extract_users(unassign_reviewer_param) if quick_action_target.allows_multiple_reviewers?
end
- command :unassign_reviewer do |users = nil|
+ command :unassign_reviewer, :remove_reviewer do |users = nil|
if quick_action_target.allows_multiple_reviewers? && users&.any?
@updates[:reviewer_ids] ||= quick_action_target.reviewers.map(&:id)
@updates[:reviewer_ids] -= users.map(&:id)
@@ -239,11 +240,7 @@ module Gitlab
end
def reviewer_users_sentence(users)
- if quick_action_target.allows_multiple_reviewers?
- users
- else
- [users.first]
- end.map(&:to_reference).to_sentence
+ reviewers_to_add(users).map(&:to_reference).to_sentence
end
def reviewers_for_removal(users)
@@ -255,6 +252,16 @@ module Gitlab
end
end
+ def reviewers_to_add(users)
+ return if users.blank?
+
+ if quick_action_target.allows_multiple_reviewers?
+ users
+ else
+ [users.first]
+ end
+ end
+
def merge_orchestration_service
@merge_orchestration_service ||= MergeRequests::MergeOrchestrationService.new(project, current_user)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f218030fcf4..c5ed8add78a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10617,6 +10617,15 @@ msgstr ""
msgid "EmailError|Your account has been blocked. If you believe this is in error, contact a staff member."
msgstr ""
+msgid "EmailParticipantsWarning|%{emails} will be notified of your comment."
+msgstr ""
+
+msgid "EmailParticipantsWarning|%{emails}, %{andMore} will be notified of your comment."
+msgstr ""
+
+msgid "EmailParticipantsWarning|and %{moreCount} more"
+msgstr ""
+
msgid "EmailToken|reset it"
msgstr ""
@@ -25947,6 +25956,9 @@ msgstr ""
msgid "Show latest version"
msgstr ""
+msgid "Show links anyways"
+msgstr ""
+
msgid "Show list"
msgstr ""
@@ -28799,6 +28811,9 @@ msgstr ""
msgid "This field is required."
msgstr ""
+msgid "This graph has a large number of jobs and showing the links between them may have performance implications."
+msgstr ""
+
msgid "This group"
msgstr ""
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 12c8c84dd77..65d8babc837 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -227,6 +227,22 @@ RSpec.describe Projects::IssuesController do
end
end
+ describe "GET #show" do
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ it "returns issue_email_participants" do
+ participants = create_list(:issue_email_participant, 2, issue: issue)
+
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['issue_email_participants']).to contain_exactly({ "email" => participants[0].email }, { "email" => participants[1].email })
+ end
+ end
+
describe 'GET #new' do
it 'redirects to signin if not logged in' do
get :new, params: { namespace_id: project.namespace, project_id: project }
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index def60c9cb69..f5e496080c4 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -45,6 +45,7 @@ FactoryBot.define do
import_correlation_id { nil }
import_last_error { nil }
forward_deployment_enabled { nil }
+ restrict_user_defined_variables { nil }
end
before(:create) do |project, evaluator|
@@ -84,6 +85,7 @@ FactoryBot.define do
project.merge_pipelines_enabled = evaluator.merge_pipelines_enabled unless evaluator.merge_pipelines_enabled.nil?
project.merge_trains_enabled = evaluator.merge_trains_enabled unless evaluator.merge_trains_enabled.nil?
project.ci_keep_latest_artifact = evaluator.ci_keep_latest_artifact unless evaluator.ci_keep_latest_artifact.nil?
+ project.restrict_user_defined_variables = evaluator.restrict_user_defined_variables unless evaluator.restrict_user_defined_variables.nil?
if evaluator.import_status
import_state = project.import_state || project.build_import_state
diff --git a/spec/features/projects/issues/email_participants_spec.rb b/spec/features/projects/issues/email_participants_spec.rb
new file mode 100644
index 00000000000..3ffe0a5ced8
--- /dev/null
+++ b/spec/features/projects/issues/email_participants_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'viewing an issue', :js do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:note) { create(:note_on_issue, project: project, noteable: issue) }
+ let_it_be(:participants) { create_list(:issue_email_participant, 4, issue: issue) }
+
+ before do
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ shared_examples 'email participants warning' do |selector|
+ it 'shows the correct message' do
+ expect(find(selector)).to have_content(", and 1 more will be notified of your comment")
+ end
+ end
+
+ context 'for a new note' do
+ it_behaves_like 'email participants warning', '.new-note'
+ end
+
+ context 'for a reply form' do
+ before do
+ find('.js-reply-button').click
+ end
+
+ it_behaves_like 'email participants warning', '.note-edit-form'
+ end
+end
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json b/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json
new file mode 100644
index 00000000000..bcf64a6e567
--- /dev/null
+++ b/spec/fixtures/api/schemas/graphql/packages/package_composer_details.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "allOf": [{ "$ref": "./package_details.json" }],
+ "properties": {
+ "target_sha": {
+ "type": "string"
+ },
+ "composer_json": {
+ "type": "object"
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
new file mode 100644
index 00000000000..4f90285183c
--- /dev/null
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -0,0 +1,36 @@
+{
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "version": {
+ "type": ["string", "null"]
+ },
+ "package_type": {
+ "type": ["string"],
+ "enum": ["MAVEN", "NPM", "CONAN", "NUGET", "PYPI", "COMPOSER", "GENERIC", "GOLANG", "DEBIAN"]
+ },
+ "tags": {
+ "type": "object"
+ },
+ "project": {
+ "type": "object"
+ },
+ "pipelines": {
+ "type": "object"
+ },
+ "versions": {
+ "type": "object"
+ }
+ }
+}
diff --git a/spec/frontend/jira_connect/components/app_spec.js b/spec/frontend/jira_connect/components/app_spec.js
index 42ce14e397a..be990d5061c 100644
--- a/spec/frontend/jira_connect/components/app_spec.js
+++ b/spec/frontend/jira_connect/components/app_spec.js
@@ -1,16 +1,28 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { GlAlert } from '@gitlab/ui';
import JiraConnectApp from '~/jira_connect/components/app.vue';
+import createStore from '~/jira_connect/store';
+import { SET_ERROR_MESSAGE } from '~/jira_connect/store/mutation_types';
+
+Vue.use(Vuex);
describe('JiraConnectApp', () => {
let wrapper;
+ let store;
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findHeader = () => wrapper.findByTestId('new-jira-connect-ui-heading');
const findHeaderText = () => findHeader().text();
const createComponent = (options = {}) => {
+ store = createStore();
+
wrapper = extendedWrapper(
shallowMount(JiraConnectApp, {
+ store,
provide: {
glFeatures: { newJiraConnectUi: true },
},
@@ -43,5 +55,26 @@ describe('JiraConnectApp', () => {
expect(findHeader().exists()).toBe(false);
});
});
+
+ it.each`
+ errorMessage | errorShouldRender
+ ${'Test error'} | ${true}
+ ${''} | ${false}
+ ${undefined} | ${false}
+ `(
+ 'renders correct alert when errorMessage is `$errorMessage`',
+ async ({ errorMessage, errorShouldRender }) => {
+ createComponent();
+
+ store.commit(SET_ERROR_MESSAGE, errorMessage);
+ await wrapper.vm.$nextTick();
+
+ expect(findAlert().exists()).toBe(errorShouldRender);
+ if (errorShouldRender) {
+ expect(findAlert().isVisible()).toBe(errorShouldRender);
+ expect(findAlert().html()).toContain(errorMessage);
+ }
+ },
+ );
});
});
diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js
new file mode 100644
index 00000000000..4d9b4ea8c6f
--- /dev/null
+++ b/spec/frontend/notes/components/comment_field_layout_spec.js
@@ -0,0 +1,137 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
+import EmailParticipantsWarning from '~/notes/components/email_participants_warning.vue';
+import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
+
+describe('Comment Field Layout Component', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const LOCKED_DISCUSSION_DOCS_PATH = 'docs/locked/path';
+ const CONFIDENTIAL_ISSUES_DOCS_PATH = 'docs/confidential/path';
+
+ const noteableDataMock = {
+ confidential: false,
+ discussion_locked: false,
+ locked_discussion_docs_path: LOCKED_DISCUSSION_DOCS_PATH,
+ confidential_issues_docs_path: CONFIDENTIAL_ISSUES_DOCS_PATH,
+ };
+
+ const findIssuableNoteWarning = () => wrapper.find(NoteableWarning);
+ const findEmailParticipantsWarning = () => wrapper.find(EmailParticipantsWarning);
+ const findErrorAlert = () => wrapper.findByTestId('comment-field-alert-container');
+
+ const createWrapper = (props = {}, slots = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(CommentFieldLayout, {
+ propsData: {
+ noteableData: noteableDataMock,
+ ...props,
+ },
+ slots,
+ }),
+ );
+ };
+
+ describe('.error-alert', () => {
+ it('does not exist by default', () => {
+ createWrapper();
+
+ expect(findErrorAlert().exists()).toBe(false);
+ });
+
+ it('exists when withAlertContainer is true', () => {
+ createWrapper({ withAlertContainer: true });
+
+ expect(findErrorAlert().isVisible()).toBe(true);
+ });
+ });
+
+ describe('issue is not confidential and not locked', () => {
+ it('does not show IssuableNoteWarning', () => {
+ createWrapper();
+
+ expect(findIssuableNoteWarning().exists()).toBe(false);
+ });
+ });
+
+ describe('issue is confidential', () => {
+ beforeEach(() => {
+ createWrapper({
+ noteableData: { ...noteableDataMock, confidential: true },
+ });
+ });
+
+ it('shows IssuableNoteWarning', () => {
+ expect(findIssuableNoteWarning().isVisible()).toBe(true);
+ });
+
+ it('sets IssuableNoteWarning props', () => {
+ expect(findIssuableNoteWarning().props()).toMatchObject({
+ isLocked: false,
+ isConfidential: true,
+ lockedNoteableDocsPath: LOCKED_DISCUSSION_DOCS_PATH,
+ confidentialNoteableDocsPath: CONFIDENTIAL_ISSUES_DOCS_PATH,
+ });
+ });
+ });
+
+ describe('issue is locked', () => {
+ beforeEach(() => {
+ createWrapper({
+ noteableData: { ...noteableDataMock, discussion_locked: true },
+ });
+ });
+
+ it('shows IssuableNoteWarning', () => {
+ expect(findIssuableNoteWarning().isVisible()).toBe(true);
+ });
+
+ it('sets IssuableNoteWarning props', () => {
+ expect(findIssuableNoteWarning().props()).toMatchObject({
+ isConfidential: false,
+ isLocked: true,
+ lockedNoteableDocsPath: LOCKED_DISCUSSION_DOCS_PATH,
+ confidentialNoteableDocsPath: CONFIDENTIAL_ISSUES_DOCS_PATH,
+ });
+ });
+ });
+
+ describe('issue has no email participants', () => {
+ it('does not show EmailParticipantsWarning', () => {
+ createWrapper();
+
+ expect(findEmailParticipantsWarning().exists()).toBe(false);
+ });
+ });
+
+ describe('issue has email participants', () => {
+ beforeEach(() => {
+ createWrapper({
+ noteableData: {
+ ...noteableDataMock,
+ issue_email_participants: [
+ { email: 'someone@gitlab.com' },
+ { email: 'another@gitlab.com' },
+ ],
+ },
+ });
+ });
+
+ it('shows EmailParticipantsWarning', () => {
+ expect(findEmailParticipantsWarning().isVisible()).toBe(true);
+ });
+
+ it('sets EmailParticipantsWarning props', () => {
+ expect(findEmailParticipantsWarning().props('emails')).toEqual([
+ 'someone@gitlab.com',
+ 'another@gitlab.com',
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index fca1beca999..002c4f206cb 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -181,7 +181,7 @@ describe('issue_comment_form component', () => {
describe('edit mode', () => {
beforeEach(() => {
- mountComponent();
+ mountComponent({ mountFunction: mount });
});
it('should enter edit mode when arrow up is pressed', () => {
@@ -200,7 +200,7 @@ describe('issue_comment_form component', () => {
describe('event enter', () => {
beforeEach(() => {
- mountComponent();
+ mountComponent({ mountFunction: mount });
});
it('should save note when cmd+enter is pressed', () => {
@@ -368,17 +368,6 @@ describe('issue_comment_form component', () => {
});
});
});
-
- describe('issue is confidential', () => {
- it('shows information warning', () => {
- mountComponent({
- noteableData: { ...noteableDataMock, confidential: true },
- mountFunction: mount,
- });
-
- expect(wrapper.find('[data-testid="confidential-warning"]').exists()).toBe(true);
- });
- });
});
describe('user is not logged in', () => {
diff --git a/spec/frontend/notes/components/email_participants_warning_spec.js b/spec/frontend/notes/components/email_participants_warning_spec.js
new file mode 100644
index 00000000000..ab1a6b152a4
--- /dev/null
+++ b/spec/frontend/notes/components/email_participants_warning_spec.js
@@ -0,0 +1,70 @@
+import { mount } from '@vue/test-utils';
+import EmailParticipantsWarning from '~/notes/components/email_participants_warning.vue';
+
+describe('Email Participants Warning Component', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findMoreButton = () => wrapper.find('button');
+
+ const createWrapper = (emails) => {
+ wrapper = mount(EmailParticipantsWarning, {
+ propsData: { emails },
+ });
+ };
+
+ describe('with 3 or less emails', () => {
+ beforeEach(() => {
+ createWrapper(['a@gitlab.com', 'b@gitlab.com', 'c@gitlab.com']);
+ });
+
+ it('more button does not exist', () => {
+ expect(findMoreButton().exists()).toBe(false);
+ });
+
+ it('all emails are displayed', () => {
+ expect(wrapper.text()).toBe(
+ 'a@gitlab.com, b@gitlab.com, and c@gitlab.com will be notified of your comment.',
+ );
+ });
+ });
+
+ describe('with more than 3 emails', () => {
+ beforeEach(() => {
+ createWrapper(['a@gitlab.com', 'b@gitlab.com', 'c@gitlab.com', 'd@gitlab.com']);
+ });
+
+ it('only displays first 3 emails', () => {
+ expect(wrapper.text()).toContain('a@gitlab.com, b@gitlab.com, c@gitlab.com');
+ expect(wrapper.text()).not.toContain('d@gitlab.com');
+ });
+
+ it('more button does exist', () => {
+ expect(findMoreButton().exists()).toBe(true);
+ });
+
+ it('more button displays the correct wordage', () => {
+ expect(findMoreButton().text()).toBe('and 1 more');
+ });
+
+ describe('when more button clicked', () => {
+ beforeEach(() => {
+ findMoreButton().trigger('click');
+ });
+
+ it('more button no longer exists', () => {
+ expect(findMoreButton().exists()).toBe(false);
+ });
+
+ it('all emails are displayed', () => {
+ expect(wrapper.text()).toBe(
+ 'a@gitlab.com, b@gitlab.com, c@gitlab.com, and d@gitlab.com will be notified of your comment.',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 95f35af8ebf..e64a75bede9 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -1,5 +1,5 @@
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { shallowMount } from '@vue/test-utils';
import createStore from '~/notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
@@ -19,7 +19,7 @@ describe('issue_note_form component', () => {
let props;
const createComponentWrapper = () => {
- return shallowMount(NoteForm, {
+ return mount(NoteForm, {
store,
propsData: props,
});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index 3692e51cf0b..3eacc467c51 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -30,7 +30,7 @@ job_test_2:
job_build:
stage: build
- script:
+ script:
- echo "build"
needs: ["job_test_2"]
`;
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 7b12070e08f..dbaa0066ff8 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
+import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { GRAPHQL } from '~/pipelines/components/graph/constants';
import {
generateResponse,
@@ -13,6 +14,7 @@ describe('graph component', () => {
let wrapper;
const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn);
+ const findLinksLayer = () => wrapper.find(LinksLayer);
const findStageColumns = () => wrapper.findAll(StageColumnComponent);
const defaultProps = {
@@ -28,6 +30,9 @@ describe('graph component', () => {
provide: {
dataMethod: GRAPHQL,
},
+ stubs: {
+ 'links-inner': true,
+ },
});
};
@@ -45,6 +50,10 @@ describe('graph component', () => {
expect(findStageColumns()).toHaveLength(defaultProps.pipeline.stages.length);
});
+ it('renders the links layer', () => {
+ expect(findLinksLayer().exists()).toBe(true);
+ });
+
describe('when column requests a refresh', () => {
beforeEach(() => {
findStageColumns().at(0).vm.$emit('refreshPipelineGraph');
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 44803929f6d..202e25ccda3 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -30,6 +30,7 @@ const mockGroups = Array(4)
const defaultProps = {
title: 'Fish',
groups: mockGroups,
+ pipelineId: 159,
};
describe('stage column component', () => {
@@ -92,36 +93,51 @@ describe('stage column component', () => {
});
describe('job', () => {
- beforeEach(() => {
- createComponent({
- method: mount,
- props: {
- groups: [
- {
- id: 4259,
- name: '<img src=x onerror=alert(document.domain)>',
- status: {
- icon: 'status_success',
- label: 'success',
- tooltip: '<img src=x onerror=alert(document.domain)>',
+ describe('text handling', () => {
+ beforeEach(() => {
+ createComponent({
+ method: mount,
+ props: {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
},
- },
- ],
- title: 'test <img src=x onerror=alert(document.domain)>',
- },
+ ],
+ title: 'test <img src=x onerror=alert(document.domain)>',
+ },
+ });
});
- });
- it('capitalizes and escapes name', () => {
- expect(findStageColumnTitle().text()).toBe(
- 'Test &lt;img src=x onerror=alert(document.domain)&gt;',
- );
+ it('capitalizes and escapes name', () => {
+ expect(findStageColumnTitle().text()).toBe(
+ 'Test &lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
+
+ it('escapes id', () => {
+ expect(findStageColumnGroup().attributes('id')).toBe(
+ 'ci-badge-&lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
});
- it('escapes id', () => {
- expect(findStageColumnGroup().attributes('id')).toBe(
- 'ci-badge-&lt;img src=x onerror=alert(document.domain)&gt;',
- );
+ describe('interactions', () => {
+ beforeEach(() => {
+ createComponent({ method: mount });
+ });
+
+ it('emits jobHovered event on mouseenter and mouseleave', async () => {
+ await findStageColumnGroup().trigger('mouseenter');
+ expect(wrapper.emitted().jobHover).toEqual([[defaultProps.groups[0].name]]);
+ await findStageColumnGroup().trigger('mouseleave');
+ expect(wrapper.emitted().jobHover).toEqual([[defaultProps.groups[0].name], ['']]);
+ });
});
});
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
index 1f0dc97f2f3..7d1a7a79c7f 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -1,4 +1,4 @@
-import { createUniqueLinkId } from '~/pipelines/utils';
+import { createUniqueLinkId } from '~/pipelines/components/graph_shared/drawing_utils';
export const yamlString = `stages:
- empty
diff --git a/spec/frontend/pipelines/shared/links_layer_spec.js b/spec/frontend/pipelines/shared/links_layer_spec.js
new file mode 100644
index 00000000000..9ef5233dbce
--- /dev/null
+++ b/spec/frontend/pipelines/shared/links_layer_spec.js
@@ -0,0 +1,99 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
+import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
+import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
+
+describe('links layer component', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.find(GlAlert);
+ const findShowAnyways = () => findAlert().find(GlButton);
+ const findLinksInner = () => wrapper.find(LinksInner);
+
+ const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
+ const containerId = `pipeline-links-container-${pipeline.id}`;
+ const slotContent = "<div>Ceci n'est pas un graphique</div>";
+
+ const tooManyStages = Array(101)
+ .fill(0)
+ .flatMap(() => pipeline.stages);
+
+ const defaultProps = {
+ containerId,
+ containerMeasurements: { width: 400, height: 400 },
+ pipelineId: pipeline.id,
+ pipelineData: pipeline.stages,
+ };
+
+ const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
+ wrapper = mountFn(LinksLayer, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ slots: {
+ default: slotContent,
+ },
+ stubs: {
+ 'links-inner': true,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('with data under max stages', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the default slot', () => {
+ expect(wrapper.html()).toContain(slotContent);
+ });
+
+ it('renders the inner links component', () => {
+ expect(findLinksInner().exists()).toBe(true);
+ });
+ });
+
+ describe('with more than the max number of stages', () => {
+ describe('rendering', () => {
+ beforeEach(() => {
+ createComponent({ props: { pipelineData: tooManyStages } });
+ });
+
+ it('renders the default slot', () => {
+ expect(wrapper.html()).toContain(slotContent);
+ });
+
+ it('renders the alert component', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('does not render the inner links component', () => {
+ expect(findLinksInner().exists()).toBe(false);
+ });
+ });
+
+ describe('interactions', () => {
+ beforeEach(() => {
+ createComponent({ mountFn: mount, props: { pipelineData: tooManyStages } });
+ });
+
+ it('renders the disable button', () => {
+ expect(findShowAnyways().exists()).toBe(true);
+ expect(findShowAnyways().text()).toBe(wrapper.vm.$options.i18n.showLinksAnyways);
+ });
+
+ it('shows links when override is clicked', async () => {
+ expect(findLinksInner().exists()).toBe(false);
+ await findShowAnyways().trigger('click');
+ expect(findLinksInner().exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index 7c964be7825..b58ce0083c0 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -25,6 +25,7 @@ import {
tokenValueLabel,
tokenValueMilestone,
tokenValueMembership,
+ tokenValueConfidential,
} from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
@@ -227,12 +228,13 @@ describe('FilteredSearchBarRoot', () => {
});
describe('removeQuotesEnclosure', () => {
- const mockFilters = [tokenValueAuthor, tokenValueLabel, 'foo'];
+ const mockFilters = [tokenValueAuthor, tokenValueLabel, tokenValueConfidential, 'foo'];
it('returns filter array with unescaped strings for values which have spaces', () => {
expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([
tokenValueAuthor,
tokenValueLabel,
+ tokenValueConfidential,
'foo',
]);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index 2c7b6de9ce3..7606b3bd91c 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -155,6 +155,14 @@ export const tokenValueMembership = {
},
};
+export const tokenValueConfidential = {
+ type: 'confidential',
+ value: {
+ operator: '=',
+ data: true,
+ },
+};
+
export const tokenValuePlain = {
type: 'filtered-search-term',
value: { data: 'foo' },
diff --git a/spec/graphql/resolvers/package_details_resolver_spec.rb b/spec/graphql/resolvers/package_details_resolver_spec.rb
new file mode 100644
index 00000000000..825b2aed40a
--- /dev/null
+++ b/spec/graphql/resolvers/package_details_resolver_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::PackageDetailsResolver do
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:user) { project.owner }
+ let_it_be(:package) { create(:composer_package, project: project) }
+
+ describe '#resolve' do
+ let(:args) do
+ { id: package.to_global_id.to_s }
+ end
+
+ subject { resolve(described_class, ctx: { current_user: user }, args: args).sync }
+
+ it { is_expected.to eq(package) }
+ end
+end
diff --git a/spec/graphql/types/packages/composer/details_type_spec.rb b/spec/graphql/types/packages/composer/details_type_spec.rb
new file mode 100644
index 00000000000..2e4cb965ded
--- /dev/null
+++ b/spec/graphql/types/packages/composer/details_type_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageComposerDetails'] do
+ it { expect(described_class.graphql_name).to eq('PackageComposerDetails') }
+
+ it 'includes all the package fields' do
+ expected_fields = %w[
+ id name version created_at updated_at package_type tags project pipelines versions
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+
+ it 'includes composer specific files' do
+ expected_fields = %w[
+ composer_metadatum
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/composer/json_type_spec.rb b/spec/graphql/types/packages/composer/json_type_spec.rb
new file mode 100644
index 00000000000..af5194ffb49
--- /dev/null
+++ b/spec/graphql/types/packages/composer/json_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageComposerJsonType'] do
+ it { expect(described_class.graphql_name).to eq('PackageComposerJsonType') }
+
+ it 'includes composer json files' do
+ expected_fields = %w[
+ name type license version
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/composer/metadatum_type_spec.rb b/spec/graphql/types/packages/composer/metadatum_type_spec.rb
new file mode 100644
index 00000000000..0f47d8f1812
--- /dev/null
+++ b/spec/graphql/types/packages/composer/metadatum_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageComposerMetadatumType'] do
+ it { expect(described_class.graphql_name).to eq('PackageComposerMetadatumType') }
+
+ it 'includes composer metadatum fields' do
+ expected_fields = %w[
+ target_sha composer_json
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/package_type_enum_spec.rb b/spec/graphql/types/packages/package_type_enum_spec.rb
index 407d5786f65..407d5786f65 100644
--- a/spec/graphql/types/package_type_enum_spec.rb
+++ b/spec/graphql/types/packages/package_type_enum_spec.rb
diff --git a/spec/graphql/types/package_type_spec.rb b/spec/graphql/types/packages/package_type_spec.rb
index 22048e7a693..7003a4d4d07 100644
--- a/spec/graphql/types/package_type_spec.rb
+++ b/spec/graphql/types/packages/package_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Package'] do
it 'includes all the package fields' do
expected_fields = %w[
- id name version created_at updated_at package_type
+ id name version created_at updated_at package_type tags project pipelines versions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/tag_type_spec.rb b/spec/graphql/types/packages/tag_type_spec.rb
new file mode 100644
index 00000000000..83b705157d8
--- /dev/null
+++ b/spec/graphql/types/packages/tag_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageTag'] do
+ it { expect(described_class.graphql_name).to eq('PackageTag') }
+
+ it 'includes all the package tag fields' do
+ expected_fields = %w[
+ id name created_at updated_at
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 7a0b3035607..3e716865e56 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -94,4 +94,10 @@ RSpec.describe GitlabSchema.types['Query'] do
it { is_expected.to have_graphql_type(Types::ContainerRepositoryDetailsType) }
end
+
+ describe 'package_composer_details field' do
+ subject { described_class.fields['packageComposerDetails'] }
+
+ it { is_expected.to have_graphql_type(Types::Packages::Composer::DetailsType) }
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 40eb8638a83..20406acb658 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let(:pipeline) { Ci::Pipeline.new }
@@ -29,29 +29,96 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
let(:step) { described_class.new(pipeline, command) }
- before do
- stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
+ shared_examples 'builds pipeline' do
+ it 'builds a pipeline with the expected attributes' do
+ step.perform!
+
+ expect(pipeline.sha).not_to be_empty
+ expect(pipeline.sha).to eq project.commit.id
+ expect(pipeline.ref).to eq 'master'
+ expect(pipeline.tag).to be false
+ expect(pipeline.user).to eq user
+ expect(pipeline.project).to eq project
+ end
end
- it 'never breaks the chain' do
- step.perform!
+ shared_examples 'breaks the chain' do
+ it 'returns true' do
+ step.perform!
- expect(step.break?).to be false
+ expect(step.break?).to be true
+ end
end
- it 'fills pipeline object with data' do
+ shared_examples 'does not break the chain' do
+ it 'returns false' do
+ step.perform!
+
+ expect(step.break?).to be false
+ end
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
+ end
+
+ it_behaves_like 'does not break the chain'
+ it_behaves_like 'builds pipeline'
+
+ it 'sets pipeline variables' do
step.perform!
- expect(pipeline.sha).not_to be_empty
- expect(pipeline.sha).to eq project.commit.id
- expect(pipeline.ref).to eq 'master'
- expect(pipeline.tag).to be false
- expect(pipeline.user).to eq user
- expect(pipeline.project).to eq project
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
.to eq variables_attributes.map(&:with_indifferent_access)
end
+ context 'when project setting restrict_user_defined_variables is enabled' do
+ before do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ context 'when user is developer' do
+ it_behaves_like 'breaks the chain'
+ it_behaves_like 'builds pipeline'
+
+ it 'returns an error on variables_attributes', :aggregate_failures do
+ step.perform!
+
+ expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables'])
+ expect(pipeline.variables).to be_empty
+ end
+
+ context 'when variables_attributes is not specified' do
+ let(:variables_attributes) { nil }
+
+ it_behaves_like 'does not break the chain'
+ it_behaves_like 'builds pipeline'
+
+ it 'assigns empty variables' do
+ step.perform!
+
+ expect(pipeline.variables).to be_empty
+ end
+ end
+ end
+
+ context 'when user is maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'does not break the chain'
+ it_behaves_like 'builds pipeline'
+
+ it 'assigns variables_attributes' do
+ step.perform!
+
+ expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
+ end
+ end
+ end
+
it 'returns a valid pipeline' do
step.perform!
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index bc2012e83bd..9ca5aeeea58 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -295,4 +295,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
it { is_expected.to eq(false) }
end
end
+
+ describe '#creates_child_pipeline?' do
+ let(:command) { described_class.new(bridge: bridge) }
+
+ subject { command.creates_child_pipeline? }
+
+ context 'when bridge is present' do
+ context 'when bridge triggers a child pipeline' do
+ let(:bridge) { double(:bridge, triggers_child_pipeline?: true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when bridge triggers a multi-project pipeline' do
+ let(:bridge) { double(:bridge, triggers_child_pipeline?: false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when bridge is not present' do
+ let(:bridge) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index 0ac54a20fcc..02e67488d3f 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -21,6 +21,47 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor))
end
+ # see: https://gitlab.com/gitlab-org/gitlab/-/issues/297358
+ context 'the relation has been preloaded' do
+ let(:projects) { Project.all.preload(:issues) }
+ let(:nodes) { projects.first.issues }
+
+ before do
+ project = create(:project)
+ create_list(:issue, 3, project: project)
+ end
+
+ it 'is loaded' do
+ expect(nodes).to be_loaded
+ end
+
+ it 'does not error when accessing pagination information' do
+ connection.first = 2
+
+ expect(connection).to have_attributes(
+ has_previous_page: false,
+ has_next_page: true
+ )
+ end
+
+ it 'can generate cursors' do
+ connection.send(:ordered_items) # necessary to generate the order-list
+
+ expect(connection.cursor_for(nodes.first)).to be_a(String)
+ end
+
+ it 'can read the next page' do
+ connection.send(:ordered_items) # necessary to generate the order-list
+ ordered = nodes.reorder(id: :desc)
+ next_page = described_class.new(nodes,
+ context: context,
+ max_page_size: 3,
+ after: connection.cursor_for(ordered.second))
+
+ expect(next_page.sliced_nodes).to contain_exactly(ordered.third)
+ end
+ end
+
it_behaves_like 'a connection with collection methods'
it_behaves_like 'a redactable connection' do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index c21d3b0939f..e6650549f7f 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -401,6 +401,48 @@ RSpec.describe ProjectPolicy do
end
end
+ describe 'set_pipeline_variables' do
+ context 'when user is developer' do
+ let(:current_user) { developer }
+
+ context 'when project allows user defined variables' do
+ before do
+ project.update!(restrict_user_defined_variables: false)
+ end
+
+ it { is_expected.to be_allowed(:set_pipeline_variables) }
+ end
+
+ context 'when project restricts use of user defined variables' do
+ before do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it { is_expected.not_to be_allowed(:set_pipeline_variables) }
+ end
+ end
+
+ context 'when user is maintainer' do
+ let(:current_user) { maintainer }
+
+ context 'when project allows user defined variables' do
+ before do
+ project.update!(restrict_user_defined_variables: false)
+ end
+
+ it { is_expected.to be_allowed(:set_pipeline_variables) }
+ end
+
+ context 'when project restricts use of user defined variables' do
+ before do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it { is_expected.to be_allowed(:set_pipeline_variables) }
+ end
+ end
+ end
+
context 'support bot' do
let(:current_user) { User.support_bot }
diff --git a/spec/requests/api/graphql/packages/package_composer_details_spec.rb b/spec/requests/api/graphql/packages/package_composer_details_spec.rb
new file mode 100644
index 00000000000..1a2cf4a972a
--- /dev/null
+++ b/spec/requests/api/graphql/packages/package_composer_details_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'package composer details' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package) { create(:composer_package, project: project) }
+ let_it_be(:composer_metadatum) do
+ # we are forced to manually create the metadatum, without using the factory to force the sha to be a string
+ # and avoid an error where gitaly can't find the repository
+ create(:composer_metadatum, package: package, target_sha: 'foo_sha', composer_json: { name: 'name', type: 'type', license: 'license', version: 1 })
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'packageComposerDetails',
+ { id: package_global_id },
+ all_graphql_fields_for('PackageComposerDetails', max_depth: 2)
+ )
+ end
+
+ let(:user) { project.owner }
+ let(:package_global_id) { package.to_global_id.to_s }
+ let(:package_composer_details_response) { graphql_data.dig('packageComposerDetails') }
+
+ subject { post_graphql(query, current_user: user) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(package_composer_details_response).to match_schema('graphql/packages/package_composer_details')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index d711376024a..e1b867ad097 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -252,4 +252,41 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(merge_request_graphql_data['mergeStatus']).to eq('checking')
end
end
+
+ # see: https://gitlab.com/gitlab-org/gitlab/-/issues/297358
+ context 'when the notes have been preloaded (by participants)' do
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ mrs: mergeRequests(first: 1) {
+ nodes {
+ participants { nodes { id } }
+ notes(first: 1) {
+ pageInfo { endCursor hasPreviousPage hasNextPage }
+ nodes { id }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ before do
+ create_list(:note_on_merge_request, 3, project: project, noteable: merge_request)
+ end
+
+ it 'does not error' do
+ post_graphql(query,
+ current_user: current_user,
+ variables: { path: project.full_path })
+
+ expect(graphql_data_at(:project, :mrs, :nodes, :notes, :pageInfo)).to contain_exactly a_hash_including(
+ 'endCursor' => String,
+ 'hasNextPage' => true,
+ 'hasPreviousPage' => false
+ )
+ end
+ end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 53c0e95c777..72f2dc79633 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1583,6 +1583,7 @@ RSpec.describe API::Projects do
expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline)
+ expect(json_response['restrict_user_defined_variables']).to eq(project.restrict_user_defined_variables?)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
expect(json_response['operations_access_level']).to be_present
end
@@ -1654,6 +1655,7 @@ RSpec.describe API::Projects do
expect(json_response['shared_with_groups'][0]).to have_key('expires_at')
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline)
+ expect(json_response['restrict_user_defined_variables']).to eq(project.restrict_user_defined_variables?)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
expect(json_response['ci_default_git_depth']).to eq(project.ci_default_git_depth)
expect(json_response['ci_forward_deployment_enabled']).to eq(project.ci_forward_deployment_enabled)
@@ -2597,6 +2599,18 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:bad_request)
end
+ it 'updates restrict_user_defined_variables', :aggregate_failures do
+ project_param = { restrict_user_defined_variables: true }
+
+ put api("/projects/#{project3.id}", user), params: project_param
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
it 'updates avatar' do
project_param = {
avatar: fixture_file_upload('spec/fixtures/banana_sample.gif',
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
index 03cea4074bf..860932d4fde 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -371,6 +371,26 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
expect { service.execute(bridge) }.to change { Ci::Pipeline.count }.by(1)
end
end
+
+ context 'when downstream project does not allow user-defined variables for child pipelines' do
+ before do
+ bridge.yaml_variables = [{ key: 'BRIDGE', value: '$PIPELINE_VARIABLE-var', public: true }]
+
+ upstream_pipeline.project.update!(restrict_user_defined_variables: true)
+ end
+
+ it 'creates a new pipeline allowing variables to be passed downstream' do
+ expect { service.execute(bridge) }.to change { Ci::Pipeline.count }.by(1)
+ end
+
+ it 'passes variables downstream from the bridge' do
+ pipeline = service.execute(bridge)
+
+ pipeline.variables.map(&:key).tap do |variables|
+ expect(variables).to include 'BRIDGE'
+ end
+ end
+ end
end
end
@@ -460,6 +480,33 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
expect(variable.value).to eq 'my-value-var'
end
end
+
+ context 'when downstream project does not allow user-defined variables for multi-project pipelines' do
+ before do
+ downstream_project.update!(restrict_user_defined_variables: true)
+ end
+
+ it 'does not create a new pipeline' do
+ expect { service.execute(bridge) }
+ .not_to change { Ci::Pipeline.count }
+ end
+
+ it 'ignores variables passed downstream from the bridge' do
+ pipeline = service.execute(bridge)
+
+ pipeline.variables.map(&:key).tap do |variables|
+ expect(variables).not_to include 'BRIDGE'
+ end
+ end
+
+ it 'sets errors', :aggregate_failures do
+ service.execute(bridge)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
+ expect(bridge.options[:downstream_errors]).to eq(['Insufficient permissions to set pipeline variables'])
+ end
+ end
end
end
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index ac077e3c30e..0cc66e67b91 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -3,14 +3,16 @@
require 'spec_helper'
RSpec.describe Ci::PipelineTriggerService do
- let(:project) { create(:project, :repository) }
+ include AfterNextHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
before do
stub_ci_pipeline_to_return_yaml_file
end
describe '#execute' do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
let(:result) { described_class.new(project, user, params).execute }
before do
@@ -29,8 +31,8 @@ RSpec.describe Ci::PipelineTriggerService do
end
end
- context 'when params have an existsed trigger token' do
- context 'when params have an existsed ref' do
+ context 'when params have an existing trigger token' do
+ context 'when params have an existing ref' do
let(:params) { { token: trigger.token, ref: 'master', variables: nil } }
it 'triggers a pipeline' do
@@ -45,9 +47,7 @@ RSpec.describe Ci::PipelineTriggerService do
context 'when commit message has [ci skip]' do
before do
- allow_next_instance_of(Ci::Pipeline) do |instance|
- allow(instance).to receive(:git_commit_message) { '[ci skip]' }
- end
+ allow_next(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }
end
it 'ignores [ci skip] and create as general' do
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index c9ecbad3167..00c6de7681d 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -72,6 +72,31 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
end
+
+ context 'when user defined variables are restricted' do
+ before do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ context 'when user is maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'assigns the variables to the build' do
+ service.execute(build, job_variables)
+
+ expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
+ end
+ end
+
+ context 'when user is developer' do
+ it 'raises an error' do
+ expect { service.execute(build, job_variables) }
+ .to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+ end
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index e95f24c4200..21e294418a1 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -669,15 +669,19 @@ RSpec.describe QuickActions::InterpretService do
shared_examples 'assign_reviewer command' do
it 'assigns a reviewer to a single user' do
- _, updates, _ = service.execute(content, issuable)
+ _, updates, message = service.execute(content, issuable)
expect(updates).to eq(reviewer_ids: [developer.id])
+ expect(message).to eq("Assigned #{developer.to_reference} as reviewer.")
end
+ end
- it 'returns the assign reviewer message' do
- _, _, message = service.execute(content, issuable)
+ shared_examples 'unassign_reviewer command' do
+ it 'removes a single reviewer' do
+ _, updates, message = service.execute(content, issuable)
- expect(message).to eq("Assigned #{developer.to_reference} as reviewer.")
+ expect(updates).to eq(reviewer_ids: [])
+ expect(message).to eq("Removed reviewer #{developer.to_reference}.")
end
end
@@ -876,85 +880,117 @@ RSpec.describe QuickActions::InterpretService do
end
context 'when the merge_request_reviewers flag is enabled' do
- context 'assign_reviewer command with one user' do
- it_behaves_like 'assign_reviewer command' do
- let(:content) { "/assign_reviewer @#{developer.username}" }
- let(:issuable) { merge_request }
+ describe 'assign_reviewer command' do
+ let(:content) { "/assign_reviewer @#{developer.username}" }
+ let(:issuable) { merge_request }
+
+ context 'with one user' do
+ it_behaves_like 'assign_reviewer command'
end
- it_behaves_like 'empty command' do
- let(:content) { "/assign_reviewer @#{developer.username}" }
+ context 'with an issue instead of a merge request' do
let(:issuable) { issue }
+
+ it_behaves_like 'empty command'
end
- end
- # CE does not have multiple reviewers
- context 'assign_reviewer command with multiple assignees' do
- let(:issuable) { merge_request }
+ # CE does not have multiple reviewers
+ context 'assign command with multiple assignees' do
+ before do
+ project.add_developer(developer2)
+ end
+
+ # There's no guarantee that the reference extractor will preserve
+ # the order of the mentioned users since this is dependent on the
+ # order in which rows are returned. We just ensure that at least
+ # one of the mentioned users is assigned.
+ context 'assigns to one of the two users' do
+ let(:content) { "/assign_reviewer @#{developer.username} @#{developer2.username}" }
- it 'assigns only the first reviewer to the merge request' do
- content = "/assign_reviewer @#{developer.username} @#{developer2.username}"
- _, updates, _ = service.execute(content, issuable)
+ it 'assigns to a single reviewer' do
+ _, updates, message = service.execute(content, issuable)
- expect(updates).to eq(reviewer_ids: [developer.id])
+ expect(updates[:reviewer_ids].count).to eq(1)
+ reviewer = updates[:reviewer_ids].first
+ expect([developer.id, developer2.id]).to include(reviewer)
+
+ user = reviewer == developer.id ? developer : developer2
+
+ expect(message).to match("Assigned #{user.to_reference} as reviewer.")
+ end
+ end
end
- end
- context 'assign_reviewer command with me alias' do
- it_behaves_like 'assign_reviewer command' do
+ context 'with "me" alias' do
let(:content) { '/assign_reviewer me' }
- let(:issuable) { merge_request }
+
+ it_behaves_like 'assign_reviewer command'
end
- end
- context 'assign_reviewer command with me alias and whitespace' do
- it_behaves_like 'assign_reviewer command' do
+ context 'with an alias and whitespace' do
let(:content) { '/assign_reviewer me ' }
- let(:issuable) { merge_request }
+
+ it_behaves_like 'assign_reviewer command'
end
- end
- it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found." do
- let(:content) { '/assign_reviewer @abcd1234' }
- let(:issuable) { merge_request }
- end
+ context 'with an incorrect user' do
+ let(:content) { '/assign_reviewer @abcd1234' }
- it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found." do
- let(:content) { '/assign_reviewer' }
- let(:issuable) { merge_request }
- end
+ it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found."
+ end
- describe 'assign_reviewer command' do
- let(:content) { "/assign_reviewer @#{developer.username} do it!" }
+ context 'with the "reviewer" alias' do
+ let(:content) { "/reviewer @#{developer.username}" }
+
+ it_behaves_like 'assign_reviewer command'
+ end
+
+ context 'with no user' do
+ let(:content) { '/assign_reviewer' }
+
+ it_behaves_like 'empty command', "Failed to assign a reviewer because no user was found."
+ end
- it 'includes only the user reference' do
- _, explanations = service.explain(content, merge_request)
+ context 'includes only the user reference with extra text' do
+ let(:content) { "/assign_reviewer @#{developer.username} do it!" }
- expect(explanations).to eq(["Assigns @#{developer.username} as reviewer."])
+ it_behaves_like 'assign_reviewer command'
end
end
describe 'unassign_reviewer command' do
- let(:content) { '/unassign_reviewer' }
- let(:merge_request) { create(:merge_request, reviewers: [developer]) }
+ # CE does not have multiple reviewers, so basically anything
+ # after /unassign_reviewer (including whitespace) will remove
+ # all the current reviewers.
+ let(:issuable) { create(:merge_request, reviewers: [developer]) }
+ let(:content) { "/unassign_reviewer @#{developer.username}" }
+
+ context 'with one user' do
+ it_behaves_like 'unassign_reviewer command'
+ end
- it 'includes current reviewer reference' do
- _, explanations = service.explain(content, merge_request)
+ context 'with an issue instead of a merge request' do
+ let(:issuable) { issue }
- expect(explanations).to eq(["Removes reviewer @#{developer.username}."])
+ it_behaves_like 'empty command'
end
- it 'populates reviewer_ids: [] if content contains /unassign_reviewer' do
- _, updates, _ = service.execute(content, merge_request)
+ context 'with anything after the command' do
+ let(:content) { '/unassign_reviewer supercalifragilisticexpialidocious' }
- expect(updates).to eq(reviewer_ids: [])
+ it_behaves_like 'unassign_reviewer command'
end
- it 'returns the unassign reviewer message for all the reviewers if content contains /unassign_reviewer' do
- merge_request.update!(reviewer_ids: [developer.id, developer2.id])
- _, _, message = service.execute(content, merge_request)
+ context 'with the "remove_reviewer" alias' do
+ let(:content) { "/remove_reviewer @#{developer.username}" }
- expect(message).to eq("Removed reviewers #{developer.to_reference} and #{developer2.to_reference}.")
+ it_behaves_like 'unassign_reviewer command'
+ end
+
+ context 'with no user' do
+ let(:content) { '/unassign_reviewer' }
+
+ it_behaves_like 'unassign_reviewer command'
end
end
end
@@ -1969,6 +2005,28 @@ RSpec.describe QuickActions::InterpretService do
end
end
+ describe 'unassign_reviewer command' do
+ let(:content) { '/unassign_reviewer' }
+ let(:merge_request) { create(:merge_request, source_project: project, reviewers: [developer]) }
+
+ it 'includes current assignee reference' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(["Removes reviewer @#{developer.username}."])
+ end
+ end
+
+ describe 'assign_reviewer command' do
+ let(:content) { "/assign_reviewer #{developer.to_reference}" }
+ let(:merge_request) { create(:merge_request, source_project: project, assignees: [developer]) }
+
+ it 'includes only the user reference' do
+ _, explanations = service.explain(content, merge_request)
+
+ expect(explanations).to eq(["Assigns #{developer.to_reference} as reviewer."])
+ end
+ end
+
describe 'milestone command' do
let(:content) { '/milestone %wrong-milestone' }
let!(:milestone) { create(:milestone, project: project, title: '9.10') }