summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/blob/utils.js6
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_actions.vue18
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_app.vue31
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue87
-rw-r--r--app/assets/javascripts/jira_import/utils/constants.js29
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue39
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue110
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss28
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss4
-rw-r--r--app/controllers/projects/ci/lints_controller.rb35
-rw-r--r--app/helpers/ci/pipelines_helper.rb18
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/models/concerns/ci/contextable.rb10
-rw-r--r--app/services/ci/create_pipeline_service.rb53
-rw-r--r--app/services/resource_access_tokens/create_service.rb2
-rw-r--r--app/services/web_hook_service.rb5
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml2
-rw-r--r--app/views/projects/ci/lints/_create.html.haml77
-rw-r--r--app/views/projects/ci/lints/_lint_warnings.html.haml6
-rw-r--r--app/views/projects/ci/lints/show.html.haml8
-rw-r--r--app/views/projects/pipelines/_pipeline_warnings.html.haml6
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml1
-rw-r--r--app/views/projects/pipelines/new.html.haml1
-rw-r--r--app/views/projects/pipelines/show.html.haml8
-rw-r--r--app/workers/git_garbage_collect_worker.rb12
-rw-r--r--changelogs/unreleased/213717-update-environments-dropdowns.yml6
-rw-r--r--changelogs/unreleased/217801-step-2-fix-editor-lite-scroll.yml5
-rw-r--r--changelogs/unreleased/218722-enable-project-access-tokens-by-default.yml5
-rw-r--r--changelogs/unreleased/219638-date-picked-utc-label-color.yml5
-rw-r--r--changelogs/unreleased/219774-legacy-dropdown-edit.yml5
-rw-r--r--changelogs/unreleased/223621-prune-lfs-objects-during-git-gc.yml5
-rw-r--r--changelogs/unreleased/ci-lint-view-error-messages.yml5
-rw-r--r--changelogs/unreleased/use-dry-run-on-ci-lint.yml5
-rw-r--r--config/feature_flags/development/sse_erb_support.yml7
-rw-r--r--doc/administration/instance_limits.md2
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/api/projects.md2
-rw-r--r--doc/ci/img/ci_lint.pngbin0 -> 37745 bytes
-rw-r--r--doc/ci/img/ci_lint_dry_run.pngbin0 -> 18688 bytes
-rw-r--r--doc/ci/lint.md41
-rw-r--r--doc/ci/quick_start/README.md4
-rw-r--r--doc/ci/variables/predefined_variables.md6
-rw-r--r--doc/ci/yaml/README.md62
-rw-r--r--doc/development/fe_guide/graphql.md1
-rw-r--r--doc/development/filtering_by_label.md5
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/development/issuable-like-models.md11
-rw-r--r--doc/development/issue_types.md6
-rw-r--r--doc/operations/metrics/index.md85
-rw-r--r--doc/security/webhooks.md2
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md4
-rw-r--r--doc/user/gitlab_com/index.md3
-rw-r--r--doc/user/group/index.md4
-rw-r--r--doc/user/project/clusters/add_eks_clusters.md2
-rw-r--r--doc/user/project/index.md2
-rw-r--r--doc/user/project/integrations/webhooks.md2
-rw-r--r--doc/user/project/settings/index.md2
-rw-r--r--lib/gitlab/ci/features.rb8
-rw-r--r--lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb40
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/parameter.rb1
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb8
-rw-r--r--lib/gitlab/ci/pipeline/chain/metrics.rb23
-rw-r--r--lib/gitlab/ci/pipeline/chain/pipeline/process.rb24
-rw-r--r--lib/gitlab/ci/pipeline/chain/sequence.rb19
-rw-r--r--lib/gitlab/ci/pipeline/chain/stop_dry_run.rb22
-rw-r--r--lib/gitlab/ci/pipeline/metrics.rb18
-rw-r--r--lib/gitlab/json.rb28
-rw-r--r--lib/gitlab/static_site_editor/config.rb4
-rw-r--r--locale/gitlab.pot16
-rw-r--r--qa/qa/page/dashboard/snippet/edit.rb3
-rw-r--r--qa/qa/page/dashboard/snippet/show.rb4
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb73
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb2
-rw-r--r--spec/features/projects/ci/lint_spec.rb50
-rw-r--r--spec/frontend/blob/utils_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_actions_spec.js6
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js92
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js62
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap22
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js25
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js34
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js152
-rw-r--r--spec/helpers/ci/pipelines_helper_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb28
-rw-r--r--spec/lib/gitlab/json_spec.rb32
-rw-r--r--spec/lib/gitlab/static_site_editor/config_spec.rb22
-rw-r--r--spec/models/ci/build_spec.rb8
-rw-r--r--spec/services/ci/create_pipeline_service/dry_run_spec.rb119
-rw-r--r--spec/services/ci/create_pipeline_service/parameter_content_spec.rb9
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb21
-rw-r--r--spec/services/web_hook_service_spec.rb8
-rw-r--r--spec/views/projects/ci/lints/show.html.haml_spec.rb23
-rw-r--r--spec/views/projects/pipelines/new.html.haml_spec.rb34
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb77
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb52
108 files changed, 1771 insertions, 484 deletions
diff --git a/Gemfile b/Gemfile
index 51525ff8ee9..7f7307a45cc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -510,3 +510,4 @@ gem 'json-schema', '~> 2.8.0'
gem 'json_schemer', '~> 0.2.12'
gem 'oj', '~> 3.10.6'
gem 'multi_json', '~> 1.14.1'
+gem 'yajl-ruby', '~> 1.4.1', require: 'yajl'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7c3eed6a6d6..a6b4be38837 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1180,6 +1180,7 @@ GEM
xml-simple (1.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
+ yajl-ruby (1.4.1)
zeitwerk (2.3.0)
PLATFORMS
@@ -1447,6 +1448,7 @@ DEPENDENCIES
webmock (~> 3.5.1)
webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1)
+ yajl-ruby (~> 1.4.1)
BUNDLED WITH
1.17.3
diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js
index 052de0cb9d4..a0211c8bb8e 100644
--- a/app/assets/javascripts/blob/utils.js
+++ b/app/assets/javascripts/blob/utils.js
@@ -4,7 +4,11 @@ export function initEditorLite({ el, ...args }) {
if (!el) {
throw new Error(`"el" parameter is required to initialize Editor`);
}
- const editor = new Editor();
+ const editor = new Editor({
+ scrollbar: {
+ alwaysConsumeMouseWheel: false,
+ },
+ });
editor.createInstance({
el,
...args,
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
index df03cc126b2..db61957d452 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlIcon, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const IGNORED = 'ignored';
@@ -10,7 +10,7 @@ const statusValidation = [IGNORED, RESOLVED, UNRESOLVED];
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlIcon,
GlButtonGroup,
},
@@ -45,7 +45,7 @@ export default {
<template>
<div>
<gl-button-group class="gl-flex-direction-column flex-md-row gl-ml-0 ml-md-n4">
- <gl-deprecated-button
+ <gl-button
:key="ignoreBtn.status"
:ref="`${ignoreBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
@@ -55,8 +55,8 @@ export default {
>
<gl-icon class="gl-display-none d-md-inline gl-m-0" :name="ignoreBtn.icon" :size="12" />
<span class="d-md-none">{{ ignoreBtn.title }}</span>
- </gl-deprecated-button>
- <gl-deprecated-button
+ </gl-button>
+ <gl-button
:key="resolveBtn.status"
:ref="`${resolveBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
@@ -66,15 +66,15 @@ export default {
>
<gl-icon class="gl-display-none d-md-inline gl-m-0" :name="resolveBtn.icon" :size="12" />
<span class="d-md-none">{{ resolveBtn.title }}</span>
- </gl-deprecated-button>
+ </gl-button>
</gl-button-group>
- <gl-deprecated-button
+ <gl-button
:href="detailsLink"
- category="secondary"
+ category="primary"
variant="info"
class="gl-display-block d-md-none gl-mb-4 mb-md-0"
>
{{ __('More details') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/jira_import/components/jira_import_app.vue b/app/assets/javascripts/jira_import/components/jira_import_app.vue
index 60ddacd49dd..a1b1fec9c8b 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_app.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { last } from 'lodash';
import { __ } from '~/locale';
import getJiraImportDetailsQuery from '../queries/get_jira_import_details.query.graphql';
@@ -16,7 +16,6 @@ export default {
components: {
GlAlert,
GlLoadingIcon,
- GlSprintf,
JiraImportForm,
JiraImportProgress,
JiraImportSetup,
@@ -55,7 +54,6 @@ export default {
return {
isSubmitting: false,
jiraImportDetails: {},
- selectedProject: undefined,
userMappings: [],
errorMessage: '',
showAlert: false,
@@ -80,22 +78,6 @@ export default {
},
},
},
- computed: {
- numberOfPreviousImports() {
- return this.jiraImportDetails.imports?.reduce?.(
- (acc, jiraProject) => (jiraProject.jiraProjectKey === this.selectedProject ? acc + 1 : acc),
- 0,
- );
- },
- hasPreviousImports() {
- return this.numberOfPreviousImports > 0;
- },
- importLabel() {
- return this.selectedProject
- ? `jira-import::${this.selectedProject}-${this.numberOfPreviousImports + 1}`
- : 'jira-import::KEY-1';
- },
- },
mounted() {
if (this.isJiraConfigured) {
this.$apollo
@@ -168,9 +150,6 @@ export default {
this.showAlert = false;
},
},
- previousImportsMessage: __(
- 'You have imported from this project %{numberOfPreviousImports} times before. Each new import will create duplicate issues.',
- ),
};
</script>
@@ -179,11 +158,6 @@ export default {
<gl-alert v-if="showAlert" variant="danger" @dismiss="dismissAlert">
{{ errorMessage }}
</gl-alert>
- <gl-alert v-if="hasPreviousImports" variant="warning" :dismissible="false">
- <gl-sprintf :message="$options.previousImportsMessage">
- <template #numberOfPreviousImports>{{ numberOfPreviousImports }}</template>
- </gl-sprintf>
- </gl-alert>
<jira-import-setup
v-if="!isJiraConfigured"
@@ -201,10 +175,9 @@ export default {
/>
<jira-import-form
v-else
- v-model="selectedProject"
- :import-label="importLabel"
:is-submitting="isSubmitting"
:issues-path="issuesPath"
+ :jira-imports="jiraImportDetails.imports"
:jira-projects="jiraImportDetails.projects"
:project-id="projectId"
:user-mappings="userMappings"
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index eb72e8581e8..db3b295642a 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAlert,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
@@ -10,15 +11,23 @@ import {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
+ GlSprintf,
GlTable,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
+import {
+ debounceWait,
+ dropdownLabel,
+ previousImportsMessage,
+ tableConfig,
+ userMappingMessage,
+} from '../utils/constants';
export default {
name: 'JiraImportForm',
components: {
+ GlAlert,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
@@ -29,29 +38,15 @@ export default {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
+ GlSprintf,
GlTable,
},
currentUsername: gon.current_username,
- dropdownLabel: __('The GitLab user to which the Jira user %{jiraDisplayName} will be mapped'),
- tableConfig: [
- {
- key: 'jiraDisplayName',
- label: __('Jira display name'),
- },
- {
- key: 'arrow',
- label: '',
- },
- {
- key: 'gitlabUsername',
- label: __('GitLab username'),
- },
- ],
+ dropdownLabel,
+ previousImportsMessage,
+ tableConfig,
+ userMappingMessage,
props: {
- importLabel: {
- type: String,
- required: true,
- },
isSubmitting: {
type: Boolean,
required: true,
@@ -60,6 +55,10 @@ export default {
type: String,
required: true,
},
+ jiraImports: {
+ type: Array,
+ required: true,
+ },
jiraProjects: {
type: Array,
required: true,
@@ -72,16 +71,12 @@ export default {
type: Array,
required: true,
},
- value: {
- type: String,
- required: false,
- default: undefined,
- },
},
data() {
return {
isFetching: false,
searchTerm: '',
+ selectedProject: undefined,
selectState: null,
users: [],
};
@@ -90,11 +85,25 @@ export default {
shouldShowNoMatchesFoundText() {
return !this.isFetching && this.users.length === 0;
},
+ numberOfPreviousImports() {
+ return this.jiraImports?.reduce?.(
+ (acc, jiraProject) => (jiraProject.jiraProjectKey === this.selectedProject ? acc + 1 : acc),
+ 0,
+ );
+ },
+ hasPreviousImports() {
+ return this.numberOfPreviousImports > 0;
+ },
+ importLabel() {
+ return this.selectedProject
+ ? `jira-import::${this.selectedProject}-${this.numberOfPreviousImports + 1}`
+ : 'jira-import::KEY-1';
+ },
},
watch: {
searchTerm: debounce(function debouncedUserSearch() {
this.searchUsers();
- }, 500),
+ }, debounceWait),
},
mounted() {
this.searchUsers()
@@ -129,9 +138,9 @@ export default {
},
initiateJiraImport(event) {
event.preventDefault();
- if (this.value) {
+ if (this.selectedProject) {
this.hideValidationError();
- this.$emit('initiateJiraImport', this.value);
+ this.$emit('initiateJiraImport', this.selectedProject);
} else {
this.showValidationError();
}
@@ -148,8 +157,16 @@ export default {
<template>
<div>
+ <gl-alert v-if="hasPreviousImports" variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.previousImportsMessage">
+ <template #numberOfPreviousImports>{{ numberOfPreviousImports }}</template>
+ </gl-sprintf>
+ </gl-alert>
+
<h3 class="page-title">{{ __('New Jira import') }}</h3>
+
<hr />
+
<form @submit="initiateJiraImport">
<gl-form-group
class="row align-items-center"
@@ -160,12 +177,11 @@ export default {
>
<gl-form-select
id="jira-project-select"
+ v-model="selectedProject"
data-qa-selector="jira_project_dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
- :value="value"
- @change="$emit('input', $event)"
/>
</gl-form-group>
@@ -186,16 +202,7 @@ export default {
<h4 class="gl-mb-4">{{ __('Jira-GitLab user mapping template') }}</h4>
- <p>
- {{
- __(
- `Jira users have been imported from the configured Jira instance.
- They can be mapped by selecting a GitLab user from the dropdown in the "GitLab
- username" column.
- When the form appears, the dropdown defaults to the user conducting the import.`,
- )
- }}
- </p>
+ <p>{{ $options.userMappingMessage }}</p>
<gl-table :fields="$options.tableConfig" :items="userMappings" fixed>
<template #cell(arrow)>
diff --git a/app/assets/javascripts/jira_import/utils/constants.js b/app/assets/javascripts/jira_import/utils/constants.js
new file mode 100644
index 00000000000..6adc3e5306c
--- /dev/null
+++ b/app/assets/javascripts/jira_import/utils/constants.js
@@ -0,0 +1,29 @@
+import { __ } from '~/locale';
+
+export const debounceWait = 500;
+
+export const dropdownLabel = __(
+ 'The GitLab user to which the Jira user %{jiraDisplayName} will be mapped',
+);
+
+export const previousImportsMessage = __(`You have imported from this project
+ %{numberOfPreviousImports} times before. Each new import will create duplicate issues.`);
+
+export const tableConfig = [
+ {
+ key: 'jiraDisplayName',
+ label: __('Jira display name'),
+ },
+ {
+ key: 'arrow',
+ label: '',
+ },
+ {
+ key: 'gitlabUsername',
+ label: __('GitLab username'),
+ },
+];
+
+export const userMappingMessage = __(`Jira users have been imported from the configured Jira
+ instance. They can be mapped by selecting a GitLab user from the dropdown in the "GitLab username"
+ column. When the form appears, the dropdown defaults to the user conducting the import.`);
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 1048e72a431..6a7bf81c643 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -3,11 +3,10 @@ import { debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlButton,
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownDivider,
+ GlNewDropdown,
GlLoadingIcon,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
GlSearchBoxByType,
GlModalDirective,
GlTooltipDirective,
@@ -29,11 +28,11 @@ export default {
components: {
Icon,
GlButton,
- GlDeprecatedDropdown,
+ GlNewDropdown,
GlLoadingIcon,
- GlDeprecatedDropdownItem,
- GlDeprecatedDropdownHeader,
- GlDeprecatedDropdownDivider,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
+
GlSearchBoxByType,
DateTimePicker,
@@ -112,6 +111,9 @@ export default {
showRearrangePanelsBtn() {
return !this.shouldShowEmptyState && this.rearrangePanelsAvailable;
},
+ environmentDropdownText() {
+ return this.currentEnvironmentName ?? '';
+ },
displayUtc() {
return this.dashboardTimezone === timezones.UTC;
},
@@ -179,31 +181,30 @@ export default {
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
<div class="mb-2 pr-2 d-flex d-sm-block">
- <gl-deprecated-dropdown
+ <gl-new-dropdown
id="monitor-environments-dropdown"
ref="monitorEnvironmentsDropdown"
class="flex-grow-1"
data-qa-selector="environments_dropdown"
toggle-class="dropdown-menu-toggle"
menu-class="monitor-environment-dropdown-menu"
- :text="currentEnvironmentName"
+ :text="environmentDropdownText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-deprecated-dropdown-header class="monitor-environment-dropdown-header text-center">
- {{ __('Environment') }}
- </gl-deprecated-dropdown-header>
- <gl-deprecated-dropdown-divider />
+ <gl-new-dropdown-header>{{ __('Environment') }}</gl-new-dropdown-header>
<gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" />
+
<gl-loading-icon v-if="environmentsLoading" :inline="true" />
<div v-else class="flex-fill overflow-auto">
- <gl-deprecated-dropdown-item
+ <gl-new-dropdown-item
v-for="environment in filteredEnvironments"
:key="environment.id"
- :active="environment.name === currentEnvironmentName"
- active-class="is-active"
+ :is-check-item="true"
+ :is-checked="environment.name === currentEnvironmentName"
:href="getEnvironmentPath(environment.id)"
- >{{ environment.name }}</gl-deprecated-dropdown-item
>
+ {{ environment.name }}
+ </gl-new-dropdown-item>
</div>
<div
v-show="shouldShowEnvironmentsDropdownNoMatchedMsg"
@@ -213,7 +214,7 @@ export default {
{{ __('No matching results') }}
</div>
</div>
- </gl-deprecated-dropdown>
+ </gl-new-dropdown>
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index c5e95036f4f..95bd4638d7e 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
@@ -19,7 +19,7 @@ import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
},
directives: {
@@ -82,16 +82,16 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
+ <gl-button
:id="`js-ci-action-${link}`"
v-gl-tooltip="{ boundary: 'viewport' }"
:title="tooltipText"
:class="cssClass"
:disabled="isDisabled"
- class="js-ci-action btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper d-flex align-items-center justify-content-center"
+ class="js-ci-action ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
@click="onClickAction"
>
<gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
<icon v-else :name="actionIcon" />
- </gl-deprecated-button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index 8f471fe2603..ea1379ee1d5 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -221,7 +221,9 @@ export default {
>
<template #button-content>
<span class="gl-flex-grow-1 text-truncate">{{ timeWindowText }}</span>
- <span v-if="utc" class="text-muted gl-font-weight-bold gl-font-sm">{{ __('UTC') }}</span>
+ <span v-if="utc" class="gl-text-gray-500 gl-font-weight-bold gl-font-sm">{{
+ __('UTC')
+ }}</span>
<gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 59401c34c48..7b3d1d0afd6 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const ANY_AUTHOR = 'Any';
export const NO_LABEL = 'No label';
@@ -8,3 +10,14 @@ export const SortDirection = {
descending: 'descending',
ascending: 'ascending',
};
+
+export const defaultMilestones = [
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'None', text: __('None') },
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'Any', text: __('Any') },
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'Upcoming', text: __('Upcoming') },
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'Started', text: __('Started') },
+];
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 74f312d9b8f..fe2e4c09618 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
@@ -15,6 +15,7 @@ import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
+import { stripQuotes } from './filtered_search_utils';
import { SortDirection } from './constants';
export default {
@@ -203,6 +204,26 @@ export default {
searchInputEl.blur();
}
},
+ /**
+ * This method removes quotes enclosure from filter values which are
+ * done by `GlFilteredSearch` internally when filter value contains
+ * spaces.
+ */
+ removeQuotesEnclosure(filters = []) {
+ return filters.map(filter => {
+ if (typeof filter === 'object') {
+ const valueString = filter.value.data;
+ return {
+ ...filter,
+ value: {
+ data: stripQuotes(valueString),
+ operator: filter.value.operator,
+ },
+ };
+ }
+ return filter;
+ });
+ },
handleSortOptionClick(sortBy) {
this.selectedSortOption = sortBy;
this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]);
@@ -215,7 +236,7 @@ export default {
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleHistoryItemSelected(filters) {
- this.$emit('onFilter', filters);
+ this.$emit('onFilter', this.removeQuotesEnclosure(filters));
},
handleClearHistory() {
const resultantSearches = this.recentSearchesStore.setRecentSearches([]);
@@ -237,7 +258,7 @@ export default {
});
}
this.blurSearchInput();
- this.$emit('onFilter', filters);
+ this.$emit('onFilter', this.removeQuotesEnclosure(filters));
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
new file mode 100644
index 00000000000..85f7f746b49
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -0,0 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
+export const stripQuotes = value => {
+ return value.includes(' ') ? value.slice(1, -1) : value;
+};
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 0d9bc70d38f..4c4a0125784 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -13,6 +13,7 @@ import { __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { stripQuotes } from '../filtered_search_utils';
import { NO_LABEL, DEBOUNCE_DELAY } from '../constants';
export default {
@@ -45,12 +46,9 @@ export default {
return this.value.data.toLowerCase();
},
activeLabel() {
- // Strip double quotes
- const strippedCurrentValue = this.currentValue.includes(' ')
- ? this.currentValue.substring(1, this.currentValue.length - 1)
- : this.currentValue;
-
- return this.labels.find(label => label.title.toLowerCase() === strippedCurrentValue);
+ return this.labels.find(
+ label => label.title.toLowerCase() === stripQuotes(this.currentValue),
+ );
},
containerStyle() {
if (this.activeLabel) {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
new file mode 100644
index 00000000000..cf1ac4e718b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -0,0 +1,110 @@
+<script>
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlNewDropdownDivider as GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+
+import { stripQuotes } from '../filtered_search_utils';
+import { defaultMilestones, DEBOUNCE_DELAY } from '../constants';
+
+export default {
+ defaultMilestones,
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ milestones: this.config.initialMilestones || [],
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data.toLowerCase();
+ },
+ activeMilestone() {
+ return this.milestones.find(
+ milestone => milestone.title.toLowerCase() === stripQuotes(this.currentValue),
+ );
+ },
+ },
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.milestones.length) {
+ this.fetchMilestoneBySearchTerm(this.value.data);
+ }
+ },
+ },
+ },
+ methods: {
+ fetchMilestoneBySearchTerm(searchTerm = '') {
+ this.loading = true;
+ this.config
+ .fetchMilestones(searchTerm)
+ .then(({ data }) => {
+ this.milestones = data;
+ })
+ .catch(() => createFlash(__('There was a problem fetching milestones.')))
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ searchMilestones: debounce(function debouncedSearch({ data }) {
+ this.fetchMilestoneBySearchTerm(data);
+ }, DEBOUNCE_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchMilestones"
+ >
+ <template #view="{ inputValue }">
+ <span>%{{ activeMilestone ? activeMilestone.title : inputValue }}</span>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="milestone in $options.defaultMilestones"
+ :key="milestone.value"
+ :value="milestone.value"
+ >{{ milestone.text }}</gl-filtered-search-suggestion
+ >
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ :value="milestone.title"
+ >
+ <div>{{ milestone.title }}</div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ece678d2f02..212324ddbca 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -670,24 +670,14 @@
&.ci-action-icon-wrapper {
height: 30px;
width: 30px;
- background: $white;
- border: 1px solid $border-color;
border-radius: 100%;
display: block;
-
- &:hover {
- background-color: $gray-darker;
- border: 1px solid $dropdown-toggle-active-border-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
+ padding: 0;
+ line-height: initial;
svg {
fill: $gl-text-color-secondary;
- position: relative;
- top: -1px;
+ vertical-align: initial;
}
.spinner {
@@ -696,7 +686,8 @@
&.play {
svg {
- left: 2px;
+ left: 1px;
+ top: 1px;
}
}
}
@@ -846,15 +837,12 @@ button.mini-pipeline-graph-dropdown-toggle {
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
width: $ci-action-dropdown-button-size;
-
- background: $white;
- border: 1px solid $border-color;
border-radius: 50%;
display: block;
&:hover {
+ box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
background-color: $gray-darker;
- border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
@@ -867,7 +855,7 @@ button.mini-pipeline-graph-dropdown-toggle {
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
- top: auto;
+ top: 1px;
vertical-align: initial;
}
}
@@ -875,7 +863,7 @@ button.mini-pipeline-graph-dropdown-toggle {
// SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
- top: 4px;
+ top: 5px;
}
.scrollable-menu {
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index 258a3f3e180..a3b6cbdff25 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -63,10 +63,6 @@
}
.prometheus-graphs-header {
- .monitor-environment-dropdown-header header {
- font-size: $gl-font-size;
- }
-
.monitor-environment-dropdown-menu,
.monitor-dashboard-dropdown-menu {
&.show {
diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb
index ca6b3da67bf..c13baaea8c6 100644
--- a/app/controllers/projects/ci/lints_controller.rb
+++ b/app/controllers/projects/ci/lints_controller.rb
@@ -8,17 +8,30 @@ class Projects::Ci::LintsController < Projects::ApplicationController
def create
@content = params[:content]
- result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options)
-
- @status = result.valid?
- @errors = result.errors
- @warnings = result.warnings
-
- if result.valid?
- @config_processor = result.config
- @stages = @config_processor.stages
- @builds = @config_processor.builds
- @jobs = @config_processor.jobs
+ @dry_run = params[:dry_run]
+
+ if @dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
+ pipeline = Ci::CreatePipelineService
+ .new(@project, current_user, ref: @project.default_branch)
+ .execute(:push, dry_run: true, content: @content)
+
+ @status = pipeline.error_messages.empty?
+ @stages = pipeline.stages
+ @errors = pipeline.error_messages.map(&:content)
+ @warnings = pipeline.warning_messages.map(&:content)
+ else
+ result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options)
+
+ @status = result.valid?
+ @errors = result.errors
+ @warnings = result.warnings
+
+ if result.valid?
+ @config_processor = result.config
+ @stages = @config_processor.stages
+ @builds = @config_processor.builds
+ @jobs = @config_processor.jobs
+ end
end
render :show
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
new file mode 100644
index 00000000000..749726e0e33
--- /dev/null
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Ci
+ module PipelinesHelper
+ def pipeline_warnings(pipeline)
+ return unless pipeline.warning_messages.any?
+
+ content_tag(:div, class: 'alert alert-warning') do
+ content_tag(:h4, 'Warning:') <<
+ content_tag(:div) do
+ pipeline.warning_messages.each do |warning|
+ concat(markdown(warning.content))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 2121916159a..089d187da61 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -774,7 +774,7 @@ module ProjectsHelper
def project_access_token_available?(project)
return false if ::Gitlab.com?
- ::Feature.enabled?(:resource_access_token, project)
+ ::Feature.enabled?(:resource_access_token, project, default_enabled: true)
end
end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index fdca0ec696b..c8b55e7b39f 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -9,7 +9,7 @@ module Ci
##
# Variables in the environment name scope.
#
- def scoped_variables(environment: expanded_environment_name)
+ def scoped_variables(environment: expanded_environment_name, dependencies: true)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
@@ -18,7 +18,7 @@ module Ci
variables.concat(deployment_variables(environment: environment))
variables.concat(yaml_variables)
variables.concat(user_variables)
- variables.concat(dependency_variables)
+ variables.concat(dependency_variables) if dependencies
variables.concat(secret_instance_variables)
variables.concat(secret_group_variables)
variables.concat(secret_project_variables(environment: environment))
@@ -45,6 +45,12 @@ module Ci
end
end
+ def simple_variables_without_dependencies
+ strong_memoize(:variables_without_dependencies) do
+ scoped_variables(environment: nil, dependencies: false).to_runner_variables
+ end
+ end
+
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables if user.blank?
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 2d7f5014aa9..70ad18e80eb 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -19,9 +19,13 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Validate::External,
Gitlab::Ci::Pipeline::Chain::Populate,
+ Gitlab::Ci::Pipeline::Chain::StopDryRun,
Gitlab::Ci::Pipeline::Chain::Create,
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
- Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
+ Gitlab::Ci::Pipeline::Chain::Limit::JobActivity,
+ Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines,
+ Gitlab::Ci::Pipeline::Chain::Metrics,
+ Gitlab::Ci::Pipeline::Chain::Pipeline::Process].freeze
# Create a new pipeline in the specified project.
#
@@ -68,21 +72,14 @@ module Ci
bridge: bridge,
**extra_options(options))
- sequence = Gitlab::Ci::Pipeline::Chain::Sequence
- .new(pipeline, command, SEQUENCE)
+ # Ensure we never persist the pipeline when dry_run: true
+ @pipeline.readonly! if command.dry_run?
- sequence.build! do |pipeline, sequence|
- schedule_head_pipeline_update
+ Gitlab::Ci::Pipeline::Chain::Sequence
+ .new(pipeline, command, SEQUENCE)
+ .build!
- if sequence.complete?
- cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
- pipeline_created_counter.increment(source: source)
-
- Ci::ProcessPipelineService
- .new(pipeline)
- .execute(nil, initial_process: true)
- end
- end
+ schedule_head_pipeline_update if pipeline.persisted?
# If pipeline is not persisted, try to recover IID
pipeline.reset_project_iid unless pipeline.persisted? ||
@@ -110,38 +107,14 @@ module Ci
commit.try(:id)
end
- def cancel_pending_pipelines
- Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
- cancelables.find_each do |cancelable|
- cancelable.auto_cancel_running(pipeline)
- end
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def auto_cancelable_pipelines
- project.ci_pipelines
- .where(ref: pipeline.ref)
- .where.not(id: pipeline.same_family_pipeline_ids)
- .where.not(sha: project.commit(pipeline.ref).try(:id))
- .alive_or_scheduled
- .with_only_interruptible_builds
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def pipeline_created_counter
- @pipeline_created_counter ||= Gitlab::Metrics
- .counter(:pipelines_created_total, "Counter of pipelines created")
- end
-
def schedule_head_pipeline_update
pipeline.all_merge_requests.opened.each do |merge_request|
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
end
- def extra_options(content: nil)
- { content: content }
+ def extra_options(content: nil, dry_run: false)
+ { content: content, dry_run: dry_run }
end
end
end
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index 2d0a78feb8e..3aa0e5650ee 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -32,7 +32,7 @@ module ResourceAccessTokens
attr_reader :resource_type, :resource
def feature_enabled?
- ::Feature.enabled?(:resource_access_token, resource)
+ ::Feature.enabled?(:resource_access_token, resource, default_enabled: true)
end
def has_permission_to_create?
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 91a26ff45b1..792b71b577f 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -13,6 +13,7 @@ class WebHookService
end
end
+ REQUEST_BODY_SIZE_LIMIT = 25.megabytes
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
attr_accessor :hook, :data, :hook_name, :request_options
@@ -53,7 +54,7 @@ class WebHookService
http_status: response.code,
message: response.to_s
}
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep => e
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep, Gitlab::Json::LimitedEncoder::LimitExceeded => e
log_execution(
trigger: hook_name,
url: hook.url,
@@ -83,7 +84,7 @@ class WebHookService
def make_request(url, basic_auth = false)
Gitlab::HTTP.post(url,
- body: data.to_json,
+ body: Gitlab::Json::LimitedEncoder.encode(data, limit: REQUEST_BODY_SIZE_LIMIT),
headers: build_headers(hook_name),
verify: hook.enable_ssl_verification,
basic_auth: basic_auth,
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index d9d646c77d9..69cb41b1713 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -16,5 +16,5 @@
%span.ci-build-text.text-truncate.mw-70p.gl-pl-1-deprecated-no-really-do-not-use-me= subject.name
- if status.has_action?
- = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
+ = link_to status.action_path, class: "gl-button ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/projects/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml
index d65c06aa2a4..5cc89343ba3 100644
--- a/app/views/projects/ci/lints/_create.html.haml
+++ b/app/views/projects/ci/lints/_create.html.haml
@@ -4,6 +4,8 @@
%b= _("Status:")
= _("syntax is correct")
+ = render "projects/ci/lints/lint_warnings", warnings: @warnings
+
.table-holder
%table.table.table-bordered
%thead
@@ -11,33 +13,54 @@
%th= _("Parameter")
%th= _("Value")
%tbody
- - @stages.each do |stage|
- - @builds.select { |build| build[:stage] == stage }.each do |build|
- - job = @jobs[build[:name].to_sym]
- %tr
- %td #{stage.capitalize} Job - #{build[:name]}
- %td
- %pre= job[:before_script].to_a.join('\n')
- %pre= job[:script].to_a.join('\n')
- %pre= job[:after_script].to_a.join('\n')
+ - if @dry_run
+ - @stages.each do |stage|
+ - stage.statuses.each do |job|
+ %tr
+ %td #{stage.name.capitalize} Job - #{job.name}
+ %td
+ %pre= job.options[:before_script].to_a.join('\n')
+ %pre= job.options[:script].to_a.join('\n')
+ %pre= job.options[:after_script].to_a.join('\n')
+ %br
+ %b= _("Tag list:")
+ = job.tag_list.to_a.join(", ") if job.is_a?(Ci::Build)
+ %br
+ %b= _("Environment:")
+ = job.options.dig(:environment, :name)
+ %br
+ %b= _("When:")
+ = job.when
+ - if job.allow_failure
+ %b= _("Allowed to fail")
- %br
- %b= _("Tag list:")
- = build[:tag_list].to_a.join(", ")
- %br
- %b= _("Only policy:")
- = job[:only].to_a.join(", ")
- %br
- %b= _("Except policy:")
- = job[:except].to_a.join(", ")
- %br
- %b= _("Environment:")
- = build[:environment]
- %br
- %b= _("When:")
- = build[:when]
- - if build[:allow_failure]
- %b= _("Allowed to fail")
+ - else
+ - @stages.each do |stage|
+ - @builds.select { |build| build[:stage] == stage }.each do |build|
+ - job = @jobs[build[:name].to_sym]
+ %tr
+ %td #{stage.capitalize} Job - #{build[:name]}
+ %td
+ %pre= job[:before_script].to_a.join('\n')
+ %pre= job[:script].to_a.join('\n')
+ %pre= job[:after_script].to_a.join('\n')
+ %br
+ %b= _("Tag list:")
+ = build[:tag_list].to_a.join(", ")
+ %br
+ %b= _("Only policy:")
+ = job[:only].to_a.join(", ")
+ %br
+ %b= _("Except policy:")
+ = job[:except].to_a.join(", ")
+ %br
+ %b= _("Environment:")
+ = build[:environment]
+ %br
+ %b= _("When:")
+ = build[:when]
+ - if build[:allow_failure]
+ %b= _("Allowed to fail")
- else
.bs-callout.bs-callout-danger
@@ -47,3 +70,5 @@
%pre
- @errors.each do |message|
%p= message
+
+ = render "projects/ci/lints/lint_warnings", warnings: @warnings
diff --git a/app/views/projects/ci/lints/_lint_warnings.html.haml b/app/views/projects/ci/lints/_lint_warnings.html.haml
new file mode 100644
index 00000000000..0a5bb8f76ef
--- /dev/null
+++ b/app/views/projects/ci/lints/_lint_warnings.html.haml
@@ -0,0 +1,6 @@
+- if warnings
+ - warnings.each do |warning|
+ .bs-callout.bs-callout-warning
+ %p
+ %b= _("Warning:")
+ = markdown(warning)
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
index 33d9cd62aaf..0c51c978bfe 100644
--- a/app/views/projects/ci/lints/show.html.haml
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -3,7 +3,7 @@
- content_for :library_javascripts do
= page_specific_javascript_tag('lib/ace.js')
-%h2.pt-3.pb-3= _("Check your .gitlab-ci.yml")
+%h2.pt-3.pb-3= _("Validate your GitLab CI configuration")
.project-ci-linter
= form_tag project_ci_lint_path(@project), method: :post do
@@ -17,7 +17,11 @@
.col-sm-12
.float-left.gl-mt-3
= submit_tag(_('Validate'), class: 'btn btn-success submit-yml')
- .float-right.gl-mt-3
+ - if Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
+ = check_box_tag(:dry_run, 'true', params[:dry_run])
+ = label_tag(:dry_run, _('Simulate a pipeline created for the default branch'))
+ = link_to icon('question-circle'), help_page_path('ci/lint', anchor: 'pipeline-simulation'), target: '_blank', rel: 'noopener noreferrer'
+ .float-right.prepend-top-10
= button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml')
.row.prepend-top-20
diff --git a/app/views/projects/pipelines/_pipeline_warnings.html.haml b/app/views/projects/pipelines/_pipeline_warnings.html.haml
new file mode 100644
index 00000000000..e27bd440462
--- /dev/null
+++ b/app/views/projects/pipelines/_pipeline_warnings.html.haml
@@ -0,0 +1,6 @@
+- if warnings.any?
+ - warnings.map(&:content).each do |warning|
+ .bs-callout.bs-callout-warning
+ %p
+ %b= _("Warning:")
+ = markdown(warning)
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index f652346599d..5530033ca1b 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,3 +1,4 @@
+- return if pipeline_has_errors
- test_reports_enabled = Feature.enabled?(:junit_pipeline_view, @project)
- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab, @project, default_enabled: true)
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 0b1393f7392..726bf9af223 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -12,6 +12,7 @@
- else
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
+ = pipeline_warnings(@pipeline)
.form-group.row
.col-sm-12
= f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label'
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 2b2133b8296..e1a606b1765 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -1,6 +1,7 @@
- add_to_breadcrumbs _('Pipelines'), project_pipelines_path(@project)
- breadcrumb_title "##{@pipeline.id}"
- page_title _('Pipeline')
+- pipeline_has_errors = @pipeline.builds.empty? && @pipeline.yaml_errors.present?
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
#js-pipeline-header-vue.pipeline-header-container
@@ -8,7 +9,7 @@
- if @pipeline.commit.present?
= render "projects/pipelines/info", commit: @pipeline.commit
- - if @pipeline.builds.empty? && @pipeline.yaml_errors.present?
+ - if pipeline_has_errors
.bs-callout.bs-callout-danger
%h4= _('Found errors in your %{gitlab_ci_yml}:') % { gitlab_ci_yml: '.gitlab-ci.yml' }
%ul
@@ -17,7 +18,8 @@
- lint_link_url = project_ci_lint_path(@project)
- lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
- - else
- = render "projects/pipelines/with_tabs", pipeline: @pipeline
+
+ = render "projects/pipelines/pipeline_warnings", warnings: @pipeline.warning_messages
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } }
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index c3f5e6271d3..6e4feea1b26 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -27,7 +27,10 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
task = task.to_sym
- ::Projects::GitDeduplicationService.new(project).execute if task == :gc
+ if task == :gc
+ ::Projects::GitDeduplicationService.new(project).execute
+ cleanup_orphan_lfs_file_references(project)
+ end
gitaly_call(task, project.repository.raw_repository)
@@ -90,6 +93,13 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
raise Gitlab::Git::CommandError.new(e)
end
+ def cleanup_orphan_lfs_file_references(project)
+ return unless Feature.enabled?(:cleanup_lfs_during_gc, project)
+ return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
+
+ ::Gitlab::Cleanup::OrphanLfsFileReferences.new(project, dry_run: false, logger: logger).run!
+ end
+
def flush_ref_caches(project)
project.repository.after_create_branch
project.repository.branch_names
diff --git a/changelogs/unreleased/213717-update-environments-dropdowns.yml b/changelogs/unreleased/213717-update-environments-dropdowns.yml
new file mode 100644
index 00000000000..b52bf76317f
--- /dev/null
+++ b/changelogs/unreleased/213717-update-environments-dropdowns.yml
@@ -0,0 +1,6 @@
+---
+title: Improve environment dropdowns in operations metrics dashboard and highlight
+ selected environment
+merge_request: 39303
+author:
+type: changed
diff --git a/changelogs/unreleased/217801-step-2-fix-editor-lite-scroll.yml b/changelogs/unreleased/217801-step-2-fix-editor-lite-scroll.yml
new file mode 100644
index 00000000000..d55036ceec3
--- /dev/null
+++ b/changelogs/unreleased/217801-step-2-fix-editor-lite-scroll.yml
@@ -0,0 +1,5 @@
+---
+title: Fix scroll stuck on editor in snippets
+merge_request: 39251
+author:
+type: fixed
diff --git a/changelogs/unreleased/218722-enable-project-access-tokens-by-default.yml b/changelogs/unreleased/218722-enable-project-access-tokens-by-default.yml
new file mode 100644
index 00000000000..97ff421200e
--- /dev/null
+++ b/changelogs/unreleased/218722-enable-project-access-tokens-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable `:resource_access_token` feature flag by default
+merge_request: 39287
+author:
+type: added
diff --git a/changelogs/unreleased/219638-date-picked-utc-label-color.yml b/changelogs/unreleased/219638-date-picked-utc-label-color.yml
new file mode 100644
index 00000000000..c70931e831b
--- /dev/null
+++ b/changelogs/unreleased/219638-date-picked-utc-label-color.yml
@@ -0,0 +1,5 @@
+---
+title: Increase contrast between UTC label and input
+merge_request: 34998
+author:
+type: changed
diff --git a/changelogs/unreleased/219774-legacy-dropdown-edit.yml b/changelogs/unreleased/219774-legacy-dropdown-edit.yml
new file mode 100644
index 00000000000..8fec0880e66
--- /dev/null
+++ b/changelogs/unreleased/219774-legacy-dropdown-edit.yml
@@ -0,0 +1,5 @@
+---
+title: Replace <gl-deprecated-button> with <gl-button> in app/assets/javascripts/pipelines/components/graph/action_component.vue
+merge_request: 38923
+author:
+type: changed
diff --git a/changelogs/unreleased/223621-prune-lfs-objects-during-git-gc.yml b/changelogs/unreleased/223621-prune-lfs-objects-during-git-gc.yml
new file mode 100644
index 00000000000..b6147c6fce1
--- /dev/null
+++ b/changelogs/unreleased/223621-prune-lfs-objects-during-git-gc.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up orphaned LFS file references during GC
+merge_request: 38813
+author:
+type: added
diff --git a/changelogs/unreleased/ci-lint-view-error-messages.yml b/changelogs/unreleased/ci-lint-view-error-messages.yml
new file mode 100644
index 00000000000..1a22fe4b73a
--- /dev/null
+++ b/changelogs/unreleased/ci-lint-view-error-messages.yml
@@ -0,0 +1,5 @@
+---
+title: UI warning messages for pipeline configurations
+merge_request: 38734
+author:
+type: added
diff --git a/changelogs/unreleased/use-dry-run-on-ci-lint.yml b/changelogs/unreleased/use-dry-run-on-ci-lint.yml
new file mode 100644
index 00000000000..5e26394acb5
--- /dev/null
+++ b/changelogs/unreleased/use-dry-run-on-ci-lint.yml
@@ -0,0 +1,5 @@
+---
+title: Allow user to simulate pipeline creation via CI Lint and go beyond syntax checks
+merge_request: 37828
+author:
+type: added
diff --git a/config/feature_flags/development/sse_erb_support.yml b/config/feature_flags/development/sse_erb_support.yml
new file mode 100644
index 00000000000..21a3ad2104b
--- /dev/null
+++ b/config/feature_flags/development/sse_erb_support.yml
@@ -0,0 +1,7 @@
+---
+name: sse_erb_support
+introduced_by_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235460
+group: group::static site editor
+type: development
+default_enabled: false
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 6ffde46a212..f30dba331b8 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -160,7 +160,7 @@ There is a limit when embedding metrics in GFM for performance reasons.
## Number of webhooks
-On GitLab.com, the [maximum number of webhooks](../user/gitlab_com/index.md#maximum-number-of-webhooks) per project, and per group, is limited.
+On GitLab.com, the [maximum number of webhooks and their size](../user/gitlab_com/index.md#webhooks) per project, and per group, is limited.
To set this limit on a self-managed installation, run the following in the
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 82ee2fb1db5..79ac10d4ac6 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -847,7 +847,7 @@ Only available to group owners and administrators.
This endpoint either:
- Removes group, and queues a background job to delete all projects in the group as well.
-- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
+- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-delay-premium-only).
```plaintext
DELETE /groups/:id
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0b59e0193f9..06ddce7a871 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1841,7 +1841,7 @@ This endpoint:
group admins can [configure](../user/group/index.md#enabling-delayed-project-removal-premium) projects within a group
to be deleted after a delayed period.
When enabled, actual deletion happens after the number of days
-specified in the [default deletion period](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
+specified in the [default deletion delay](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-delay-premium-only).
CAUTION: **Warning:**
The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6
diff --git a/doc/ci/img/ci_lint.png b/doc/ci/img/ci_lint.png
new file mode 100644
index 00000000000..e62de011293
--- /dev/null
+++ b/doc/ci/img/ci_lint.png
Binary files differ
diff --git a/doc/ci/img/ci_lint_dry_run.png b/doc/ci/img/ci_lint_dry_run.png
new file mode 100644
index 00000000000..4092b66d534
--- /dev/null
+++ b/doc/ci/img/ci_lint_dry_run.png
Binary files differ
diff --git a/doc/ci/lint.md b/doc/ci/lint.md
new file mode 100644
index 00000000000..716a4218d97
--- /dev/null
+++ b/doc/ci/lint.md
@@ -0,0 +1,41 @@
+# CI Lint
+
+If you want to test the validity of your GitLab CI/CD configuration before committing
+the changes, you can use the CI Lint tool. This tool checks for syntax and logical
+errors by default, and can simulate pipeline creation to try to find more complicated
+issues as well.
+
+To access the CI Lint tool, navigate to **CI/CD > Pipelines** or **CI/CD > Jobs**
+in your project and click **CI lint**.
+
+## Validate basic logic and syntax
+
+By default, the CI lint checks the syntax of your CI YAML configuration and also runs
+some basic logical validations.
+
+To use the CI lint, paste a complete CI configuration (`.gitlab-ci.yml` for example)
+into the text box and click **Validate**:
+
+![CI Lint](img/ci_lint.png)
+
+## Pipeline simulation
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229794) in GitLab 13.3.
+
+Not all pipeline configuration issues can be found by the [basic CI lint validation](#validate-basic-logic-and-syntax).
+You can simulate the creation of a pipeline for deeper validation that can discover
+more complicated issues.
+
+To validate the configuration by running a pipeline simulation:
+
+1. Paste the GitLab CI configuration to verify into the text box.
+1. Click the **Simulate pipeline creation for the default branch** checkbox.
+1. Click **Validate**.
+
+![Dry run](img/ci_lint_dry_run.png)
+
+### Pipeline simulation limitations
+
+Simulations run as `git push` events against the default branch. You must have
+[permissions](../user/permissions.md#project-members-permissions) to create pipelines
+on this branch to validate with a simulation.
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 2ced58db663..050df243af4 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -114,9 +114,7 @@ Jobs are used to create jobs, which are then picked by
What is important is that each job is run independently from each other.
If you want to check whether the `.gitlab-ci.yml` of your project is valid, there is a
-Lint tool under the page `/-/ci/lint` of your project namespace. You can also find
-a "CI Lint" button to go to this page under **CI/CD âž” Pipelines** and
-**Pipelines âž” Jobs** in your project.
+[CI Lint tool](../lint.md) available in every project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
[the reference documentation on `.gitlab-ci.yml`](../yaml/README.md).
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index f13f1f1f36e..f68c502d828 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -73,7 +73,7 @@ Kubernetes-specific environment variables are detailed in the
| `CI_KUBERNETES_ACTIVE` | 13.0 | all | Included with the value `true` only if the pipeline has a Kubernetes cluster available for deployments. Not included if no cluster is available. Can be used as an alternative to [`only:kubernetes`/`except:kubernetes`](../yaml/README.md#onlykubernetesexceptkubernetes) with [`rules:if`](../yaml/README.md#rulesif) |
| `CI_MERGE_REQUEST_ASSIGNEES` | 11.9 | all | Comma-separated list of username(s) of assignee(s) for the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_ID` | 11.6 | all | The instance-level ID of the merge request. Only available if [the pipelines are for merge requests](../merge_request_pipelines/index.md) and the merge request is created. |
-| `CI_MERGE_REQUEST_IID` | 11.6 | all | The project-level IID of the merge request. Only available If [the pipelines are for merge requests](../merge_request_pipelines/index.md) and the merge request is created. |
+| `CI_MERGE_REQUEST_IID` | 11.6 | all | The project-level IID (internal ID) of the merge request. Only available If [the pipelines are for merge requests](../merge_request_pipelines/index.md) and the merge request is created. |
| `CI_MERGE_REQUEST_LABELS` | 11.9 | all | Comma-separated label names of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_MILESTONE` | 11.9 | all | The milestone title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_PROJECT_ID` | 11.6 | all | The ID of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
@@ -93,8 +93,8 @@ Kubernetes-specific environment variables are detailed in the
| `CI_NODE_TOTAL` | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. |
| `CI_PAGES_DOMAIN` | 11.8 | all | The configured domain that hosts GitLab Pages. |
| `CI_PAGES_URL` | 11.8 | all | URL to GitLab Pages-built pages. Always belongs to a subdomain of `CI_PAGES_DOMAIN`. |
-| `CI_PIPELINE_ID` | 8.10 | all | The unique ID of the current pipeline that GitLab CI/CD uses internally |
-| `CI_PIPELINE_IID` | 11.0 | all | The unique ID of the current pipeline scoped to project |
+| `CI_PIPELINE_ID` | 8.10 | all | The instance-level ID of the current pipeline. |
+| `CI_PIPELINE_IID` | 11.0 | all | The project-level IID (internal ID) of the current pipeline. |
| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `schedule`, `api`, `external`, `chat`, `webide`, `merge_request_event`, `external_pull_request_event`, `parent_pipeline`, [`trigger`, or `pipeline`](../triggers/README.md#authentication-tokens) (renamed to `cross_project_pipeline` since 13.0). For pipelines created before GitLab 9.5, this is displayed as `unknown`. |
| `CI_PIPELINE_TRIGGERED` | all | all | The flag to indicate that job was [triggered](../triggers/README.md) |
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 660e5782cf2..75f95988cee 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -304,6 +304,18 @@ To define your own `workflow: rules`, the configuration options currently availa
If a pipeline attempts to run but matches no rule, it's dropped and doesn't run.
+Use the example rules below exactly as written to allow pipelines that match the rule
+to run. Add `when: never` to prevent pipelines that match the rule from running. See
+the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
+
+| Example rules | Details |
+|------------------------------------------------------|-----------------------------------------------------------|
+| `if: '$CI_PIPELINE_SOURCE == "merge_request_event"'` | Control when merge request pipelines run. |
+| `if: '$CI_PIPELINE_SOURCE == "push"'` | Control when both branch pipelines and tag pipelines run. |
+| `if: $CI_COMMIT_TAG` | Control when tag pipelines run. |
+| `if: $CI_COMMIT_BRANCH` | Control when branch pipelines run. |
+| `if: '$CI_COMMIT_BRANCH && $CI_COMMIT_BEFORE_SHA != "0000000000000000000000000000000000000000"'` | Control when pipelines run for new branches that are created or pushed with no commits. See the [skip job if branch is empty](#skip-job-if-branch-is-empty) example for more details. |
+
For example, with the following configuration, pipelines run for all `push` events (changes to
branches and new tags) as long as they *don't* have `-wip` in the commit message. Scheduled
pipelines and merge request pipelines don't run, as there's no rule allowing them.
@@ -339,14 +351,6 @@ As with `rules` defined in jobs, be careful not to use a configuration that allo
merge request pipelines and branch pipelines to run at the same time, or you could
have [duplicate pipelines](#differences-between-rules-and-onlyexcept).
-Useful workflow rules clauses:
-
-| Clause | Details |
-|----------------------------------------------------------------------------|---------------------------------------------------------|
-| `if: '$CI_PIPELINE_SOURCE == "merge_request_event"'` | Allow or block merge request pipelines. |
-| `if: '$CI_PIPELINE_SOURCE == "push"'` | Allow or block both branch pipelines and tag pipelines. |
-| `if: '$CI_COMMIT_BEFORE_SHA == '0000000000000000000000000000000000000000'` | Allow or block pipeline creation when new branches are created or pushed with no commits. This will also skip tag and scheduled pipelines. See [common `rules:if` clauses](#common-if-clauses-for-rules) for examples on how to define these rules more strictly. |
-
#### `workflow:rules` templates
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217732) in GitLab 13.0.
@@ -1365,27 +1369,17 @@ Other commonly used variables for `if` clauses:
- `if: '$CUSTOM_VARIABLE == "value1"'`: If the custom variable `CUSTOM_VARIABLE` is
exactly `value1`.
-To avoid running pipelines when a branch is created without any changes,
-check the value of `$CI_COMMIT_BEFORE_SHA`. It has a value of
-`0000000000000000000000000000000000000000`:
-
-- In branches with no commits.
-- In tag pipelines and scheduled pipelines. You should define rules very
- narrowly if you don't want to skip these.
-
-To skip pipelines on all empty branches, but also tags and schedules:
+##### Skip job if branch is empty
-```yaml
-rules:
- - if: $CI_COMMIT_BEFORE_SHA == '0000000000000000000000000000000000000000'
- when: never
-```
+A branch has no commits if the value of`$CI_COMMIT_BEFORE_SHA` is
+`0000000000000000000000000000000000000000`. You can use this value to
+avoid running a job on branches with no commits.
-To skip branch pipelines when the branch is empty:
+To run a job only on branches with commits:
```yaml
rules:
- - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BEFORE_SHA != '0000000000000000000000000000000000000000'
+ - if: '$CI_COMMIT_BRANCH && $CI_COMMIT_BEFORE_SHA != "0000000000000000000000000000000000000000"'
```
#### `rules:changes`
@@ -2134,6 +2128,26 @@ build_job:
artifacts: true
```
+Environment variables support for `project:`, `job:`, and `ref` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202093)
+in GitLab 13.3. This is under development, but it is ready for production use. It is deployed
+behind the `ci_expand_names_for_cross_pipeline_artifacts` feature flag, which is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can enable it for your instance.
+
+For example:
+
+```yaml
+build_job:
+ stage: build
+ script:
+ - ls -lhR
+ needs:
+ - project: $CI_PROJECT_PATH
+ job: $DEPENDENCY_JOB_NAME
+ ref: $CI_COMMIT_BRANCH
+ artifacts: true
+```
+
NOTE: **Note:**
Downloading artifacts from jobs that are run in [`parallel:`](#parallel) is not supported.
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 9aaea677d00..51d0e33f67d 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -497,6 +497,7 @@ If we need to test how our component renders when results from the GraphQL API a
designs: {
loading,
},
+ },
};
wrapper = shallowMount(Index, {
diff --git a/doc/development/filtering_by_label.md b/doc/development/filtering_by_label.md
index ef92bd35985..9c1993fdf7f 100644
--- a/doc/development/filtering_by_label.md
+++ b/doc/development/filtering_by_label.md
@@ -1,3 +1,8 @@
+---
+stage: Plan
+group: Project Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
# Filtering by label
## Introduction
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index f8c20bd06c9..980691c6b4f 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -196,7 +196,7 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. Make s
// => 'This is &lt;strong&gt;&lt;script&gt;alert(&#x27;evil&#x27;)&lt;/script&gt;&lt;/strong&gt;'
// OK:
- sprintf(__('This is %{value}'), { value: `<strong>${escape(someDynamicValue)}</strong>`, false);
+ sprintf(__('This is %{value}'), { value: `<strong>${escape(someDynamicValue)}</strong>` }, false);
// => 'This is <strong>&lt;script&gt;alert(&#x27;evil&#x27;)&lt;/script&gt;</strong>'
```
diff --git a/doc/development/issuable-like-models.md b/doc/development/issuable-like-models.md
index d252735dbd8..9029886c334 100644
--- a/doc/development/issuable-like-models.md
+++ b/doc/development/issuable-like-models.md
@@ -1,3 +1,8 @@
+---
+stage: Plan
+group: Project Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
# Issuable-like Rails models utilities
GitLab Rails codebase contains several models that hold common functionality and behave similarly to
@@ -9,9 +14,9 @@ This guide accumulates guidelines on working with such Rails models.
## Important text fields
-There are max length constraints for the most important text fields for `Issuable`s:
+There are maximum length constraints for the most important text fields for issuables:
-- `title`: 255 chars
-- `title_html`: 800 chars
+- `title`: 255 characters
+- `title_html`: 800 characters
- `description`: 1 megabyte
- `description_html`: 5 megabytes
diff --git a/doc/development/issue_types.md b/doc/development/issue_types.md
index 028d42b27fc..416aa65b13f 100644
--- a/doc/development/issue_types.md
+++ b/doc/development/issue_types.md
@@ -1,3 +1,9 @@
+---
+stage: Plan
+group: Project Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Issue Types
Sometimes when a new resource type is added it's not clear if it should be only an
diff --git a/doc/operations/metrics/index.md b/doc/operations/metrics/index.md
index 99b3a485002..6c6986bd6a3 100644
--- a/doc/operations/metrics/index.md
+++ b/doc/operations/metrics/index.md
@@ -4,7 +4,64 @@ group: APM
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
-# Metrics dashboard for your CI/CD environment **(CORE)**
+# Monitor your CI/CD environment's metrics **(CORE)**
+
+GitLab helps your team monitor the health and performance of your applications
+and infrastructure by turning statistics and log files into charts and graphs
+that are easy to understand, especially when time is short and decisions are
+critical. For GitLab to display your information in charts, you must:
+
+1. **Instrument your application** - Collect accurate and complete measurements.
+ <I class="fa fa-youtube-play youtube" aria-hidden="true"></I>
+ For an overview, see [How to instrument Prometheus metrics in GitLab](https://www.youtube.com/watch?v=tuI2oJ3TTB4).
+1. **Expose metrics for capture** - Make logs, metrics, and traces available for capture.
+1. [**Configure Prometheus to gather metrics**](#configure-prometheus-to-gather-metrics) -
+ Deploy managed applications like Elasticsearch, Prometheus, and Jaeger to gather
+ the data you've exposed.
+1. **GitLab collects metrics** - GitLab uses Prometheus to scrape the data you've
+ captured in your managed apps, and prepares the data for display. To learn more, read
+ [Collect and process metrics](#collect-and-process-metrics).
+1. **Display charts in the GitLab user interface** - GitLab converts your metrics
+ into easy-to-read charts on a default dashboard. You can create as many custom charts
+ and custom dashboards as needed so your team has full insight into your
+ application's health.
+
+## Configure Prometheus to gather metrics
+
+You must connect a Prometheus instance to GitLab to collect metrics. How you configure
+your Prometheus integration depends on where your apps are running:
+
+- **For manually-configured Prometheus** -
+ [Specify your Prometheus server](../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus),
+ and define at least one environment.
+- **For GitLab-managed Prometheus** - GitLab can
+ [deploy and manage Prometheus](../../user/project/integrations/prometheus.md#managed-prometheus-on-kubernetes) for you.
+ You must also complete a code deployment, as described in
+ [Deploy code with GitLab-managed Prometheus](#deploy-code-with-gitlab-managed-prometheus),
+ for the **Operations > Metrics** page to contain data.
+
+### Deploy code with GitLab-managed Prometheus
+
+For GitLab-managed Prometheus, you can set up [Auto DevOps](../../topics/autodevops/index.md)
+to quickly create a deployment:
+
+1. Navigate to your project's **Operations > Kubernetes** page.
+1. Ensure that, in addition to Prometheus, you also have Runner and Ingress
+ installed.
+1. After installing Ingress, copy its endpoint.
+1. Navigate to your project's **Settings > CI/CD** page. In the
+ **Auto DevOps** section, select a deployment strategy and save your changes.
+1. On the same page, in the **Variables** section, add a variable named
+ `KUBE_INGRESS_BASE_DOMAIN` with the value of the Ingress endpoint you
+ copied previously. Leave the type as **Variable**.
+1. Navigate to your project's **{rocket}** **CI/CD > Pipelines** page, and run a
+ pipeline on any branch.
+1. When the pipeline has run successfully, graphs are available on the
+ **Operations > Metrics** page.
+
+![Monitoring Dashboard](img/prometheus_monitoring_dashboard_v13_3.png)
+
+## Collect and process metrics
After [configuring Prometheus for a cluster](../../user/project/integrations/prometheus.md),
GitLab attempts to retrieve performance metrics for any [environment](../../ci/environments/index.md) with
@@ -16,7 +73,8 @@ the supported metrics and scan processes, see the
[Prometheus Metrics Library documentation](../../user/project/integrations/prometheus_library/index.md).
To view the metrics dashboard for an environment that has
-[completed at least one deployment](#populate-your-metrics-dashboard):
+To view the metrics dashboard for an environment that is
+[configured to gather metrics](#configure-prometheus-to-gather-metrics):
1. *If the metrics dashboard is only visible to project members,* sign in to
GitLab as a member of a project. Learn more about [metrics dashboard visibility](#metrics-dashboard-visibility).
@@ -53,29 +111,6 @@ navigation bar contains:
- **Metrics settings** - Configure the
[settings for this dashboard](dashboards/index.md#manage-the-metrics-dashboard-settings).
-## Populate your metrics dashboard
-
-After [configuring Prometheus for a cluster](../../user/project/integrations/prometheus.md),
-you must also deploy code for the **Operations > Metrics** page
-to contain data. Setting up [Auto DevOps](../../topics/autodevops/index.md)
-helps quickly create a deployment:
-
-1. Navigate to your project's **Operations > Kubernetes** page.
-1. Ensure that, in addition to Prometheus, you also have Runner and Ingress
- installed.
-1. After installing Ingress, copy its endpoint.
-1. Navigate to your project's **Settings > CI/CD** page. In the
- **Auto DevOps** section, select a deployment strategy and save your changes.
-1. On the same page, in the **Variables** section, add a variable named
- `KUBE_INGRESS_BASE_DOMAIN` with the value of the Ingress endpoint you
- copied previously. Leave the type as **Variable**.
-1. Navigate to your project's **{rocket}** **CI/CD > Pipelines** page, and run a
- pipeline on any branch.
-1. When the pipeline has run successfully, graphs are available on the
- **Operations > Metrics** page.
-
-![Monitoring Dashboard](img/prometheus_monitoring_dashboard_v13_3.png)
-
## Customize your metrics dashboard
After creating your dashboard, you can customize it to meet your needs:
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index af9be499e80..3d7aa3026ab 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -5,7 +5,7 @@ type: concepts, reference, howto
# Webhooks and insecure internal web services
NOTE: **Note:**
-On GitLab.com the [maximum number of webhooks](../user/gitlab_com/index.md#maximum-number-of-webhooks) per project is limited.
+On GitLab.com, the [maximum number of webhooks and their size](../user/gitlab_com/index.md#webhooks) per project, and per group, is limited.
If you have non-GitLab web services running on your GitLab server or within its
local network, these may be vulnerable to exploitation via Webhooks.
diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md
index 82718f0d2e4..e5c7947399d 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -67,7 +67,7 @@ To ensure only admin users can delete projects:
1. Check the **Default project deletion protection** checkbox.
1. Click **Save changes**.
-## Default deletion adjourned period **(PREMIUM ONLY)**
+## Default deletion delay **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6.
@@ -88,7 +88,7 @@ To change this period:
1. Select the desired option.
1. Click **Save changes**.
-### Override default deletion adjourned period
+### Override default deletion delayed period
Alternatively, projects that are marked for removal can be deleted immediately. To do so:
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 063e5f77adc..aa9e6715335 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -116,12 +116,13 @@ All our runners are deployed into Google Cloud Platform (GCP) - any IP based
firewall can be configured by looking up all
[IP address ranges or CIDR blocks for GCP](https://cloud.google.com/compute/docs/faq#where_can_i_find_product_name_short_ip_ranges).
-## Maximum number of webhooks
+## Webhooks
A limit of:
- 100 webhooks applies to projects.
- 50 webhooks applies to groups. **(BRONZE ONLY)**
+- Payload is limited to 25MB
## Shared Runners
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index cad74900f16..0d7d9bb2a7b 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -480,7 +480,7 @@ To remove a group and its contents:
This action either:
- Removes the group, and also queues a background job to delete all projects in that group.
-- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
+- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay-premium-only).
### Restore a group **(PREMIUM)**
@@ -660,7 +660,7 @@ Optionally, on [Premium or Silver](https://about.gitlab.com/pricing/) or higher
you can configure the projects within a group to be deleted after a delayed interval.
During this interval period, the projects will be in a read-only state and can be restored, if required.
-The interval period defaults to 7 days, and can be modified by an admin in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
+The interval period defaults to 7 days, and can be modified by an admin in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay-premium-only).
To enable delayed deletion of projects:
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index a1d957ce128..fbbc5050a1f 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -142,7 +142,7 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
- **Kubernetes cluster name** - The name you wish to give the cluster.
- **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
- **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14.
- - **Role name** - Select the **EKS IAM role** you created earlier to allow Amazon EKS
+ - **Service role** - Select the **EKS IAM role** you created earlier to allow Amazon EKS
and the Kubernetes control plane to manage AWS resources on your behalf.
NOTE: **Note:**
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 908007239ac..154cc882397 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -193,7 +193,7 @@ To remove a project, first navigate to the home page for that project.
### Delayed removal **(PREMIUM)**
By default, clicking to remove a project is followed by a seven day delay. Admins can restore the project during this period of time.
-This delay [may be changed by an admin](../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
+This delay [may be changed by an admin](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay-premium-only).
Admins can view all projects pending deletion. If you're an administrator, go to the top navigation bar, click **Projects > Your projects**, and then select the **Removed projects** tab.
From this tab an admin can restore any project.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index ac24e074b96..d8e8ab2fab7 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -35,7 +35,7 @@ Navigate to the webhooks page by going to your project's
**Settings âž” Webhooks**.
NOTE: **Note:**
-On GitLab.com, the [maximum number of webhooks](../../../user/gitlab_com/index.md#maximum-number-of-webhooks) per project, and per group, is limited.
+On GitLab.com, the [maximum number of webhooks and their size](../../../user/gitlab_com/index.md#webhooks) per project, and per group, is limited.
## Version history
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 2753add507e..77875c8faa7 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -237,7 +237,7 @@ This action:
group admins can [configure](../../group/index.md#enabling-delayed-project-removal-premium) projects within a group
to be deleted after a delayed period.
When enabled, actual deletion happens after number of days
-specified in [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
+specified in [instance settings](../../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay-premium-only).
CAUTION: **Warning:**
The default behavior of [Delayed Project deletion](https://gitlab.com/gitlab-org/gitlab/-/issues/32935) in GitLab 12.6 was changed to
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 356e881a0a5..d4b3c58ec56 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -76,9 +76,17 @@ module Gitlab
::Feature.enabled?(:ci_job_entry_matches_all_keys)
end
+ def self.lint_creates_pipeline_with_dry_run?(project)
+ ::Feature.enabled?(:ci_lint_creates_pipeline_with_dry_run, project)
+ end
+
def self.reset_ci_minutes_for_all_namespaces?
::Feature.enabled?(:reset_ci_minutes_for_all_namespaces, default_enabled: false)
end
+
+ def self.expand_names_for_cross_pipeline_artifacts?(project)
+ ::Feature.enabled?(:ci_expand_names_for_cross_pipeline_artifacts, project)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb b/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb
new file mode 100644
index 00000000000..468f3bc4689
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class CancelPendingPipelines < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ return unless project.auto_cancel_pending_pipelines?
+
+ Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
+ cancelables.find_each do |cancelable|
+ cancelable.auto_cancel_running(pipeline)
+ end
+ end
+ end
+
+ def break?
+ false
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def auto_cancelable_pipelines
+ project.ci_pipelines
+ .where(ref: pipeline.ref)
+ .where.not(id: pipeline.same_family_pipeline_ids)
+ .where.not(sha: project.commit(pipeline.ref).try(:id))
+ .alive_or_scheduled
+ .with_only_interruptible_builds
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 74b28b181bc..dbaa6951e64 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
- :chat_data, :allow_mirror_update, :bridge, :content,
+ :chat_data, :allow_mirror_update, :bridge, :content, :dry_run,
# These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds
) do
@@ -22,6 +22,8 @@ module Gitlab
end
end
+ alias_method :dry_run?, :dry_run
+
def branch_exists?
strong_memoize(:is_branch) do
project.repository.branch_exists?(ref)
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb b/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb
index 3dd216b33d1..9954aedc4b7 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content/parameter.rb
@@ -12,7 +12,6 @@ module Gitlab
def content
strong_memoize(:content) do
next unless command.content.present?
- raise UnsupportedSourceError, "#{command.source} not a dangling build" unless command.dangling_build?
command.content
end
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
index aba7dab508d..d7271df1694 100644
--- a/lib/gitlab/ci/pipeline/chain/helpers.rb
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -6,13 +6,13 @@ module Gitlab
module Chain
module Helpers
def error(message, config_error: false, drop_reason: nil)
- if config_error && command.save_incompleted
+ if config_error
drop_reason = :config_error
pipeline.yaml_errors = message
end
pipeline.add_error_message(message)
- pipeline.drop!(drop_reason) if drop_reason
+ pipeline.drop!(drop_reason) if drop_reason && persist_pipeline?
# TODO: consider not to rely on AR errors directly as they can be
# polluted with other unrelated errors (e.g. state machine)
@@ -23,6 +23,10 @@ module Gitlab
def warning(message)
pipeline.add_warning_message(message)
end
+
+ def persist_pipeline?
+ command.save_incompleted && !pipeline.readonly?
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/metrics.rb b/lib/gitlab/ci/pipeline/chain/metrics.rb
new file mode 100644
index 00000000000..0d7449813b4
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/metrics.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Metrics < Chain::Base
+ def perform!
+ counter.increment(source: @pipeline.source)
+ end
+
+ def break?
+ false
+ end
+
+ def counter
+ ::Gitlab::Ci::Pipeline::Metrics.new.pipelines_created_counter
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/pipeline/process.rb b/lib/gitlab/ci/pipeline/chain/pipeline/process.rb
new file mode 100644
index 00000000000..98e9e1479bc
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/pipeline/process.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Pipeline
+ # After pipeline has been successfully created we can start processing it.
+ class Process < Chain::Base
+ def perform!
+ ::Ci::ProcessPipelineService
+ .new(@pipeline)
+ .execute(nil, initial_process: true)
+ end
+
+ def break?
+ false
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb
index 204c7725214..dc648568129 100644
--- a/lib/gitlab/ci/pipeline/chain/sequence.rb
+++ b/lib/gitlab/ci/pipeline/chain/sequence.rb
@@ -9,30 +9,21 @@ module Gitlab
@pipeline = pipeline
@command = command
@sequence = sequence
- @completed = []
@start = Time.now
end
def build!
- @sequence.each do |chain|
- step = chain.new(@pipeline, @command)
+ @sequence.each do |step_class|
+ step = step_class.new(@pipeline, @command)
step.perform!
break if step.break?
-
- @completed.push(step)
end
- @pipeline.tap do
- yield @pipeline, self if block_given?
-
- @command.observe_creation_duration(Time.now - @start)
- @command.observe_pipeline_size(@pipeline)
- end
- end
+ @command.observe_creation_duration(Time.now - @start)
+ @command.observe_pipeline_size(@pipeline)
- def complete?
- @completed.size == @sequence.size
+ @pipeline
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/stop_dry_run.rb b/lib/gitlab/ci/pipeline/chain/stop_dry_run.rb
new file mode 100644
index 00000000000..0e9add4ee74
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/stop_dry_run.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ # During the dry run we don't want to persist the pipeline and skip
+ # all the other steps that operate on a persisted context.
+ # This causes the chain to break at this point.
+ class StopDryRun < Chain::Base
+ def perform!
+ # no-op
+ end
+
+ def break?
+ @command.dry_run?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/metrics.rb b/lib/gitlab/ci/pipeline/metrics.rb
index 649da745eea..ef6981eeb4d 100644
--- a/lib/gitlab/ci/pipeline/metrics.rb
+++ b/lib/gitlab/ci/pipeline/metrics.rb
@@ -36,6 +36,24 @@ module Gitlab
Gitlab::Metrics.counter(name, comment)
end
end
+
+ def pipelines_created_counter
+ strong_memoize(:pipelines_created_count) do
+ name = :pipelines_created_total
+ comment = 'Counter of pipelines created'
+
+ Gitlab::Metrics.counter(name, comment)
+ end
+ end
+
+ def pipelines_created_counter
+ strong_memoize(:pipelines_created_count) do
+ name = :pipelines_created_total
+ comment = 'Counter of pipelines created'
+
+ Gitlab::Metrics.counter(name, comment)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb
index 21f837c58bb..d6681347f42 100644
--- a/lib/gitlab/json.rb
+++ b/lib/gitlab/json.rb
@@ -220,5 +220,33 @@ module Gitlab
end
end
end
+
+ class LimitedEncoder
+ LimitExceeded = Class.new(StandardError)
+
+ # Generates JSON for an object or raise an error if the resulting json string is too big
+ #
+ # @param object [Hash, Array, Object] must be hash, array, or an object that responds to .to_h or .to_json
+ # @param limit [Integer] max size of the resulting json string
+ # @return [String]
+ # @raise [LimitExceeded] if the resulting json string is bigger than the specified limit
+ def self.encode(object, limit: 25.megabytes)
+ return ::Gitlab::Json.dump(object) unless Feature.enabled?(:json_limited_encoder)
+
+ buffer = []
+ buffer_size = 0
+
+ ::Yajl::Encoder.encode(object) do |data_chunk|
+ chunk_size = data_chunk.bytesize
+
+ raise LimitExceeded if buffer_size + chunk_size > limit
+
+ buffer << data_chunk
+ buffer_size += chunk_size
+ end
+
+ buffer.join('')
+ end
+ end
end
end
diff --git a/lib/gitlab/static_site_editor/config.rb b/lib/gitlab/static_site_editor/config.rb
index 08ed6599a6e..d335a434335 100644
--- a/lib/gitlab/static_site_editor/config.rb
+++ b/lib/gitlab/static_site_editor/config.rb
@@ -3,7 +3,7 @@
module Gitlab
module StaticSiteEditor
class Config
- SUPPORTED_EXTENSIONS = %w[.md .md.erb].freeze
+ SUPPORTED_EXTENSIONS = %w[.md].freeze
def initialize(repository, ref, file_path, return_url)
@repository = repository
@@ -42,6 +42,8 @@ module Gitlab
end
def extension_supported?
+ return true if file_path.end_with?('.md.erb') && Feature.enabled?(:sse_erb_support, project)
+
SUPPORTED_EXTENSIONS.any? { |ext| file_path.end_with?(ext) }
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 45709207051..3b30832fa98 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4550,9 +4550,6 @@ msgstr ""
msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
msgstr ""
-msgid "Check your .gitlab-ci.yml"
-msgstr ""
-
msgid "Check your Docker images for known vulnerabilities."
msgstr ""
@@ -7725,7 +7722,7 @@ msgstr ""
msgid "Default classification label"
msgstr ""
-msgid "Default deletion adjourned period"
+msgid "Default deletion delay"
msgstr ""
msgid "Default description template for issues"
@@ -12142,7 +12139,7 @@ msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
-msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day waiting period. This period can be %{customization_link} in instance settings"
+msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{customization_link} in instance settings"
msgstr ""
msgid "GroupSettings|Select a sub-group as the custom project template source for this group."
@@ -22459,6 +22456,9 @@ msgstr ""
msgid "Similar issues"
msgstr ""
+msgid "Simulate a pipeline created for the default branch"
+msgstr ""
+
msgid "Single or combined queries"
msgstr ""
@@ -24470,6 +24470,9 @@ msgstr ""
msgid "There was a problem fetching labels."
msgstr ""
+msgid "There was a problem fetching milestones."
+msgstr ""
+
msgid "There was a problem fetching project branches."
msgstr ""
@@ -26754,6 +26757,9 @@ msgstr ""
msgid "Validate"
msgstr ""
+msgid "Validate your GitLab CI configuration"
+msgstr ""
+
msgid "Validate your GitLab CI configuration file"
msgstr ""
diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb
index d28b8178c99..c650b8e4f90 100644
--- a/qa/qa/page/dashboard/snippet/edit.rb
+++ b/qa/qa/page/dashboard/snippet/edit.rb
@@ -20,8 +20,7 @@ module QA
end
def save_changes
- click_element(:submit_button)
- wait_until { assert_no_element(:submit_button) }
+ click_element(:submit_button, Page::Dashboard::Snippet::Show)
end
private
diff --git a/qa/qa/page/dashboard/snippet/show.rb b/qa/qa/page/dashboard/snippet/show.rb
index 73e6abe174f..576e287d40d 100644
--- a/qa/qa/page/dashboard/snippet/show.rb
+++ b/qa/qa/page/dashboard/snippet/show.rb
@@ -6,6 +6,10 @@ module QA
module Snippet
class Show < Page::Base
include Page::Component::Snippet
+
+ view 'app/assets/javascripts/snippets/components/snippet_title.vue' do
+ element :snippet_title_content, required: true
+ end
end
end
end
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
index eb92385fc83..b3e08292546 100644
--- a/spec/controllers/projects/ci/lints_controller_spec.rb
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -45,6 +45,9 @@ RSpec.describe Projects::Ci::LintsController do
end
describe 'POST #create' do
+ subject { post :create, params: params }
+
+ let(:params) { { namespace_id: project.namespace, project_id: project, content: content } }
let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:remote_file_content) do
@@ -72,18 +75,62 @@ RSpec.describe Projects::Ci::LintsController do
before do
stub_full_request(remote_file_path).to_return(body: remote_file_content)
project.add_developer(user)
+ end
- post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
+ shared_examples 'returns a successful validation' do
+ it 'returns successfully' do
+ subject
+ expect(response).to be_successful
+ end
+
+ it 'render show page' do
+ subject
+ expect(response).to render_template :show
+ end
+
+ it 'retrieves project' do
+ subject
+ expect(assigns(:project)).to eq(project)
+ end
end
- it { expect(response).to be_successful }
+ context 'using legacy validation (YamlProcessor)' do
+ it_behaves_like 'returns a successful validation'
- it 'render show page' do
- expect(response).to render_template :show
+ it 'runs validations through YamlProcessor' do
+ expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original
+
+ subject
+ end
end
- it 'retrieves project' do
- expect(assigns(:project)).to eq(project)
+ context 'using dry_run mode' do
+ subject { post :create, params: params.merge(dry_run: 'true') }
+
+ it_behaves_like 'returns a successful validation'
+
+ it 'runs validations through Ci::CreatePipelineService' do
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, ref: 'master')
+ .and_call_original
+
+ subject
+ end
+
+ context 'when dry_run feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_lint_creates_pipeline_with_dry_run: false)
+ end
+
+ it_behaves_like 'returns a successful validation'
+
+ it 'runs validations through YamlProcessor' do
+ expect(Gitlab::Ci::YamlProcessor).to receive(:new_with_validation_errors).and_call_original
+
+ subject
+ end
+ end
end
end
@@ -98,13 +145,23 @@ RSpec.describe Projects::Ci::LintsController do
before do
project.add_developer(user)
-
- post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
it 'assigns errors' do
+ subject
+
expect(assigns[:errors]).to eq(['root config contains unknown keys: rubocop'])
end
+
+ context 'with dry_run mode' do
+ subject { post :create, params: params.merge(dry_run: 'true') }
+
+ it 'assigns errors' do
+ subject
+
+ expect(assigns[:errors]).to eq(['root config contains unknown keys: rubocop'])
+ end
+ end
end
context 'without enough privileges' do
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index fb7fa92dbb5..e0f288294ad 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
context 'when a user created a merge request in the parent project' do
- let(:merge_request) do
+ let!(:merge_request) do
create(:merge_request,
source_project: project,
target_project: project,
diff --git a/spec/features/projects/ci/lint_spec.rb b/spec/features/projects/ci/lint_spec.rb
index f3845bb8dec..ba063acbe70 100644
--- a/spec/features/projects/ci/lint_spec.rb
+++ b/spec/features/projects/ci/lint_spec.rb
@@ -21,32 +21,48 @@ RSpec.describe 'CI Lint', :js do
end
describe 'YAML parsing' do
- before do
- click_on 'Validate'
- end
+ shared_examples 'validates the YAML' do
+ before do
+ click_on 'Validate'
+ end
- context 'YAML is correct' do
- let(:yaml_content) do
- File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ context 'YAML is correct' do
+ let(:yaml_content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+
+ it 'parses Yaml and displays the jobs' do
+ expect(page).to have_content('Status: syntax is correct')
+
+ within "table" do
+ aggregate_failures do
+ expect(page).to have_content('Job - rspec')
+ expect(page).to have_content('Job - spinach')
+ expect(page).to have_content('Deploy Job - staging')
+ expect(page).to have_content('Deploy Job - production')
+ end
+ end
+ end
end
- it 'parses Yaml' do
- within "table" do
- expect(page).to have_content('Job - rspec')
- expect(page).to have_content('Job - spinach')
- expect(page).to have_content('Deploy Job - staging')
- expect(page).to have_content('Deploy Job - production')
+ context 'YAML is incorrect' do
+ let(:yaml_content) { 'value: cannot have :' }
+
+ it 'displays information about an error' do
+ expect(page).to have_content('Status: syntax is incorrect')
+ expect(page).to have_selector('.ace_content', text: yaml_content)
end
end
end
- context 'YAML is incorrect' do
- let(:yaml_content) { 'value: cannot have :' }
+ it_behaves_like 'validates the YAML'
- it 'displays information about an error' do
- expect(page).to have_content('Status: syntax is incorrect')
- expect(page).to have_selector('.ace_content', text: yaml_content)
+ context 'when Dry Run is checked' do
+ before do
+ check 'Simulate a pipeline created for the default branch'
end
+
+ it_behaves_like 'validates the YAML'
end
describe 'YAML revalidate' do
diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js
index 62e5d57dba2..ab9e325e963 100644
--- a/spec/frontend/blob/utils_spec.js
+++ b/spec/frontend/blob/utils_spec.js
@@ -17,7 +17,11 @@ describe('Blob utilities', () => {
describe('Monaco editor', () => {
it('initializes the Editor Lite', () => {
utils.initEditorLite({ el: editorEl });
- expect(Editor).toHaveBeenCalled();
+ expect(Editor).toHaveBeenCalledWith({
+ scrollbar: {
+ alwaysConsumeMouseWheel: false,
+ },
+ });
});
it.each([[{}], [{ blobPath, blobContent, blobGlobalId }]])(
diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
index 1ea92883e54..b22805f5227 100644
--- a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
describe('Error Tracking Actions', () => {
@@ -20,7 +20,7 @@ describe('Error Tracking Actions', () => {
},
...props,
},
- stubs: { GlDeprecatedButton },
+ stubs: { GlButton },
});
}
@@ -34,7 +34,7 @@ describe('Error Tracking Actions', () => {
}
});
- const findButtons = () => wrapper.findAll(GlDeprecatedButton);
+ const findButtons = () => wrapper.findAll(GlButton);
describe('when error status is unresolved', () => {
it('renders the correct actions buttons to allow ignore and resolve', () => {
diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js
index 64b4461d7b2..1fd728aece4 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -1,5 +1,5 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
@@ -29,14 +29,12 @@ describe('JiraImportApp', () => {
const mountComponent = ({
isJiraConfigured = true,
errorMessage = '',
- selectedProject = 'MTG',
showAlert = false,
isInProgress = false,
loading = false,
mutate = mutateSpy,
- mountFunction = shallowMount,
} = {}) =>
- mountFunction(JiraImportApp, {
+ shallowMount(JiraImportApp, {
propsData: {
inProgressIllustration: 'in-progress-illustration.svg',
isJiraConfigured,
@@ -49,7 +47,6 @@ describe('JiraImportApp', () => {
data() {
return {
isSubmitting: false,
- selectedProject,
userMappings,
errorMessage,
showAlert,
@@ -202,38 +199,6 @@ describe('JiraImportApp', () => {
});
});
- describe('jira import form screen', () => {
- describe('when selected project has been imported before', () => {
- it('shows jira-import::MTG-3 label since project MTG has been imported 2 time before', () => {
- wrapper = mountComponent();
-
- expect(getFormComponent().props('importLabel')).toBe('jira-import::MTG-3');
- });
-
- it('shows warning alert to explain project MTG has been imported 2 times before', () => {
- wrapper = mountComponent({ mountFunction: mount });
-
- expect(getAlert().text()).toBe(
- 'You have imported from this project 2 times before. Each new import will create duplicate issues.',
- );
- });
- });
-
- describe('when selected project has not been imported before', () => {
- beforeEach(() => {
- wrapper = mountComponent({ selectedProject: 'MJP' });
- });
-
- it('shows jira-import::MJP-1 label since project MJP has not been imported before', () => {
- expect(getFormComponent().props('importLabel')).toBe('jira-import::MJP-1');
- });
-
- it('does not show warning alert since project MJP has not been imported before', () => {
- expect(getAlert().exists()).toBe(false);
- });
- });
- });
-
describe('initiating a Jira import', () => {
it('calls the mutation with the expected arguments', () => {
wrapper = mountComponent();
@@ -263,24 +228,22 @@ describe('JiraImportApp', () => {
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
- it('shows alert message with error message on error', () => {
- const mutate = jest.fn(() => Promise.reject());
-
- wrapper = mountComponent({ mutate });
+ describe('when there is an error', () => {
+ beforeEach(() => {
+ const mutate = jest.fn(() => Promise.reject());
+ wrapper = mountComponent({ mutate });
- getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
+ getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
+ });
- // One tick doesn't update the dom to the desired state so we have two ticks here
- return Vue.nextTick()
- .then(Vue.nextTick)
- .then(() => {
- expect(getAlert().text()).toBe('There was an error importing the Jira project.');
- });
+ it('shows alert message with error message', async () => {
+ expect(getAlert().text()).toBe('There was an error importing the Jira project.');
+ });
});
});
describe('alert', () => {
- it('can be dismissed', () => {
+ it('can be dismissed', async () => {
wrapper = mountComponent({
errorMessage: 'There was an error importing the Jira project.',
showAlert: true,
@@ -291,14 +254,14 @@ describe('JiraImportApp', () => {
getAlert().vm.$emit('dismiss');
- return Vue.nextTick().then(() => {
- expect(getAlert().exists()).toBe(false);
- });
+ await Vue.nextTick();
+
+ expect(getAlert().exists()).toBe(false);
});
});
- describe('on mount', () => {
- it('makes a GraphQL mutation call to get user mappings', () => {
+ describe('on mount GraphQL user mapping mutation', () => {
+ it('is called with the expected arguments', () => {
wrapper = mountComponent();
const mutationArguments = {
@@ -313,18 +276,23 @@ describe('JiraImportApp', () => {
expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
});
- it('does not make a GraphQL mutation call to get user mappings when Jira is not configured', () => {
- wrapper = mountComponent({ isJiraConfigured: false });
+ describe('when Jira is not configured', () => {
+ it('is not called', () => {
+ wrapper = mountComponent({ isJiraConfigured: false });
- expect(mutateSpy).not.toHaveBeenCalled();
+ expect(mutateSpy).not.toHaveBeenCalled();
+ });
});
- it('shows error message when there is an error with the GraphQL mutation call', () => {
- const mutate = jest.fn(() => Promise.reject());
-
- wrapper = mountComponent({ mutate });
+ describe('when there is an error when called', () => {
+ beforeEach(() => {
+ const mutate = jest.fn(() => Promise.reject());
+ wrapper = mountComponent({ mutate });
+ });
- expect(getAlert().exists()).toBe(true);
+ it('shows error message', () => {
+ expect(getAlert().exists()).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 42773934945..d1019add8f6 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -1,18 +1,23 @@
-import { GlButton, GlNewDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
+import { GlAlert, GlButton, GlNewDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
-import { issuesPath, jiraProjects, userMappings as defaultUserMappings } from '../mock_data';
+import {
+ imports,
+ issuesPath,
+ jiraProjects,
+ userMappings as defaultUserMappings,
+} from '../mock_data';
describe('JiraImportForm', () => {
let axiosMock;
let wrapper;
const currentUsername = 'mrgitlab';
- const importLabel = 'jira-import::MTG-1';
- const value = 'MTG';
+
+ const getAlert = () => wrapper.find(GlAlert);
const getSelectDropdown = () => wrapper.find(GlFormSelect);
@@ -20,6 +25,8 @@ describe('JiraImportForm', () => {
const getCancelButton = () => wrapper.findAll(GlButton).at(1);
+ const getLabel = () => wrapper.find(GlLabel);
+
const getTable = () => wrapper.find(GlTable);
const getUserDropdown = () => getTable().find(GlNewDropdown);
@@ -28,22 +35,23 @@ describe('JiraImportForm', () => {
const mountComponent = ({
isSubmitting = false,
+ selectedProject = 'MTG',
userMappings = defaultUserMappings,
mountFunction = shallowMount,
} = {}) =>
mountFunction(JiraImportForm, {
propsData: {
- importLabel,
isSubmitting,
issuesPath,
+ jiraImports: imports,
jiraProjects,
projectId: '5',
userMappings,
- value,
},
data: () => ({
isFetching: false,
searchTerm: '',
+ selectedProject,
selectState: null,
users: [],
}),
@@ -60,7 +68,7 @@ describe('JiraImportForm', () => {
wrapper = null;
});
- describe('select dropdown', () => {
+ describe('select dropdown project selection', () => {
it('is shown', () => {
wrapper = mountComponent();
@@ -77,12 +85,34 @@ describe('JiraImportForm', () => {
});
});
- it('emits an "input" event when the input select value changes', () => {
- wrapper = mountComponent();
+ describe('when selected project has been imported before', () => {
+ it('shows jira-import::MTG-3 label since project MTG has been imported 2 time before', () => {
+ wrapper = mountComponent();
+
+ expect(getLabel().props('title')).toBe('jira-import::MTG-3');
+ });
+
+ it('shows warning alert to explain project MTG has been imported 2 times before', () => {
+ wrapper = mountComponent({ mountFunction: mount });
+
+ expect(getAlert().text()).toBe(
+ 'You have imported from this project 2 times before. Each new import will create duplicate issues.',
+ );
+ });
+ });
- getSelectDropdown().vm.$emit('change', value);
+ describe('when selected project has not been imported before', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ selectedProject: 'MJP' });
+ });
+
+ it('shows jira-import::MJP-1 label since project MJP has not been imported before', () => {
+ expect(getLabel().props('title')).toBe('jira-import::MJP-1');
+ });
- expect(wrapper.emitted('input')[0]).toEqual([value]);
+ it('does not show warning alert since project MJP has not been imported before', () => {
+ expect(getAlert().exists()).toBe(false);
+ });
});
});
@@ -91,10 +121,6 @@ describe('JiraImportForm', () => {
wrapper = mountComponent();
});
- it('shows a label which will be applied to imported Jira projects', () => {
- expect(wrapper.find(GlLabel).props('title')).toBe(importLabel);
- });
-
it('shows a heading for the user mapping section', () => {
expect(
getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' }),
@@ -214,11 +240,13 @@ describe('JiraImportForm', () => {
describe('form', () => {
it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
- wrapper = mountComponent();
+ const selectedProject = 'MTG';
+
+ wrapper = mountComponent({ selectedProject });
wrapper.find('form').trigger('submit');
- expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]);
+ expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([selectedProject]);
});
});
});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index d4c3e1905bc..7ef956f8e05 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -32,26 +32,24 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
- <gl-deprecated-dropdown-stub
+ <gl-new-dropdown-stub
+ category="tertiary"
class="flex-grow-1"
data-qa-selector="environments_dropdown"
+ headertext=""
id="monitor-environments-dropdown"
menu-class="monitor-environment-dropdown-menu"
+ size="medium"
text="production"
- toggle-class="dropdown-menu-toggle"
+ toggleclass="dropdown-menu-toggle"
+ variant="default"
>
<div
class="d-flex flex-column overflow-hidden"
>
- <gl-deprecated-dropdown-header-stub
- class="monitor-environment-dropdown-header text-center"
- >
-
- Environment
-
- </gl-deprecated-dropdown-header-stub>
-
- <gl-deprecated-dropdown-divider-stub />
+ <gl-new-dropdown-header-stub>
+ Environment
+ </gl-new-dropdown-header-stub>
<gl-search-box-by-type-stub
class="m-2"
@@ -71,7 +69,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
- </gl-deprecated-dropdown-stub>
+ </gl-new-dropdown-stub>
</div>
<div
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 134c5f32bf7..f44f16107e0 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
-import { GlDeprecatedDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
+import { GlNewDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
@@ -31,7 +31,7 @@ describe('Dashboard header', () => {
const findDashboardDropdown = () => wrapper.find(DashboardsDropdown);
const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
- const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlDeprecatedDropdownItem);
+ const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlNewDropdownItem);
const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType);
const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' });
const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon);
@@ -116,7 +116,7 @@ describe('Dashboard header', () => {
});
it('there are no environments listed', () => {
- expect(findEnvsDropdownItems().length).toBe(0);
+ expect(findEnvsDropdownItems()).toHaveLength(0);
});
});
@@ -145,12 +145,17 @@ describe('Dashboard header', () => {
});
});
- it('renders the environments dropdown with an active element', () => {
- const selectedItems = findEnvsDropdownItems().filter(
- item => item.attributes('active') === 'true',
- );
+ it('environments dropdown items can be checked', () => {
+ const items = findEnvsDropdownItems();
+ const checkItems = findEnvsDropdownItems().filter(item => item.props('isCheckItem'));
- expect(selectedItems.length).toBe(1);
+ expect(items).toHaveLength(checkItems.length);
+ });
+
+ it('checks the currently selected environment', () => {
+ const selectedItems = findEnvsDropdownItems().filter(item => item.props('isChecked'));
+
+ expect(selectedItems).toHaveLength(1);
expect(selectedItems.at(0).text()).toBe(currentEnvironmentName);
});
@@ -160,7 +165,7 @@ describe('Dashboard header', () => {
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick().then(() => {
- expect(findEnvsDropdownItems().length).toBe(resultEnvs.length);
+ expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length);
});
});
@@ -169,7 +174,7 @@ describe('Dashboard header', () => {
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => {
- expect(findEnvsDropdownItems().length).toBe(environmentData.length);
+ expect(findEnvsDropdownItems()).toHaveLength(environmentData.length);
});
});
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index 3c5938cfa1f..ab477292bc1 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -7,7 +8,7 @@ import ActionComponent from '~/pipelines/components/graph/action_component.vue';
describe('pipeline graph action component', () => {
let wrapper;
let mock;
- const findButton = () => wrapper.find('button');
+ const findButton = () => wrapper.find(GlButton);
beforeEach(() => {
mock = new MockAdapter(axios);
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 5f474193495..c803b16f485 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
@@ -171,6 +171,46 @@ describe('FilteredSearchBarRoot', () => {
});
});
+ describe('removeQuotesEnclosure', () => {
+ const mockFilters = [
+ {
+ type: 'author_username',
+ value: {
+ data: 'root',
+ operator: '=',
+ },
+ },
+ {
+ type: 'label_name',
+ value: {
+ data: '"Documentation Update"',
+ operator: '=',
+ },
+ },
+ 'foo',
+ ];
+
+ it('returns filter array with unescaped strings for values which have spaces', () => {
+ expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([
+ {
+ type: 'author_username',
+ value: {
+ data: 'root',
+ operator: '=',
+ },
+ },
+ {
+ type: 'label_name',
+ value: {
+ data: 'Documentation Update',
+ operator: '=',
+ },
+ },
+ 'foo',
+ ]);
+ });
+ });
+
describe('handleSortOptionClick', () => {
it('emits component event `onSort` with selected sort by value', () => {
wrapper.vm.handleSortOptionClick(mockSortOptions[1]);
@@ -204,9 +244,12 @@ describe('FilteredSearchBarRoot', () => {
describe('handleHistoryItemSelected', () => {
it('emits `onFilter` event with provided filters param', () => {
+ jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
+
wrapper.vm.handleHistoryItemSelected(mockHistoryItems[0]);
expect(wrapper.emitted('onFilter')[0]).toEqual([mockHistoryItems[0]]);
+ expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockHistoryItems[0]);
});
});
@@ -274,9 +317,12 @@ describe('FilteredSearchBarRoot', () => {
});
it('emits component event `onFilter` with provided filters param', () => {
+ jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
+
wrapper.vm.handleFilterSubmit(mockFilters);
expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]);
+ expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockFilters);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
new file mode 100644
index 00000000000..a857f84adf1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -0,0 +1,19 @@
+import * as filteredSearchUtils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+
+describe('Filtered Search Utils', () => {
+ describe('stripQuotes', () => {
+ it.each`
+ inputValue | outputValue
+ ${'"Foo Bar"'} | ${'Foo Bar'}
+ ${"'Foo Bar'"} | ${'Foo Bar'}
+ ${'FooBar'} | ${'FooBar'}
+ ${"Foo'Bar"} | ${"Foo'Bar"}
+ ${'Foo"Bar'} | ${'Foo"Bar'}
+ `(
+ 'returns string $outputValue when called with string $inputValue',
+ ({ inputValue, outputValue }) => {
+ expect(filteredSearchUtils.stripQuotes(inputValue)).toBe(outputValue);
+ },
+ );
+ });
+});
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 ff834c0e9d1..b7dbce8c2ea 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
@@ -1,6 +1,7 @@
import Api from '~/api';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
@@ -33,6 +34,28 @@ export const mockAuthor3 = {
export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
+export const mockRegularMilestone = {
+ id: 1,
+ name: '4.0',
+ title: '4.0',
+};
+
+export const mockEscapedMilestone = {
+ id: 3,
+ name: '5.0 RC1',
+ title: '5.0 RC1',
+};
+
+export const mockMilestones = [
+ {
+ id: 2,
+ name: '5.0',
+ title: '5.0',
+ },
+ mockRegularMilestone,
+ mockEscapedMilestone,
+];
+
export const mockAuthorToken = {
type: 'author_username',
icon: 'user',
@@ -56,6 +79,17 @@ export const mockLabelToken = {
fetchLabels: () => Promise.resolve(mockLabels),
};
+export const mockMilestoneToken = {
+ type: 'milestone_title',
+ icon: 'clock',
+ title: 'Milestone',
+ unique: true,
+ symbol: '%',
+ token: MilestoneToken,
+ operators: [{ value: '=', description: 'is', default: 'true' }],
+ fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
+};
+
export const mockAvailableTokens = [mockAuthorToken, mockLabelToken];
export const mockHistoryItems = [
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
new file mode 100644
index 00000000000..de893bf44c8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -0,0 +1,152 @@
+import { mount } from '@vue/test-utils';
+import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+
+import createFlash from '~/flash';
+import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+
+import {
+ mockMilestoneToken,
+ mockMilestones,
+ mockRegularMilestone,
+ mockEscapedMilestone,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+const createComponent = ({
+ config = mockMilestoneToken,
+ value = { data: '' },
+ active = false,
+} = {}) =>
+ mount(MilestoneToken, {
+ propsData: {
+ config,
+ value,
+ active,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ },
+ stubs: {
+ Portal: {
+ template: '<div><slot></slot></div>',
+ },
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+ },
+ });
+
+describe('MilestoneToken', () => {
+ let mock;
+ let wrapper;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ beforeEach(async () => {
+ // Milestone title with spaces is always enclosed in quotations by component.
+ wrapper = createComponent({ value: { data: `"${mockEscapedMilestone.title}"` } });
+
+ wrapper.setData({
+ milestones: mockMilestones,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ describe('currentValue', () => {
+ it('returns lowercase string for `value.data`', () => {
+ expect(wrapper.vm.currentValue).toBe('"5.0 rc1"');
+ });
+ });
+
+ describe('activeMilestone', () => {
+ it('returns object for currently present `value.data`', () => {
+ expect(wrapper.vm.activeMilestone).toEqual(mockEscapedMilestone);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('fetchMilestoneBySearchTerm', () => {
+ it('calls `config.fetchMilestones` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones');
+
+ wrapper.vm.fetchMilestoneBySearchTerm('foo');
+
+ expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo');
+ });
+
+ it('sets response to `milestones` when request is successful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
+ data: mockMilestones,
+ });
+
+ wrapper.vm.fetchMilestoneBySearchTerm();
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.milestones).toEqual(mockMilestones);
+ });
+ });
+
+ it('calls `createFlash` with flash error message when request fails', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
+
+ wrapper.vm.fetchMilestoneBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalledWith('There was a problem fetching milestones.');
+ });
+ });
+
+ it('sets `loading` to false when request completes', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
+
+ wrapper.vm.fetchMilestoneBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.loading).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
+
+ wrapper.setData({
+ milestones: mockMilestones,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('renders gl-filtered-search-token component', () => {
+ expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ });
+
+ it('renders token item when value is selected', () => {
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"'
+ expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1"
+ });
+ });
+});
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
new file mode 100644
index 00000000000..89b9907d0c2
--- /dev/null
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelinesHelper do
+ include Devise::Test::ControllerHelpers
+
+ describe 'pipeline_warnings' do
+ let(:pipeline) { double(:pipeline, warning_messages: warning_messages) }
+
+ subject { helper.pipeline_warnings(pipeline) }
+
+ context 'when pipeline has no warnings' do
+ let(:warning_messages) { [] }
+
+ it 'is empty' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when pipeline has warnings' do
+ let(:warning_messages) { [double(content: 'Warning 1'), double(content: 'Warning 2')] }
+
+ it 'returns a warning callout box' do
+ expect(subject).to have_css 'div.alert-warning'
+ expect(subject).to include 'Warning:'
+ end
+
+ it 'lists the the warnings' do
+ expect(subject).to include 'Warning 1'
+ expect(subject).to include 'Warning 2'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index 5d20b1b8fda..cc4aaffb0a4 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -23,9 +23,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do
end
it 'does not process the second step' do
- subject.build! do |pipeline, sequence|
- expect(sequence).not_to be_complete
- end
+ subject.build!
expect(second_step).not_to have_received(:perform!)
end
@@ -43,9 +41,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do
end
it 'iterates through entire sequence' do
- subject.build! do |pipeline, sequence|
- expect(sequence).to be_complete
- end
+ subject.build!
expect(first_step).to have_received(:perform!)
expect(second_step).to have_received(:perform!)
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
index 931c62701ce..de580d2e148 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
@@ -41,9 +41,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
)
end
+ let(:save_incompleted) { true }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
- project: project, current_user: user, config_processor: yaml_processor
+ project: project, current_user: user, config_processor: yaml_processor, save_incompleted: save_incompleted
)
end
@@ -84,6 +85,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
perform!
expect(pipeline.status).to eq('failed')
+ expect(pipeline).to be_persisted
expect(pipeline.errors.to_a).to include('External validation failed')
end
@@ -98,6 +100,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
perform!
end
+
+ context 'when save_incompleted is false' do
+ let(:save_incompleted) { false}
+
+ it 'adds errors to the pipeline without dropping it' do
+ perform!
+
+ expect(pipeline.status).to eq('pending')
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors.to_a).to include('External validation failed')
+ end
+
+ it 'breaks the chain' do
+ perform!
+
+ expect(step.break?).to be true
+ end
+
+ it 'logs the authorization' do
+ expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline not authorized', project_id: project.id, user_id: user.id)
+
+ perform!
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb
index d7671dda323..0402296a3a8 100644
--- a/spec/lib/gitlab/json_spec.rb
+++ b/spec/lib/gitlab/json_spec.rb
@@ -407,4 +407,36 @@ RSpec.describe Gitlab::Json do
end
end
end
+
+ describe Gitlab::Json::LimitedEncoder do
+ subject { described_class.encode(obj, limit: 8.kilobytes) }
+
+ context 'when object size is acceptable' do
+ let(:obj) { { test: true } }
+
+ it 'returns json string' do
+ is_expected.to eq("{\"test\":true}")
+ end
+ end
+
+ context 'when object is too big' do
+ let(:obj) { [{ test: true }] * 1000 }
+
+ it 'raises LimitExceeded error' do
+ expect { subject }.to raise_error(
+ Gitlab::Json::LimitedEncoder::LimitExceeded
+ )
+ end
+ end
+
+ context 'when json_limited_encoder is disabled' do
+ let(:obj) { [{ test: true }] * 1000 }
+
+ it 'does not raise an error' do
+ stub_feature_flags(json_limited_encoder: false)
+
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/static_site_editor/config_spec.rb b/spec/lib/gitlab/static_site_editor/config_spec.rb
index b60a6a9b006..56cdb573785 100644
--- a/spec/lib/gitlab/static_site_editor/config_spec.rb
+++ b/spec/lib/gitlab/static_site_editor/config_spec.rb
@@ -46,8 +46,6 @@ RSpec.describe Gitlab::StaticSiteEditor::Config do
end
context 'when file has .md.erb extension' do
- let(:file_path) { 'README.md.erb' }
-
before do
repository.create_file(
project.creator,
@@ -58,7 +56,25 @@ RSpec.describe Gitlab::StaticSiteEditor::Config do
)
end
- it { is_expected.to include(is_supported_content: 'true') }
+ context 'when feature flag is enabled' do
+ let(:file_path) { 'FEATURE_ON.md.erb' }
+
+ before do
+ stub_feature_flags(sse_erb_support: project)
+ end
+
+ it { is_expected.to include(is_supported_content: 'true') }
+ end
+
+ context 'when feature flag is disabled' do
+ let(:file_path) { 'FEATURE_OFF.md.erb' }
+
+ before do
+ stub_feature_flags(sse_erb_support: false)
+ end
+
+ it { is_expected.to include(is_supported_content: 'false') }
+ end
end
context 'when file path is nested' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 9e63b524d69..069ac23c5a4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3111,6 +3111,14 @@ RSpec.describe Ci::Build do
end
end
+ describe '#simple_variables_without_dependencies' do
+ it 'does not load dependencies' do
+ expect(build).not_to receive(:dependency_variables)
+
+ build.simple_variables_without_dependencies
+ end
+ end
+
shared_examples "secret CI variables" do
context 'when ref is branch' do
let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
diff --git a/spec/services/ci/create_pipeline_service/dry_run_spec.rb b/spec/services/ci/create_pipeline_service/dry_run_spec.rb
new file mode 100644
index 00000000000..93378df80f0
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/dry_run_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:admin) }
+ let(:ref) { 'refs/heads/master' }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+
+ subject { service.execute(:push, dry_run: true) }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ describe 'dry run' do
+ shared_examples 'returns a non persisted pipeline' do
+ it 'does not persist the pipeline' do
+ expect(subject).not_to be_persisted
+ expect(subject.id).to be_nil
+ end
+
+ it 'does not process the pipeline' do
+ expect(Ci::ProcessPipelineService).not_to receive(:new)
+
+ subject
+ end
+
+ it 'does not schedule merge request head pipeline update' do
+ expect(service).not_to receive(:schedule_head_pipeline_update)
+
+ subject
+ end
+ end
+
+ context 'when pipeline is valid' do
+ let(:config) { gitlab_ci_yaml }
+
+ it_behaves_like 'returns a non persisted pipeline'
+
+ it 'returns a valid pipeline' do
+ expect(subject.error_messages).to be_empty
+ expect(subject.yaml_errors).to be_nil
+ expect(subject.errors).to be_empty
+ end
+ end
+
+ context 'when pipeline is not valid' do
+ context 'when there are syntax errors' do
+ let(:config) do
+ <<~YAML
+ rspec:
+ script: echo
+ something: wrong
+ YAML
+ end
+
+ it_behaves_like 'returns a non persisted pipeline'
+
+ it 'returns a pipeline with errors', :aggregate_failures do
+ error_message = 'jobs:rspec config contains unknown keys: something'
+
+ expect(subject.error_messages.map(&:content)).to eq([error_message])
+ expect(subject.errors).not_to be_empty
+ expect(subject.yaml_errors).to eq(error_message)
+ end
+ end
+
+ context 'when there are logical errors' do
+ let(:config) do
+ <<~YAML
+ build:
+ script: echo
+ stage: build
+ needs: [test]
+ test:
+ script: echo
+ stage: test
+ YAML
+ end
+
+ it_behaves_like 'returns a non persisted pipeline'
+
+ it 'returns a pipeline with errors', :aggregate_failures do
+ error_message = 'build job: need test is not defined in prior stages'
+
+ expect(subject.error_messages.map(&:content)).to eq([error_message])
+ expect(subject.errors).not_to be_empty
+ end
+ end
+
+ context 'when there are errors at the seeding stage' do
+ let(:config) do
+ <<~YAML
+ build:
+ stage: build
+ script: echo
+ rules:
+ - if: '$CI_MERGE_REQUEST_ID'
+ test:
+ stage: test
+ script: echo
+ needs: ['build']
+ YAML
+ end
+
+ it_behaves_like 'returns a non persisted pipeline'
+
+ it 'returns a pipeline with errors', :aggregate_failures do
+ error_message = "test: needs 'build'"
+
+ expect(subject.error_messages.map(&:content)).to eq([error_message])
+ expect(subject.errors).not_to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
index 5157574ea04..f656ad52ac8 100644
--- a/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
+++ b/spec/services/ci/create_pipeline_service/parameter_content_spec.rb
@@ -49,14 +49,5 @@ RSpec.describe Ci::CreatePipelineService do
end
end
end
-
- context 'when source is not a dangling build' do
- subject { service.execute(:web, content: content) }
-
- it 'raises an exception' do
- klass = Gitlab::Ci::Pipeline::Chain::Config::Content::Parameter::UnsupportedSourceError
- expect { subject }.to raise_error(klass)
- end
- end
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 251020828e3..db4c2f5a047 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1692,16 +1692,23 @@ RSpec.describe Ci::CreatePipelineService do
context 'when pipeline on feature is created' do
let(:ref_name) { 'refs/heads/feature' }
+ shared_examples 'has errors' do
+ it 'contains the expected errors' do
+ expect(pipeline.builds).to be_empty
+ expect(pipeline.yaml_errors).to eq("test_a: needs 'build_a'")
+ expect(pipeline.error_messages.map(&:content)).to contain_exactly("test_a: needs 'build_a'")
+ expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'")
+ end
+ end
+
context 'when save_on_errors is enabled' do
let(:pipeline) { execute_service(save_on_errors: true) }
it 'does create a pipeline as test_a depends on build_a' do
expect(pipeline).to be_persisted
- expect(pipeline.builds).to be_empty
- expect(pipeline.yaml_errors).to eq("test_a: needs 'build_a'")
- expect(pipeline.messages.pluck(:content)).to contain_exactly("test_a: needs 'build_a'")
- expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'")
end
+
+ it_behaves_like 'has errors'
end
context 'when save_on_errors is disabled' do
@@ -1709,11 +1716,9 @@ RSpec.describe Ci::CreatePipelineService do
it 'does not create a pipeline as test_a depends on build_a' do
expect(pipeline).not_to be_persisted
- expect(pipeline.builds).to be_empty
- expect(pipeline.yaml_errors).to be_nil
- expect(pipeline.messages).not_to be_empty
- expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'")
end
+
+ it_behaves_like 'has errors'
end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 02d36ed5aba..b7b81d33c3e 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -130,6 +130,14 @@ RSpec.describe WebHookService do
end
end
+ context 'when request body size is too big' do
+ it 'does not perform the request' do
+ stub_const("#{described_class}::REQUEST_BODY_SIZE_LIMIT", 10.bytes)
+
+ expect(service_instance.execute).to eq({ status: :error, message: "Gitlab::Json::LimitedEncoder::LimitExceeded" })
+ end
+ end
+
it 'handles 200 status code' do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
diff --git a/spec/views/projects/ci/lints/show.html.haml_spec.rb b/spec/views/projects/ci/lints/show.html.haml_spec.rb
index bcfb952ca66..a71cea6d3c8 100644
--- a/spec/views/projects/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/projects/ci/lints/show.html.haml_spec.rb
@@ -82,6 +82,20 @@ RSpec.describe 'projects/ci/lints/show' do
expect(rendered).to have_content('Environment: testing')
expect(rendered).to have_content('When: on_success')
end
+
+ context 'when content has warnings' do
+ before do
+ assign(:warnings, ['Warning 1', 'Warning 2'])
+ end
+
+ it 'shows warning messages' do
+ render
+
+ expect(rendered).to have_content('Warning:')
+ expect(rendered).to have_content('Warning 1')
+ expect(rendered).to have_content('Warning 2')
+ end
+ end
end
context 'when the content is invalid' do
@@ -89,6 +103,7 @@ RSpec.describe 'projects/ci/lints/show' do
assign(:project, project)
assign(:status, false)
assign(:errors, ['Undefined error'])
+ assign(:warnings, ['Warning 1', 'Warning 2'])
end
it 'shows error message' do
@@ -98,5 +113,13 @@ RSpec.describe 'projects/ci/lints/show' do
expect(rendered).to have_content('Undefined error')
expect(rendered).not_to have_content('Tag list:')
end
+
+ it 'shows warning messages' do
+ render
+
+ expect(rendered).to have_content('Warning:')
+ expect(rendered).to have_content('Warning 1')
+ expect(rendered).to have_content('Warning 2')
+ end
end
end
diff --git a/spec/views/projects/pipelines/new.html.haml_spec.rb b/spec/views/projects/pipelines/new.html.haml_spec.rb
new file mode 100644
index 00000000000..2deacfa8478
--- /dev/null
+++ b/spec/views/projects/pipelines/new.html.haml_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/pipelines/new' do
+ include Devise::Test::ControllerHelpers
+ let_it_be(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+
+ stub_feature_flags(new_pipeline_form: false)
+ end
+
+ describe 'warning messages' do
+ let(:warning_messages) do
+ [double(content: 'warning 1'), double(content: 'warning 2')]
+ end
+
+ before do
+ allow(pipeline).to receive(:warning_messages).and_return(warning_messages)
+ end
+
+ it 'displays the warnings' do
+ render
+
+ expect(rendered).to have_css('div.alert-warning')
+ expect(rendered).to have_content('warning 1')
+ expect(rendered).to have_content('warning 2')
+ end
+ end
+end
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
new file mode 100644
index 00000000000..49add434ab5
--- /dev/null
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'projects/pipelines/show' do
+ include Devise::Test::ControllerHelpers
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:presented_pipeline) { pipeline.present(current_user: user) }
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, presented_pipeline)
+
+ stub_feature_flags(new_pipeline_form: false)
+ end
+
+ shared_examples 'pipeline with warning messages' do
+ let(:warning_messages) do
+ [double(content: 'warning 1'), double(content: 'warning 2')]
+ end
+
+ before do
+ allow(pipeline).to receive(:warning_messages).and_return(warning_messages)
+ end
+
+ it 'displays the warnings' do
+ render
+
+ expect(rendered).to have_css('.bs-callout-warning')
+ expect(rendered).to have_content('warning 1')
+ expect(rendered).to have_content('warning 2')
+ end
+ end
+
+ context 'when pipeline has errors' do
+ before do
+ allow(pipeline).to receive(:yaml_errors).and_return('some errors')
+ end
+
+ it 'shows errors' do
+ render
+
+ expect(rendered).to have_content('Found errors in your .gitlab-ci.yml')
+ expect(rendered).to have_content('some errors')
+ end
+
+ it 'does not render the pipeline tabs' do
+ render
+
+ expect(rendered).not_to have_css('ul.pipelines-tabs')
+ end
+
+ context 'when pipeline has also warnings' do
+ it_behaves_like 'pipeline with warning messages'
+ end
+ end
+
+ context 'when pipeline is valid' do
+ it 'does not show errors' do
+ render
+
+ expect(rendered).not_to have_content('Found errors in your .gitlab-ci.yml')
+ end
+
+ it 'renders the pipeline tabs' do
+ render
+
+ expect(rendered).to have_css('ul.pipelines-tabs')
+ end
+
+ context 'when pipeline has warnings' do
+ it_behaves_like 'pipeline with warning messages'
+ end
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 39ed862af82..223f5aea813 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -7,7 +7,7 @@ require 'spec_helper'
RSpec.describe GitGarbageCollectWorker do
include GitHelpers
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
let!(:lease_uuid) { SecureRandom.uuid }
let!(:lease_key) { "project_housekeeping:#{project.id}" }
@@ -114,6 +114,56 @@ RSpec.describe GitGarbageCollectWorker do
subject.perform(*params)
end
end
+
+ context 'LFS object garbage collection' do
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ let_it_be(:lfs_reference) { create(:lfs_objects_project, project: project) }
+ let(:lfs_object) { lfs_reference.lfs_object }
+
+ context 'with cleanup_lfs_during_gc feature flag enabled' do
+ before do
+ stub_feature_flags(cleanup_lfs_during_gc: true)
+ end
+
+ it 'cleans up unreferenced LFS objects' do
+ expect_next_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences) do |svc|
+ expect(svc.project).to eq(project)
+ expect(svc.dry_run).to be_falsy
+ expect(svc).to receive(:run!).and_call_original
+ end
+
+ subject.perform(*params)
+
+ expect(project.lfs_objects.reload).not_to include(lfs_object)
+ end
+
+ it 'does nothing if the database is read-only' do
+ expect(Gitlab::Database).to receive(:read_only?) { true }
+ expect_any_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences).not_to receive(:run!)
+
+ subject.perform(*params)
+
+ expect(project.lfs_objects.reload).to include(lfs_object)
+ end
+ end
+
+ context 'with cleanup_lfs_during_gc feature flag disabled' do
+ before do
+ stub_feature_flags(cleanup_lfs_during_gc: false)
+ end
+
+ it 'does not clean up unreferenced LFS objects' do
+ expect_any_instance_of(Gitlab::Cleanup::OrphanLfsFileReferences).not_to receive(:run!)
+
+ subject.perform(*params)
+
+ expect(project.lfs_objects.reload).to include(lfs_object)
+ end
+ end
+ end
end
context 'when no lease can be obtained' do