summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.markdownlint.json1
-rw-r--r--app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql2
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue32
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue46
-rw-r--r--app/assets/javascripts/ide/lib/errors.js39
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js28
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js1
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_create_root.vue44
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_form.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/alert_details_table.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue8
-rw-r--r--app/assets/stylesheets/pages/alert_management/details.scss44
-rw-r--r--app/uploaders/object_storage.rb32
-rw-r--r--app/views/admin/dev_ops_report/_no_data.html.haml4
-rw-r--r--app/workers/analytics/instance_statistics/count_job_trigger_worker.rb2
-rw-r--r--changelogs/unreleased/212595-ide-commit-errors.yml5
-rw-r--r--changelogs/unreleased/229214-replace-LoadingButton-with-GlButton.yml5
-rw-r--r--changelogs/unreleased/enable-store-instance-statistics-ff-by-default.yml5
-rw-r--r--changelogs/unreleased/mjang-devops-score-ui-text.yml5
-rw-r--r--config/feature_flags/development/ci_new_artifact_file_reader.yml7
-rw-r--r--config/feature_flags/development/store_instance_statistics_measurements.yml2
-rw-r--r--doc/.vale/gitlab/Acronyms.yml1
-rw-r--r--doc/administration/geo/disaster_recovery/index.md47
-rw-r--r--doc/administration/geo/disaster_recovery/planned_failover.md6
-rw-r--r--doc/administration/geo/replication/troubleshooting.md31
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md2
-rw-r--r--doc/ci/pipelines/img/ci_efficiency_pipeline_dag_critical_path.pngbin0 -> 124100 bytes
-rw-r--r--doc/ci/pipelines/img/ci_efficiency_pipeline_health_grafana_dashboard.pngbin0 -> 241128 bytes
-rw-r--r--doc/ci/pipelines/pipeline_efficiency.md251
-rw-r--r--doc/development/contributing/index.md5
-rw-r--r--doc/user/search/advanced_search_syntax.md32
-rw-r--r--lib/gitlab/ci/artifact_file_reader.rb25
-rw-r--r--lib/gitlab/ci/features.rb4
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb6
-rw-r--r--locale/gitlab.pot29
-rw-r--r--scripts/utils.sh4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js53
-rw-r--r--spec/frontend/ide/lib/errors_spec.js70
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js22
-rw-r--r--spec/frontend/ide/stores/modules/commit/mutations_spec.js21
-rw-r--r--spec/frontend/issuable_create/components/issuable_create_root_spec.js64
-rw-r--r--spec/frontend/issuable_create/components/issuable_form_spec.js118
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js27
-rw-r--r--spec/lib/gitlab/ci/artifact_file_reader_spec.rb11
-rw-r--r--spec/uploaders/object_storage_spec.rb36
47 files changed, 1146 insertions, 197 deletions
diff --git a/.markdownlint.json b/.markdownlint.json
index 88273682d3a..5d81905d056 100644
--- a/.markdownlint.json
+++ b/.markdownlint.json
@@ -45,6 +45,7 @@
"Debian",
"DevOps",
"Docker",
+ "DockerSlim",
"Elasticsearch",
"Facebook",
"fastlane",
diff --git a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
index 1b5f9564e59..ff6aa597f48 100644
--- a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
+++ b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql
@@ -1,4 +1,4 @@
-#import "./issue.fragment.graphql"
+#import "ee_else_ce/boards/queries/issue.fragment.graphql"
mutation IssueMoveList(
$projectPath: ID!
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index af03e39b685..c157b04b4f5 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
-import { GlModal, GlButton, GlDeprecatedButton, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import SplitButton from '~/vue_shared/components/split_button.vue';
import { s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
@@ -29,7 +29,6 @@ export default {
SplitButton,
GlModal,
GlButton,
- GlDeprecatedButton,
GlFormInput,
GlSprintf,
},
@@ -175,24 +174,31 @@ export default {
}}</span>
</template>
<template #modal-footer>
- <gl-deprecated-button variant="secondary" @click="handleCancel">{{
- s__('Cancel')
- }}</gl-deprecated-button>
+ <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button>
<template v-if="confirmCleanup">
- <gl-deprecated-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{
- s__('ClusterIntegration|Remove integration')
- }}</gl-deprecated-button>
- <gl-deprecated-button
+ <gl-button
+ :disabled="!canSubmit"
+ variant="warning"
+ category="primary"
+ @click="handleSubmit"
+ >{{ s__('ClusterIntegration|Remove integration') }}</gl-button
+ >
+ <gl-button
:disabled="!canSubmit"
variant="danger"
+ category="primary"
@click="handleSubmit(true)"
- >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-deprecated-button
+ >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-button
>
</template>
<template v-else>
- <gl-deprecated-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{
- s__('ClusterIntegration|Remove integration')
- }}</gl-deprecated-button>
+ <gl-button
+ :disabled="!canSubmit"
+ variant="danger"
+ category="primary"
+ @click="handleSubmit"
+ >{{ s__('ClusterIntegration|Remove integration') }}</gl-button
+ >
</template>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 9342ab87c1a..73c56514fce 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui';
import { n__, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
@@ -8,6 +8,7 @@ import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import consts from '../../stores/modules/commit/constants';
+import { createUnexpectedCommitError } from '../../lib/errors';
export default {
components: {
@@ -17,15 +18,20 @@ export default {
SuccessMessage,
GlModal,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
data() {
return {
isCompact: true,
componentHeight: null,
+ // Keep track of "lastCommitError" so we hold onto the value even when "commitError" is cleared.
+ lastCommitError: createUnexpectedCommitError(),
};
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
- ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']),
...mapGetters(['someUncommittedChanges']),
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
@@ -38,11 +44,28 @@ export default {
currentViewIsCommitView() {
return this.currentActivityView === leftSidebarViews.commit.name;
},
+ commitErrorPrimaryAction() {
+ if (!this.lastCommitError?.canCreateBranch) {
+ return undefined;
+ }
+
+ return {
+ text: __('Create new branch'),
+ };
+ },
},
watch: {
currentActivityView: 'handleCompactState',
someUncommittedChanges: 'handleCompactState',
lastCommitMsg: 'handleCompactState',
+ commitError(val) {
+ if (!val) {
+ return;
+ }
+
+ this.lastCommitError = val;
+ this.$refs.commitErrorModal.show();
+ },
},
methods: {
...mapActions(['updateActivityBarView']),
@@ -53,9 +76,7 @@ export default {
'updateCommitAction',
]),
commit() {
- return this.commitChanges().catch(() => {
- this.$refs.createBranchModal.show();
- });
+ return this.commitChanges();
},
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit());
@@ -164,17 +185,14 @@ export default {
</button>
</div>
<gl-modal
- ref="createBranchModal"
- modal-id="ide-create-branch-modal"
- :ok-title="__('Create new branch')"
- :title="__('Branch has changed')"
- ok-variant="success"
+ ref="commitErrorModal"
+ modal-id="ide-commit-error-modal"
+ :title="lastCommitError.title"
+ :action-primary="commitErrorPrimaryAction"
+ :action-cancel="{ text: __('Cancel') }"
@ok="forceCreateNewBranch"
>
- {{
- __(`This branch has changed since you started editing.
- Would you like to create a new branch?`)
- }}
+ <div v-safe-html="lastCommitError.messageHTML"></div>
</gl-modal>
</form>
</transition>
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
new file mode 100644
index 00000000000..6ae18bc8180
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -0,0 +1,39 @@
+import { escape } from 'lodash';
+import { __ } from '~/locale';
+
+const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
+const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
+
+export const createUnexpectedCommitError = () => ({
+ title: __('Unexpected error'),
+ messageHTML: __('Could not commit. An unexpected error occurred.'),
+ canCreateBranch: false,
+});
+
+export const createCodeownersCommitError = message => ({
+ title: __('CODEOWNERS rule violation'),
+ messageHTML: escape(message),
+ canCreateBranch: true,
+});
+
+export const createBranchChangedCommitError = message => ({
+ title: __('Branch changed'),
+ messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`,
+ canCreateBranch: true,
+});
+
+export const parseCommitError = e => {
+ const { message } = e?.response?.data || {};
+
+ if (!message) {
+ return createUnexpectedCommitError();
+ }
+
+ if (CODEOWNERS_REGEX.test(message)) {
+ return createCodeownersCommitError(message);
+ } else if (BRANCH_CHANGED_REGEX.test(message)) {
+ return createBranchChangedCommitError(message);
+ }
+
+ return createUnexpectedCommitError();
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 277e6923f17..90a6c644d17 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,6 +1,5 @@
import { sprintf, __ } from '~/locale';
import { deprecatedCreateFlash as flash } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import service from '../../../services';
@@ -8,6 +7,7 @@ import * as types from './mutation_types';
import consts from './constants';
import { leftSidebarViews } from '../../../constants';
import eventHub from '../../../eventhub';
+import { parseCommitError } from '../../../lib/errors';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
@@ -113,6 +113,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
? Promise.resolve()
: dispatch('stageAllChanges', null, { root: true });
+ commit(types.CLEAR_ERROR);
commit(types.UPDATE_LOADING, true);
return stageFilesPromise
@@ -128,6 +129,12 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return service.commit(rootState.currentProjectId, payload);
})
+ .catch(e => {
+ commit(types.UPDATE_LOADING, false);
+ commit(types.SET_ERROR, parseCommitError(e));
+
+ throw e;
+ })
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
@@ -214,24 +221,5 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
{ root: true },
),
);
- })
- .catch(err => {
- commit(types.UPDATE_LOADING, false);
-
- // don't catch bad request errors, let the view handle them
- if (err.response.status === httpStatusCodes.BAD_REQUEST) throw err;
-
- dispatch(
- 'setErrorMessage',
- {
- text: __('An error occurred while committing your changes.'),
- action: () =>
- dispatch('commitChanges').then(() => dispatch('setErrorMessage', null, { root: true })),
- actionText: __('Please try again'),
- },
- { root: true },
- );
-
- window.dispatchEvent(new Event('resize'));
});
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
index 7ad8f3570b7..47ec2ffbdde 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -3,3 +3,6 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
+
+export const CLEAR_ERROR = 'CLEAR_ERROR';
+export const SET_ERROR = 'SET_ERROR';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index 73b618e250f..2cf6e8e6f36 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -24,4 +24,10 @@ export default {
shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR,
});
},
+ [types.CLEAR_ERROR](state) {
+ state.commitError = null;
+ },
+ [types.SET_ERROR](state, error) {
+ state.commitError = error;
+ },
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
index f49737485f2..de092a569ad 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/state.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -4,4 +4,5 @@ export default () => ({
newBranchName: '',
submitCommitLoading: false,
shouldCreateMR: true,
+ commitError: null,
});
diff --git a/app/assets/javascripts/issuable_create/components/issuable_create_root.vue b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue
new file mode 100644
index 00000000000..1ef42976032
--- /dev/null
+++ b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue
@@ -0,0 +1,44 @@
+<script>
+import IssuableForm from './issuable_form.vue';
+
+export default {
+ components: {
+ IssuableForm,
+ },
+ props: {
+ descriptionPreviewPath: {
+ type: String,
+ required: true,
+ },
+ descriptionHelpPath: {
+ type: String,
+ required: true,
+ },
+ labelsFetchPath: {
+ type: String,
+ required: true,
+ },
+ labelsManagePath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-create-container">
+ <slot name="title"></slot>
+ <hr />
+ <issuable-form
+ :description-preview-path="descriptionPreviewPath"
+ :description-help-path="descriptionHelpPath"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ >
+ <template #actions="issuableMeta">
+ <slot name="actions" v-bind="issuableMeta"></slot>
+ </template>
+ </issuable-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue
new file mode 100644
index 00000000000..eac4050b53d
--- /dev/null
+++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue
@@ -0,0 +1,122 @@
+<script>
+import { GlForm, GlFormInput } from '@gitlab/ui';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+
+import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
+
+export default {
+ LabelSelectVariant: DropdownVariant,
+ components: {
+ GlForm,
+ GlFormInput,
+ MarkdownField,
+ LabelsSelect,
+ },
+ props: {
+ descriptionPreviewPath: {
+ type: String,
+ required: true,
+ },
+ descriptionHelpPath: {
+ type: String,
+ required: true,
+ },
+ labelsFetchPath: {
+ type: String,
+ required: true,
+ },
+ labelsManagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ issuableTitle: '',
+ issuableDescription: '',
+ selectedLabels: [],
+ };
+ },
+ methods: {
+ handleUpdateSelectedLabels(labels) {
+ if (labels.length) {
+ this.selectedLabels = labels;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form class="common-note-form gfm-form" @submit.stop.prevent>
+ <div data-testid="issuable-title" class="form-group row">
+ <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
+ <div class="col-sm-10">
+ <gl-form-input id="issuable-title" v-model="issuableTitle" :placeholder="__('Title')" />
+ </div>
+ </div>
+ <div data-testid="issuable-description" class="form-group row">
+ <label for="issuable-description" class="col-form-label col-sm-2">{{
+ __('Description')
+ }}</label>
+ <div class="col-sm-10">
+ <markdown-field
+ :markdown-preview-path="descriptionPreviewPath"
+ :markdown-docs-path="descriptionHelpPath"
+ :add-spacing-classes="false"
+ :show-suggest-popover="true"
+ >
+ <textarea
+ id="issuable-description"
+ ref="textarea"
+ slot="textarea"
+ v-model="issuableDescription"
+ dir="auto"
+ class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files hereā€¦')"
+ ></textarea>
+ </markdown-field>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <div data-testid="issuable-labels" class="form-group row">
+ <label for="issuable-labels" class="col-form-label col-md-2 col-lg-4">{{
+ __('Labels')
+ }}</label>
+ <div class="col-md-8 col-sm-10">
+ <div class="issuable-form-select-holder">
+ <labels-select
+ :allow-label-edit="true"
+ :allow-label-create="true"
+ :allow-multiselect="true"
+ :allow-scoped-labels="true"
+ :labels-fetch-path="labelsFetchPath"
+ :labels-manage-path="labelsManagePath"
+ :selected-labels="selectedLabels"
+ :labels-list-title="__('Select label')"
+ :footer-create-label-title="__('Create project label')"
+ :footer-manage-label-title="__('Manage project labels')"
+ :variant="$options.LabelSelectVariant.Embedded"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ data-testid="issuable-create-actions"
+ class="footer-block row-content-block gl-display-flex"
+ >
+ <slot
+ name="actions"
+ :issuable-title="issuableTitle"
+ :issuable-description="issuableDescription"
+ :selected-labels="selectedLabels"
+ ></slot>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
index 395ed0f2033..c94e784c01e 100644
--- a/app/assets/javascripts/vue_shared/components/alert_details_table.vue
+++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
@@ -1,6 +1,14 @@
<script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
+import {
+ capitalizeFirstCharacter,
+ convertToSentenceCase,
+ splitCamelCase,
+} from '~/lib/utils/text_utility';
+
+const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
+const tdClass = 'gl-border-gray-100! gl-p-5!';
export default {
components: {
@@ -18,27 +26,42 @@ export default {
required: true,
},
},
- tableHeader: {
- [s__('AlertManagement|Key')]: s__('AlertManagement|Value'),
- },
+ fields: [
+ {
+ key: 'fieldName',
+ label: s__('AlertManagement|Key'),
+ thClass,
+ tdClass,
+ formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))),
+ },
+ {
+ key: 'value',
+ thClass: `${thClass} w-60p`,
+ tdClass,
+ label: s__('AlertManagement|Value'),
+ },
+ ],
computed: {
items() {
if (!this.alert) {
return [];
}
- return [{ ...this.$options.tableHeader, ...this.alert }];
+ return Object.entries(this.alert).map(([fieldName, value]) => ({
+ fieldName,
+ value,
+ }));
},
},
};
</script>
<template>
<gl-table
- class="alert-management-details-table gl-mb-0!"
+ class="alert-management-details-table"
:busy="loading"
:empty-text="s__('AlertManagement|No alert data to display.')"
:items="items"
+ :fields="$options.fields"
show-empty
- stacked
>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="gl-mt-5" />
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 248e9929833..7aa464dd9b3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -166,7 +166,11 @@ export default {
!state.showDropdownButton &&
!state.showDropdownContents
) {
- this.handleDropdownClose(state.labels.filter(label => label.touched));
+ let filterFn = label => label.touched;
+ if (this.isDropdownVariantEmbedded) {
+ filterFn = label => label.set;
+ }
+ this.handleDropdownClose(state.labels.filter(filterFn));
}
},
/**
@@ -186,7 +190,7 @@ export default {
].some(
className =>
target?.classList.contains(className) ||
- target?.parentElement.classList.contains(className),
+ target?.parentElement?.classList.contains(className),
);
const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss
index 531693a3bbe..a104c06c853 100644
--- a/app/assets/stylesheets/pages/alert_management/details.scss
+++ b/app/assets/stylesheets/pages/alert_management/details.scss
@@ -1,48 +1,4 @@
.alert-management-details {
- // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
- table {
- tr {
- td {
- @include gl-border-0;
- @include gl-p-5;
- border-color: transparent;
-
- &:not(:last-child) {
- border-bottom: 1px solid $table-border-color;
- }
-
- &:first-child {
- div {
- font-weight: bold;
- }
- }
-
- &:not(:first-child) {
- &::before {
- color: $gray-500;
- font-weight: normal !important;
- }
-
- div {
- color: $gray-500;
- }
- }
-
- @include media-breakpoint-up(sm) {
- div {
- text-align: left !important;
- }
- }
- }
-
- &:last-child {
- &::after {
- content: none !important;
- }
- }
- }
- }
-
@include media-breakpoint-down(xs) {
.alert-details-incident-button {
width: 100%;
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index d82271e4b92..f72d02d0bf0 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -210,6 +210,20 @@ module ObjectStorage
end
end
+ class OpenFile
+ extend Forwardable
+
+ # Explicitly exclude :path, because rubyzip uses that to detect "real" files.
+ def_delegators :@file, *(Zip::File::IO_METHODS - [:path])
+
+ # Even though :size is not in IO_METHODS, we do need it.
+ def_delegators :@file, :size
+
+ def initialize(file)
+ @file = file
+ end
+ end
+
# allow to configure and overwrite the filename
def filename
@filename || super || file&.filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -259,6 +273,24 @@ module ObjectStorage
end
end
+ def use_open_file(&blk)
+ Tempfile.open(path) do |file|
+ file.unlink
+ file.binmode
+
+ if file_storage?
+ IO.copy_stream(path, file)
+ else
+ streamer = lambda { |chunk, _, _| file.write(chunk) }
+ Excon.get(url, response_block: streamer)
+ end
+
+ file.seek(0, IO::SEEK_SET)
+
+ yield OpenFile.new(file)
+ end
+ end
+
#
# Move the file to another store
#
diff --git a/app/views/admin/dev_ops_report/_no_data.html.haml b/app/views/admin/dev_ops_report/_no_data.html.haml
index 585aa878b0b..e540a4e2bce 100644
--- a/app/views/admin/dev_ops_report/_no_data.html.haml
+++ b/app/views/admin/dev_ops_report/_no_data.html.haml
@@ -3,5 +3,5 @@
= custom_icon('dev_ops_report_no_data')
%h4= _('Data is still calculating...')
%p
- = _('In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.')
- = link_to _('Learn more'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank'
+ = _('It may be several days before you see feature usage data.')
+ = link_to _('Our documentation includes an example DevOps Score report.'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank'
diff --git a/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb b/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
index 8580802fb7e..a9976c6e5cb 100644
--- a/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
+++ b/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
@@ -14,7 +14,7 @@ module Analytics
idempotent!
def perform
- return if Feature.disabled?(:store_instance_statistics_measurements)
+ return if Feature.disabled?(:store_instance_statistics_measurements, default_enabled: true)
recorded_at = Time.zone.now
measurement_identifiers = Analytics::InstanceStatistics::Measurement.identifiers
diff --git a/changelogs/unreleased/212595-ide-commit-errors.yml b/changelogs/unreleased/212595-ide-commit-errors.yml
new file mode 100644
index 00000000000..cfd28e3cb77
--- /dev/null
+++ b/changelogs/unreleased/212595-ide-commit-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error reporting for Web IDE commits
+merge_request: 42383
+author:
+type: fixed
diff --git a/changelogs/unreleased/229214-replace-LoadingButton-with-GlButton.yml b/changelogs/unreleased/229214-replace-LoadingButton-with-GlButton.yml
new file mode 100644
index 00000000000..3d14795226a
--- /dev/null
+++ b/changelogs/unreleased/229214-replace-LoadingButton-with-GlButton.yml
@@ -0,0 +1,5 @@
+---
+title: Replace LoadingButton with GlButton for the comment dismissal modal
+merge_request: 40882
+author:
+type: performance
diff --git a/changelogs/unreleased/enable-store-instance-statistics-ff-by-default.yml b/changelogs/unreleased/enable-store-instance-statistics-ff-by-default.yml
new file mode 100644
index 00000000000..25077bf65d8
--- /dev/null
+++ b/changelogs/unreleased/enable-store-instance-statistics-ff-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Store object counts periodically for instance statistics
+merge_request: 42433
+author:
+type: changed
diff --git a/changelogs/unreleased/mjang-devops-score-ui-text.yml b/changelogs/unreleased/mjang-devops-score-ui-text.yml
new file mode 100644
index 00000000000..20aa1d8eabe
--- /dev/null
+++ b/changelogs/unreleased/mjang-devops-score-ui-text.yml
@@ -0,0 +1,5 @@
+---
+title: Modify DevOps Score UI Text
+merge_request: 42256
+author:
+type: other
diff --git a/config/feature_flags/development/ci_new_artifact_file_reader.yml b/config/feature_flags/development/ci_new_artifact_file_reader.yml
new file mode 100644
index 00000000000..a6e9c67bd7e
--- /dev/null
+++ b/config/feature_flags/development/ci_new_artifact_file_reader.yml
@@ -0,0 +1,7 @@
+---
+name: ci_new_artifact_file_reader
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40268
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/249588
+group: group::pipeline authoring
+type: development
+default_enabled: false
diff --git a/config/feature_flags/development/store_instance_statistics_measurements.yml b/config/feature_flags/development/store_instance_statistics_measurements.yml
index bc7b3400694..9483b9005df 100644
--- a/config/feature_flags/development/store_instance_statistics_measurements.yml
+++ b/config/feature_flags/development/store_instance_statistics_measurements.yml
@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41300
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247871
group: group::analytics
type: development
-default_enabled: false
+default_enabled: true
diff --git a/doc/.vale/gitlab/Acronyms.yml b/doc/.vale/gitlab/Acronyms.yml
index 113c6012e4a..d26ce9810d7 100644
--- a/doc/.vale/gitlab/Acronyms.yml
+++ b/doc/.vale/gitlab/Acronyms.yml
@@ -76,6 +76,7 @@ exceptions:
- SCSS
- SDK
- SHA
+ - SLA
- SMTP
- SQL
- SSH
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index 442edfa5176..5aa90f02e6c 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -100,7 +100,7 @@ Note the following when promoting a secondary:
- If replication was paused on the secondary node, for example as a part of upgrading,
while you were running a version of GitLab lower than 13.4, you _must_
- [enable the node via the database](#while-promoting-the-secondary-i-got-an-error-activerecordrecordinvalid)
+ [enable the node via the database](../replication/troubleshooting.md#while-promoting-the-secondary-i-got-an-error-activerecordrecordinvalid)
before proceeding.
- A new **secondary** should not be added at this time. If you want to add a new
**secondary**, do this after you have completed the entire process of promoting
@@ -129,28 +129,20 @@ Note the following when promoting a secondary:
```
1. Promote the **secondary** node to the **primary** node.
-
- Before promoting a secondary node to primary, preflight checks should be run. They can be run separately or along with the promotion script.
-
+
To promote the secondary node to primary along with preflight checks:
```shell
gitlab-ctl promote-to-primary-node
```
- If you have already run the [preflight checks](planned_failover.md#preflight-checks) or don't want to run them, you can skip preflight checks with:
+ If you have already run the [preflight checks](planned_failover.md#preflight-checks) separately or don't want to run them, you can skip preflight checks with:
```shell
gitlab-ctl promote-to-primary-node --skip-preflight-check
```
- You can also run preflight checks separately:
-
- ```shell
- gitlab-ctl promotion-preflight-checks
- ```
-
- After all the checks are run, you will be asked for a final confirmation before the promotion to primary. To skip this confirmation, run:
+ You can also promote the secondary node to primary **without any further confirmation**, even when preflight checks fail:
```shell
gitlab-ctl promote-to-primary-node --force
@@ -421,33 +413,4 @@ for another **primary** node. All the old replication settings will be overwritt
## Troubleshooting
-### I followed the disaster recovery instructions and now two-factor auth is broken
-
-The setup instructions for Geo prior to 10.5 failed to replicate the
-`otp_key_base` secret, which is used to encrypt the two-factor authentication
-secrets stored in the database. If it differs between **primary** and **secondary**
-nodes, users with two-factor authentication enabled won't be able to log in
-after a failover.
-
-If you still have access to the old **primary** node, you can follow the
-instructions in the
-[Upgrading to GitLab 10.5](../replication/version_specific_updates.md#updating-to-gitlab-105)
-section to resolve the error. Otherwise, the secret is lost and you'll need to
-[reset two-factor authentication for all users](../../../security/two_factor_authentication.md#disabling-2fa-for-everyone).
-
-### While Promoting the secondary, I got an error `ActiveRecord::RecordInvalid`
-
-If you disabled a secondary node, either with the [replication pause task](../index.md#pausing-and-resuming-replication)
-(13.2) or via the UI (13.1 and earlier), you must first re-enable the
-node before you can continue. This is fixed in 13.4.
-
-From `gitlab-psql`, execute the following, replacing `<your secondary url>`
-with the URL for your secondary server starting with `http` or `https` and ending with a `/`.
-
-```shell
-SECONDARY_URL="https://<secondary url>/"
-DATABASE_NAME="gitlabhq_production"
-sudo gitlab-psql -d "$DATABASE_NAME" -c "UPDATE geo_nodes SET enabled = true WHERE url = '$SECONDARY_URL';"
-```
-
-This should update 1 row.
+This section was moved to [another location](../replication/troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-node).
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
index 26eb39abcee..9b9c386652c 100644
--- a/doc/administration/geo/disaster_recovery/planned_failover.md
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -51,12 +51,6 @@ Run this command to list out all preflight checks and automatically check if rep
gitlab-ctl promotion-preflight-checks
```
-You can run this command in `force` mode to promote to primary even if preflight checks fail:
-
-```shell
-sudo gitlab-ctl promote-to-primary-node --force
-```
-
Each step is described in more detail below.
### Object storage
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 0d7ccb344bb..d0184c641a8 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -632,6 +632,23 @@ To double check this, you can do the following:
UPDATE geo_nodes SET enabled = 't' WHERE id = ID_FROM_ABOVE;
```
+### While Promoting the secondary, I got an error `ActiveRecord::RecordInvalid`
+
+If you disabled a secondary node, either with the [replication pause task](../index.md#pausing-and-resuming-replication)
+(13.2) or via the UI (13.1 and earlier), you must first re-enable the
+node before you can continue. This is fixed in 13.4.
+
+From `gitlab-psql`, execute the following, replacing `<your secondary url>`
+with the URL for your secondary server starting with `http` or `https` and ending with a `/`.
+
+```shell
+SECONDARY_URL="https://<secondary url>/"
+DATABASE_NAME="gitlabhq_production"
+sudo gitlab-psql -d "$DATABASE_NAME" -c "UPDATE geo_nodes SET enabled = true WHERE url = '$SECONDARY_URL';"
+```
+
+This should update 1 row.
+
### Message: ``NoMethodError: undefined method `secondary?' for nil:NilClass``
When [promoting a **secondary** node](../disaster_recovery/index.md#step-3-promoting-a-secondary-node),
@@ -674,6 +691,20 @@ sudo /opt/gitlab/embedded/bin/gitlab-pg-ctl promote
GitLab 12.9 and later are [unaffected by this error](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5147).
+### Two-factor authentication is broken after a failover
+
+The setup instructions for Geo prior to 10.5 failed to replicate the
+`otp_key_base` secret, which is used to encrypt the two-factor authentication
+secrets stored in the database. If it differs between **primary** and **secondary**
+nodes, users with two-factor authentication enabled won't be able to log in
+after a failover.
+
+If you still have access to the old **primary** node, you can follow the
+instructions in the
+[Upgrading to GitLab 10.5](../replication/version_specific_updates.md#updating-to-gitlab-105)
+section to resolve the error. Otherwise, the secret is lost and you'll need to
+[reset two-factor authentication for all users](../../../security/two_factor_authentication.md#disabling-2fa-for-everyone).
+
## Expired artifacts
If you notice for some reason there are more artifacts on the Geo
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index 900d09bdd34..7932233e8cb 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -314,7 +314,7 @@ sudo gitlab-ctl reconfigure
```
If you do not perform this step, you may find that two-factor authentication
-[is broken following DR](../disaster_recovery/index.md#i-followed-the-disaster-recovery-instructions-and-now-two-factor-auth-is-broken).
+[is broken following DR](troubleshooting.md#two-factor-authentication-is-broken-after-a-failover).
To prevent SSH requests to the newly promoted **primary** node from failing
due to SSH host key mismatch when updating the **primary** node domain's DNS record
diff --git a/doc/ci/pipelines/img/ci_efficiency_pipeline_dag_critical_path.png b/doc/ci/pipelines/img/ci_efficiency_pipeline_dag_critical_path.png
new file mode 100644
index 00000000000..1715e8224ab
--- /dev/null
+++ b/doc/ci/pipelines/img/ci_efficiency_pipeline_dag_critical_path.png
Binary files differ
diff --git a/doc/ci/pipelines/img/ci_efficiency_pipeline_health_grafana_dashboard.png b/doc/ci/pipelines/img/ci_efficiency_pipeline_health_grafana_dashboard.png
new file mode 100644
index 00000000000..0956e76804e
--- /dev/null
+++ b/doc/ci/pipelines/img/ci_efficiency_pipeline_health_grafana_dashboard.png
Binary files differ
diff --git a/doc/ci/pipelines/pipeline_efficiency.md b/doc/ci/pipelines/pipeline_efficiency.md
new file mode 100644
index 00000000000..fbcc4321381
--- /dev/null
+++ b/doc/ci/pipelines/pipeline_efficiency.md
@@ -0,0 +1,251 @@
+---
+stage: Verify
+group: Continuous Integration
+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
+type: reference
+---
+
+# Pipeline Efficiency
+
+[CI/CD Pipelines](index.md) are the fundamental building blocks for [GitLab CI/CD](../README.md).
+Making pipelines more efficient helps you save developer time, which:
+
+- Speeds up your DevOps processes
+- Reduces costs
+- Shortens the development feedback loop
+
+It's common that new teams or projects start with slow and inefficient pipelines,
+and improve their configuration over time through trial and error. A better process is
+to use pipeline features that improve efficiency right away, and get a faster software
+development lifecycle earlier.
+
+First ensure you are familiar with [GitLab CI/CD fundamentals](../introduction/index.md)
+and understand the [quick start guide](../quick_start/README.md).
+
+## Identify bottlenecks and common failures
+
+The easiest indicators to check for inefficient pipelines are the runtimes of the jobs,
+stages, and the total runtime of the pipeline itself. The total pipeline duration is
+heavily influenced by the:
+
+- Total number of stages and jobs
+- Dependencies between jobs
+- The ["critical path"](#directed-acyclic-graphs-dag-visualization), which represents
+ the minimum and maximum pipeline duration
+
+Additional points to pay attention relate to [GitLab Runners](../runners/README.md):
+
+- Availability of the runners and the resources they are provisioned with
+- Build dependencies and their installation time
+- [Container image size](#docker-images)
+- Network latency and slow connections
+
+Pipelines frequently failing unnecessarily also causes slowdowns in the development
+lifecycle. You should look for problematic patterns with failed jobs:
+
+- Flaky unit tests which fail randomly, or produce unreliable test results.
+- Test coverage drops and code quality correlated to that behavior.
+- Failures that can be safely ignored, but that halt the pipeline instead.
+- Tests that fail at the end of a long pipeline, but could be in an earlier stage,
+ causing delayed feedback.
+
+## Pipeline analysis
+
+Analyze the performance of your pipeline to find ways to improve efficiency. Analysis
+can help identify possible blockers in the CI/CD infrastructure. This includes analyzing:
+
+- Job workloads
+- Bottlenecks in the execution times
+- The overall pipeline architecture
+
+It's important to understand and document the pipeline workflows, and discuss possible
+actions and changes. Refactoring pipelines may need careful interaction between teams
+in the DevSecOps lifecycle.
+
+Pipeline analysis can help identify issues with cost efficiency. For example, [runners](../runners/README.md)
+hosted with a paid cloud service may be provisioned with:
+
+- More resources than needed for CI/CD pipelines, wasting money.
+- Not enough resources, causing slow runtimes and wasting time.
+
+### Pipeline Insights
+
+The [Pipeline success and duration charts](index.md#pipeline-success-and-duration-charts)
+give information about pipeline runtime and failed job counts.
+
+Tests like [unit tests](../unit_test_reports.md), integration tests, end-to-end tests,
+[code quality](../../user/project/merge_requests/code_quality.md) tests, and others
+ensure that problems are automatically found by the CI/CD pipeline. There could be many
+pipeline stages involved causing long runtimes.
+
+You can improve runtimes by running jobs that test different things in parallel, in
+the same stage, reducing overall runtime. The downside is that you need more runners
+running simultaneously to support the parallel jobs.
+
+The [testing levels for GitLab](../../development/testing_guide/testing_levels.md)
+provide an example of a complex testing strategy with many components involved.
+
+### Directed Acyclic Graphs (DAG) visualization
+
+The [Directed Acyclic Graph](../directed_acyclic_graph/index.md) (DAG) visualization can help analyze the critical path in
+the pipeline and understand possible blockers.
+
+![CI Pipeline Critical Path with DAG](img/ci_efficiency_pipeline_dag_critical_path.png)
+
+### Pipeline Monitoring
+
+Global pipeline health is a key indicator to monitor along with job and pipeline duration.
+[CI/CD analytics](index.md#pipeline-success-and-duration-charts) give a visual
+representation of pipeline health.
+
+Instance administrators have access to additional [performance metrics and self-monitoring](../../administration/monitoring/index.md).
+
+You can fetch specific pipeline health metrics from the [API](../../api/README.md).
+External monitoring tools can poll the API and verify pipeline health or collect
+metrics for long term SLA analytics.
+
+For example, the [GitLab CI Pipelines Exporter](https://github.com/mvisonneau/gitlab-ci-pipelines-exporter)
+for Prometheus fetches metrics from the API. It can check branches in projects automatically
+and get the pipeline status and duration. In combination with a Grafana dashboard,
+this helps build an actionable view for your operations team. Metric graphs can also
+be embedded into incidents making problem resolving easier.
+
+![Grafana Dashboard for GitLab CI Pipelines Prometheus Exporter](img/ci_efficiency_pipeline_health_grafana_dashboard.png)
+
+Alternatively, you can use a monitoring tool that can execute scripts, like
+[`check_gitlab`](https://gitlab.com/6uellerBpanda/check_gitlab) for example.
+
+#### Runner monitoring
+
+You can also [monitor CI runners](https://docs.gitlab.com/runner/monitoring/) on
+their host systems, or in clusters like Kubernetes. This includes checking:
+
+- Disk and disk IO
+- CPU usage
+- Memory
+- Runner process resources
+
+The [Prometheus Node Exporter](https://prometheus.io/docs/guides/node-exporter/)
+can monitor runners on Linux hosts, and [`kube-state-metrics`](https://github.com/kubernetes/kube-state-metrics)
+runs in a Kubernetes cluster.
+
+You can also test [GitLab Runner auto-scaling](https://docs.gitlab.com/runner/configuration/autoscale.html)
+with cloud providers, and define offline times to reduce costs.
+
+#### Dashboards and incident management
+
+Use your existing monitoring tools and dashboards to integrate CI/CD pipeline monitoring,
+or build them from scratch. Ensure that the runtime data is actionable and useful
+in teams, and operations/SREs are able to identify problems early enough.
+[Incident management](../../operations/incident_management/index.md) can help here too,
+with embedded metric charts and all valuable details to analyze the problem.
+
+### Storage usage
+
+Review the storage use of the following to help analyze costs and efficiency:
+
+- [Job artifacts](job_artifacts.md) and their [`expire_in`](../yaml/README.md#artifactsexpire_in)
+ configuration. If kept for too long, storage usage grows and could slow pipelines down.
+- [Container registry](../../user/packages/container_registry/index.md) usage.
+- [Package registry](../../user/packages/package_registry/index.md) usage.
+
+## Pipeline configuration
+
+Make careful choices when configuring pipelines to speed up pipelines and reduce
+resource usage. This includes making use of GitLab CI/CD's built-in features that
+make pipelines run faster and more efficiently.
+
+### Reduce how often jobs run
+
+Try to find which jobs don't need to run in all situations, and use pipeline configuration
+to stop them from running:
+
+- Use the [`interruptible`](../yaml/README.md#interruptible) keyword to stop old pipelines
+ when they are superceded by a newer pipeline.
+- Use [`rules`](../yaml/README.md#rules) to skip tests that aren't needed. For example,
+ skip backend tests when only the frontend code is changed.
+- Run non-essential [scheduled pipelines](schedules.md) less frequently.
+
+### Fail fast
+
+Ensure that errors are detected early in the CI/CD pipeline. A job that takes a very long
+time to complete keeps a pipeline from returning a failed status until the job completes.
+
+Design pipelines so that jobs that can [fail fast](../../user/project/merge_requests/fail_fast_testing.md)
+run earlier. For example, add an early stage and move the syntax, style linting,
+Git commit message verification, and similar jobs in there.
+
+Decide if it's important for long jobs to run early, before fast feedback from
+faster jobs. The initial failures may make it clear that the rest of the pipeline
+shouldn't run, saving pipeline resources.
+
+### Directed Acyclic Graphs (DAG)
+
+In a basic configuration, jobs always wait for all other jobs in earlier stages to complete
+before running. This is the simplest configuration, but it's also the slowest in most
+cases. [Directed Acyclic Graphs](../directed_acyclic_graph/index.md) and
+[parent/child pipelines](../parent_child_pipelines.md) are more flexible and can
+be more efficient, but can also make pipelines harder to understand and analyze.
+
+### Caching
+
+Another optimization method is to use [caching](../caching/index.md) between jobs and stages,
+for example [`/node_modules` for NodeJS](../caching/index.md#caching-nodejs-dependencies).
+
+### Docker Images
+
+Downloading and initializing Docker images can be a large part of the overall runtime
+of jobs.
+
+If a Docker image is slowing down job execution, analyze the base image size and network
+connection to the registry. If GitLab is running in the cloud, look for a cloud container
+registry offered by the vendor. In addition to that, you can make use of the
+[GitLab container registry](../../user/packages/container_registry/index.md) which can be accessed
+by the GitLab instance faster than other registries.
+
+#### Optimize Docker images
+
+Build optimized Docker images because large Docker images use up a lot of space and
+take a long time to download with slower connection speeds. If possible, avoid using
+one large image for all jobs. Use multiple smaller images, each for a specific task,
+that download and run faster.
+
+Try to use custom Docker images with the software pre-installed. It's usually much
+faster to download a larger pre-configured image than to use a common image and install
+software on it each time.
+
+Methods to reduce Docker image size:
+
+- Use a small base image, for example `debian-slim`.
+- Do not install convenience tools like vim, curl, and so on, if they aren't strictly needed.
+- Create a dedicated development image.
+- Disable man pages and docs installed by packages to save space.
+- Reduce the `RUN` layers and combine software installation steps.
+- If using `apt`, add `--no-install-recommends` to avoid unnecessary packages.
+- Clean up caches and files that are no longer needed at the end. For example
+ `rm -rf /var/lib/apt/lists/*` for Debian and Ubuntu, or `yum clean all` for RHEL and CentOS.
+- Use tools like [dive](https://github.com/wagoodman/dive) or [DockerSlim](https://github.com/docker-slim/docker-slim)
+ to analyze and shrink images.
+
+To simplify Docker image management, you can create a dedicated group for managing
+[Docker images](../docker/README.md) and test, build and publish them with CI/CD pipelines.
+
+## Test, document, and learn
+
+Improving pipelines is an iterative process. Make small changes, monitor the effect,
+then iterate again. Many small improvements can add up to a large increase in pipeline
+efficiency.
+
+It can help to document the pipeline design and architecture. You can do this with
+[Mermaid charts in Markdown](../../user/markdown.md#mermaid) directly in the GitLab
+repository.
+
+Document CI/CD pipeline problems and incidents in issues, including research done
+and solutions found. This helps onboarding new team members, and also helps
+identify recurring problems with CI pipeline efficiency.
+
+### Learn More
+
+- [CI Monitoring Webcast Slides](https://docs.google.com/presentation/d/1ONwIIzRB7GWX-WOSziIIv8fz1ngqv77HO1yVfRooOHM/edit?usp=sharing)
+- [GitLab.com Monitoring Handbook](https://about.gitlab.com/handbook/engineering/monitoring/)
+- [Buildings dashboards for operational visibility](https://aws.amazon.com/builders-library/building-dashboards-for-operational-visibility/)
diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md
index 7e10e152304..7550fe69546 100644
--- a/doc/development/contributing/index.md
+++ b/doc/development/contributing/index.md
@@ -86,6 +86,11 @@ If you would like to contribute to GitLab:
- Issues with the
[`~Accepting merge requests` label](issue_workflow.md#label-for-community-contributors)
are a great place to start.
+- Optimizing our tests is another great opportunity to contribute. You can use
+ [RSpec profiling statistics](https://gitlab-org.gitlab.io/rspec_profiling_stats/) to identify
+ slowest tests. These tests are good candidates for improving and checking if any of
+ [best practices](../testing_guide/best_practices.md)
+ could speed them up.
- Consult the [Contribution Flow](#contribution-flow) section to learn the process.
If you have any questions or need help visit [Getting Help](https://about.gitlab.com/get-help/) to
diff --git a/doc/user/search/advanced_search_syntax.md b/doc/user/search/advanced_search_syntax.md
index 42103e17d6c..804d4c540ac 100644
--- a/doc/user/search/advanced_search_syntax.md
+++ b/doc/user/search/advanced_search_syntax.md
@@ -47,13 +47,13 @@ Full details can be found in the [Elasticsearch documentation](https://www.elast
here's a quick guide:
- Searches look for all the words in a query, in any order - e.g.: searching
- issues for `display bug` will return all issues matching both those words, in any order.
-- To find the exact phrase (stemming still applies), use double quotes: `"display bug"`
-- To find bugs not mentioning display, use `-`: `bug -display`
-- To find a bug in display or sound, use `|`: `bug display | sound`
-- To group terms together, use parentheses: `bug | (display +sound)`
-- To match a partial word, use `*`: `bug find_by_*`
-- To find a term containing one of these symbols, use `\`: `argument \-last`
+ issues for [`display bug`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=display+bug&group_id=9970&project_id=278964) and [`bug display`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+Display&group_id=9970&project_id=278964) will return the same results.
+- To find the exact phrase (stemming still applies), use double quotes: [`"display bug"`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=%22display+bug%22&group_id=9970&project_id=278964)
+- To find bugs not mentioning display, use `-`: [`bug -display`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+-display&group_id=9970&project_id=278964)
+- To find a bug in display or banner, use `|`: [`bug display | banner`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+display+%7C+banner&group_id=9970&project_id=278964)
+- To group terms together, use parentheses: [`bug | (display +banner)`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+%7C+%28display+%2Bbanner%29&group_id=9970&project_id=278964)
+- To match a partial word, use `*`. In this example, I want to find bugs with any 500 errors. : [`bug error 50*`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=issues&repository_ref=&search=bug+error+50*&group_id=9970&project_id=278964)
+- To use one of symbols above literally, escape the symbol with a preceding `\`: [`argument \-last`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=argument+%5C-last&group_id=9970&project_id=278964)
### Syntax search filters
@@ -68,11 +68,11 @@ To use them, simply add them to your query in the format `<filter_name>:<value>`
Examples:
-- Finding a file with any content named `hello_world.rb`: `* filename:hello_world.rb`
-- Finding a file named `hello_world` with the text `whatever` inside of it: `whatever filename:hello_world`
-- Finding the text 'def create' inside files with the `.rb` extension: `def create extension:rb`
-- Finding the text `sha` inside files in a folder called `encryption`: `sha path:encryption`
-- Finding any file starting with `hello` containing `world` and with the `.js` extension: `world filename:hello* extension:js`
+- Finding a file with any content named `search_results.rb`: [`* filename:search_results.rb`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=*+filename%3Asearch_results.rb&group_id=9970&project_id=278964)
+- Finding a file named `found_blob_spec.rb` with the text `CHANGELOG` inside of it: [`CHANGELOG filename:found_blob_spec.rb](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=CHANGELOG+filename%3Afound_blob_spec.rb&group_id=9970&project_id=278964)
+- Finding the text `EpicLinks` inside files with the `.rb` extension: [`EpicLinks extension:rb`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=EpicLinks+extension%3Arb&group_id=9970&project_id=278964)
+- Finding the text `Sidekiq` in a file, when that file is in a path that includes `elastic`: [`Sidekiq path:elastic`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=Sidekiq+path%3Aelastic&group_id=9970&project_id=278964)
+- Syntax filters can be combined for complex filtering. Finding any file starting with `search` containing `eventHub` and with the `.js` extension: [`eventHub filename:search* extension:js`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=eventHub+filename%3Asearch*+extension%3Ajs&group_id=9970&project_id=278964)
#### Excluding filters
@@ -86,7 +86,7 @@ Filters can be inversed to **filter out** results from the result set, by prefix
Examples:
-- Finding `rails` in all files but `Gemfile.lock`: `rails -filename:Gemfile.lock`
-- Finding `success` in all files excluding `.po|pot` files: `success -filename:*.po*`
-- Finding `import` excluding minified JavaScript (`.min.js`) files: `import -extension:min.js`
-- Finding `docs` for all files outside the `docs/` folder: `docs -path:docs/`
+- Finding `rails` in all files but `Gemfile.lock`: [`rails -filename:Gemfile.lock`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=rails+-filename%3AGemfile.lock&group_id=9970&project_id=278964)
+- Finding `success` in all files excluding `.po|pot` files: [`success -filename:*.po*`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=success+-filename%3A*.po*&group_id=9970&project_id=278964)
+- Finding `import` excluding minified JavaScript (`.min.js`) files: [`import -extension:min.js`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=import+-extension%3Amin.js&group_id=9970&project_id=278964)
+- Finding `docs` for all files outside the `docs/` folder: [`docs -path:docs/`](https://gitlab.com/search?utf8=%E2%9C%93&snippets=&scope=blobs&repository_ref=&search=docs+-path%3Adocs%2F&group_id=9970&project_id=278964)
diff --git a/lib/gitlab/ci/artifact_file_reader.rb b/lib/gitlab/ci/artifact_file_reader.rb
index c2d17cc176e..6395a20ca99 100644
--- a/lib/gitlab/ci/artifact_file_reader.rb
+++ b/lib/gitlab/ci/artifact_file_reader.rb
@@ -45,6 +45,31 @@ module Gitlab
end
def read_zip_file!(file_path)
+ if ::Gitlab::Ci::Features.new_artifact_file_reader_enabled?(job.project)
+ read_with_new_artifact_file_reader(file_path)
+ else
+ read_with_legacy_artifact_file_reader(file_path)
+ end
+ end
+
+ def read_with_new_artifact_file_reader(file_path)
+ job.artifacts_file.use_open_file do |file|
+ zip_file = Zip::File.new(file, false, true)
+ entry = zip_file.find_entry(file_path)
+
+ unless entry
+ raise Error, "Path `#{file_path}` does not exist inside the `#{job.name}` artifacts archive!"
+ end
+
+ if entry.name_is_directory?
+ raise Error, "Path `#{file_path}` was expected to be a file but it was a directory!"
+ end
+
+ zip_file.read(entry)
+ end
+ end
+
+ def read_with_legacy_artifact_file_reader(file_path)
job.artifacts_file.use_file do |archive_path|
Zip::File.open(archive_path) do |zip_file|
entry = zip_file.find_entry(file_path)
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index 13fb7ca868c..5ab89a56c13 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -78,6 +78,10 @@ module Gitlab
::Feature.enabled?(:ci_enable_live_trace, project) &&
::Feature.enabled?(:ci_accept_trace, project, type: :ops, default_enabled: false)
end
+
+ def self.new_artifact_file_reader_enabled?(project)
+ ::Feature.enabled?(:ci_new_artifact_file_reader, project, default_enabled: false)
+ end
end
end
end
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index 3fd6a15dec4..53bf6daea4c 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -3,8 +3,6 @@
module Gitlab
module UsageDataCounters
module HLLRedisCounter
- include Gitlab::Utils::UsageData
-
DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH = 6.weeks
DEFAULT_DAILY_KEY_EXPIRY_LENGTH = 29.days
DEFAULT_REDIS_SLOT = ''.freeze
@@ -33,6 +31,8 @@ module Gitlab
# * Track event: Gitlab::UsageDataCounters::HLLRedisCounter.track_event(user_id, 'g_compliance_dashboard')
# * Get unique counts per user: Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_dashboard', start_date: 28.days.ago, end_date: Date.current)
class << self
+ include Gitlab::Utils::UsageData
+
def track_event(entity_id, event_name, time = Time.zone.now)
return unless Gitlab::CurrentSettings.usage_ping_enabled?
@@ -54,7 +54,7 @@ module Gitlab
keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date)
- Gitlab::Redis::HLL.count(keys: keys)
+ redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) }
end
def categories
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0ea57afdc90..f38e9a97187 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2678,9 +2678,6 @@ msgstr ""
msgid "An error occurred while checking group path. Please refresh and try again."
msgstr ""
-msgid "An error occurred while committing your changes."
-msgstr ""
-
msgid "An error occurred while creating the issue. Please try again."
msgstr ""
@@ -4114,7 +4111,7 @@ msgstr ""
msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
-msgid "Branch has changed"
+msgid "Branch changed"
msgstr ""
msgid "Branch is already taken"
@@ -4420,6 +4417,9 @@ msgstr ""
msgid "CLOSED (MOVED)"
msgstr ""
+msgid "CODEOWNERS rule violation"
+msgstr ""
+
msgid "CONTRIBUTING"
msgstr ""
@@ -7134,6 +7134,9 @@ msgstr ""
msgid "Could not change HEAD: branch '%{branch}' does not exist"
msgstr ""
+msgid "Could not commit. An unexpected error occurred."
+msgstr ""
+
msgid "Could not connect to FogBugz, check your URL"
msgstr ""
@@ -13324,9 +13327,6 @@ msgstr ""
msgid "In order to enable Service Desk for your instance, you must first set up incoming email."
msgstr ""
-msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
-msgstr ""
-
msgid "In order to personalize your experience with GitLab%{br_tag}we would like to know a bit more about you."
msgstr ""
@@ -14013,6 +14013,9 @@ msgstr ""
msgid "It looks like you have some draft commits in this branch."
msgstr ""
+msgid "It may be several days before you see feature usage data."
+msgstr ""
+
msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected."
msgstr ""
@@ -17757,6 +17760,9 @@ msgstr ""
msgid "Other visibility settings have been disabled by the administrator."
msgstr ""
+msgid "Our documentation includes an example DevOps Score report."
+msgstr ""
+
msgid "Out-of-compliance with this project's policies and should be removed"
msgstr ""
@@ -25705,9 +25711,6 @@ msgstr ""
msgid "This board's scope is reduced"
msgstr ""
-msgid "This branch has changed since you started editing. Would you like to create a new branch?"
-msgstr ""
-
msgid "This chart could not be displayed"
msgstr ""
@@ -27056,6 +27059,9 @@ msgstr ""
msgid "Undo ignore"
msgstr ""
+msgid "Unexpected error"
+msgstr ""
+
msgid "Unfortunately, your email message to GitLab could not be processed."
msgstr ""
@@ -28715,6 +28721,9 @@ msgstr ""
msgid "Workflow Help"
msgstr ""
+msgid "Would you like to create a new branch?"
+msgstr ""
+
msgid "Write"
msgstr ""
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 5218c1e2d98..9d188fc7b77 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -110,7 +110,7 @@ function get_job_id() {
let "page++"
done
- if [[ "${job_id}" == "" ]]; then
+ if [[ "${job_id}" == "null" ]]; then # jq prints "null" for non-existent attribute
echoerr "The '${job_name}' job ID couldn't be retrieved!"
else
echoinfo "The '${job_name}' job ID is ${job_id}"
@@ -142,7 +142,7 @@ function fail_pipeline_early() {
local dont_interrupt_me_job_id
dont_interrupt_me_job_id=$(get_job_id 'dont-interrupt-me' 'scope=success')
- if [[ "${dont_interrupt_me_job_id}" != "" ]]; then
+ if [[ -n "${dont_interrupt_me_job_id}" ]]; then
echoinfo "This pipeline cannot be interrupted due to \`dont-interrupt-me\` job ${dont_interrupt_me_job_id}"
else
echoinfo "Failing pipeline early for fast feedback due to test failures in rspec fail-fast."
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 9245cefc183..56667d6b03d 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -1,10 +1,13 @@
import Vue from 'vue';
+import { getByText } from '@testing-library/dom';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData } from 'jest/ide/mock_data';
import waitForPromises from 'helpers/wait_for_promises';
import { createStore } from '~/ide/stores';
+import consts from '~/ide/stores/modules/commit/constants';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
import { leftSidebarViews } from '~/ide/constants';
+import { createCodeownersCommitError, createUnexpectedCommitError } from '~/ide/lib/errors';
describe('IDE commit form', () => {
const Component = Vue.extend(CommitForm);
@@ -259,21 +262,47 @@ describe('IDE commit form', () => {
});
});
- it('opens new branch modal if commitChanges throws an error', () => {
- vm.commitChanges.mockRejectedValue({ success: false });
+ it.each`
+ createError | props
+ ${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
+ ${createUnexpectedCommitError} | ${{ actionPrimary: null }}
+ `('opens error modal if commitError with $error', async ({ createError, props }) => {
+ jest.spyOn(vm.$refs.commitErrorModal, 'show');
- jest.spyOn(vm.$refs.createBranchModal, 'show').mockImplementation();
+ const error = createError();
+ store.state.commit.commitError = error;
- return vm
- .$nextTick()
- .then(() => {
- vm.$el.querySelector('.btn-success').click();
+ await vm.$nextTick();
- return vm.$nextTick();
- })
- .then(() => {
- expect(vm.$refs.createBranchModal.show).toHaveBeenCalled();
- });
+ expect(vm.$refs.commitErrorModal.show).toHaveBeenCalled();
+ expect(vm.$refs.commitErrorModal).toMatchObject({
+ actionCancel: { text: 'Cancel' },
+ ...props,
+ });
+ // Because of the legacy 'mountComponent' approach here, the only way to
+ // test the text of the modal is by viewing the content of the modal added to the document.
+ expect(document.body).toHaveText(error.messageHTML);
+ });
+ });
+
+ describe('with error modal with primary', () => {
+ beforeEach(() => {
+ jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
+ });
+
+ it('updates commit action and commits', async () => {
+ store.state.commit.commitError = createCodeownersCommitError('test message');
+
+ await vm.$nextTick();
+
+ getByText(document.body, 'Create new branch').click();
+
+ await waitForPromises();
+
+ expect(vm.$store.dispatch.mock.calls).toEqual([
+ ['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
+ ['commit/commitChanges', undefined],
+ ]);
});
});
});
diff --git a/spec/frontend/ide/lib/errors_spec.js b/spec/frontend/ide/lib/errors_spec.js
new file mode 100644
index 00000000000..8c3fb378302
--- /dev/null
+++ b/spec/frontend/ide/lib/errors_spec.js
@@ -0,0 +1,70 @@
+import {
+ createUnexpectedCommitError,
+ createCodeownersCommitError,
+ createBranchChangedCommitError,
+ parseCommitError,
+} from '~/ide/lib/errors';
+
+const TEST_SPECIAL = '&special<';
+const TEST_SPECIAL_ESCAPED = '&amp;special&lt;';
+const TEST_MESSAGE = 'Test message.';
+const CODEOWNERS_MESSAGE =
+ 'Push to protected branches that contain changes to files matching CODEOWNERS is not allowed';
+const CHANGED_MESSAGE = 'Things changed since you started editing';
+
+describe('~/ide/lib/errors', () => {
+ const createResponseError = message => ({
+ response: {
+ data: {
+ message,
+ },
+ },
+ });
+
+ describe('createCodeownersCommitError', () => {
+ it('uses given message', () => {
+ expect(createCodeownersCommitError(TEST_MESSAGE)).toEqual({
+ title: 'CODEOWNERS rule violation',
+ messageHTML: TEST_MESSAGE,
+ canCreateBranch: true,
+ });
+ });
+
+ it('escapes special chars', () => {
+ expect(createCodeownersCommitError(TEST_SPECIAL)).toEqual({
+ title: 'CODEOWNERS rule violation',
+ messageHTML: TEST_SPECIAL_ESCAPED,
+ canCreateBranch: true,
+ });
+ });
+ });
+
+ describe('createBranchChangedCommitError', () => {
+ it.each`
+ message | expectedMessage
+ ${TEST_MESSAGE} | ${`${TEST_MESSAGE}<br/><br/>Would you like to create a new branch?`}
+ ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}<br/><br/>Would you like to create a new branch?`}
+ `('uses given message="$message"', ({ message, expectedMessage }) => {
+ expect(createBranchChangedCommitError(message)).toEqual({
+ title: 'Branch changed',
+ messageHTML: expectedMessage,
+ canCreateBranch: true,
+ });
+ });
+ });
+
+ describe('parseCommitError', () => {
+ it.each`
+ message | expectation
+ ${null} | ${createUnexpectedCommitError()}
+ ${{}} | ${createUnexpectedCommitError()}
+ ${{ response: {} }} | ${createUnexpectedCommitError()}
+ ${{ response: { data: {} } }} | ${createUnexpectedCommitError()}
+ ${createResponseError('test')} | ${createUnexpectedCommitError()}
+ ${createResponseError(CODEOWNERS_MESSAGE)} | ${createCodeownersCommitError(CODEOWNERS_MESSAGE)}
+ ${createResponseError(CHANGED_MESSAGE)} | ${createBranchChangedCommitError(CHANGED_MESSAGE)}
+ `('parses message into error object with "$message"', ({ message, expectation }) => {
+ expect(parseCommitError(message)).toEqual(expectation);
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index a14879112fd..babc50e54f1 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -9,6 +9,7 @@ import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
import * as actions from '~/ide/stores/modules/commit/actions';
+import { createUnexpectedCommitError } from '~/ide/lib/errors';
import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants';
import testAction from '../../../../helpers/vuex_action_helper';
@@ -510,7 +511,7 @@ describe('IDE commit module actions', () => {
});
});
- describe('failed', () => {
+ describe('success response with failed message', () => {
beforeEach(() => {
jest.spyOn(service, 'commit').mockResolvedValue({
data: {
@@ -533,6 +534,25 @@ describe('IDE commit module actions', () => {
});
});
+ describe('failed response', () => {
+ beforeEach(() => {
+ jest.spyOn(service, 'commit').mockRejectedValue({});
+ });
+
+ it('commits error updates', async () => {
+ jest.spyOn(store, 'commit');
+
+ await store.dispatch('commit/commitChanges').catch(() => {});
+
+ expect(store.commit.mock.calls).toEqual([
+ ['commit/CLEAR_ERROR', undefined, undefined],
+ ['commit/UPDATE_LOADING', true, undefined],
+ ['commit/UPDATE_LOADING', false, undefined],
+ ['commit/SET_ERROR', createUnexpectedCommitError(), undefined],
+ ]);
+ });
+ });
+
describe('first commit of a branch', () => {
const COMMIT_RESPONSE = {
id: '123456',
diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
index 45ac1a86ab3..6393a70eac6 100644
--- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
@@ -1,5 +1,6 @@
import commitState from '~/ide/stores/modules/commit/state';
import mutations from '~/ide/stores/modules/commit/mutations';
+import * as types from '~/ide/stores/modules/commit/mutation_types';
describe('IDE commit module mutations', () => {
let state;
@@ -62,4 +63,24 @@ describe('IDE commit module mutations', () => {
expect(state.shouldCreateMR).toBe(false);
});
});
+
+ describe(types.CLEAR_ERROR, () => {
+ it('should clear commitError', () => {
+ state.commitError = {};
+
+ mutations[types.CLEAR_ERROR](state);
+
+ expect(state.commitError).toBeNull();
+ });
+ });
+
+ describe(types.SET_ERROR, () => {
+ it('should set commitError', () => {
+ const error = { title: 'foo' };
+
+ mutations[types.SET_ERROR](state, error);
+
+ expect(state.commitError).toBe(error);
+ });
+ });
});
diff --git a/spec/frontend/issuable_create/components/issuable_create_root_spec.js b/spec/frontend/issuable_create/components/issuable_create_root_spec.js
new file mode 100644
index 00000000000..675d01ae4af
--- /dev/null
+++ b/spec/frontend/issuable_create/components/issuable_create_root_spec.js
@@ -0,0 +1,64 @@
+import { mount } from '@vue/test-utils';
+
+import IssuableCreateRoot from '~/issuable_create/components/issuable_create_root.vue';
+import IssuableForm from '~/issuable_create/components/issuable_form.vue';
+
+const createComponent = ({
+ descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
+ descriptionHelpPath = '/help/user/markdown',
+ labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json',
+ labelsManagePath = '/gitlab-org/gitlab-shell/-/labels',
+} = {}) => {
+ return mount(IssuableCreateRoot, {
+ propsData: {
+ descriptionPreviewPath,
+ descriptionHelpPath,
+ labelsFetchPath,
+ labelsManagePath,
+ },
+ slots: {
+ title: `
+ <h1 class="js-create-title">New Issuable</h1>
+ `,
+ actions: `
+ <button class="js-issuable-save">Submit issuable</button>
+ `,
+ },
+ });
+};
+
+describe('IssuableCreateRoot', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ it('renders component container element with class "issuable-create-container"', () => {
+ expect(wrapper.classes()).toContain('issuable-create-container');
+ });
+
+ it('renders contents for slot "title"', () => {
+ const titleEl = wrapper.find('h1.js-create-title');
+
+ expect(titleEl.exists()).toBe(true);
+ expect(titleEl.text()).toBe('New Issuable');
+ });
+
+ it('renders issuable-form component', () => {
+ expect(wrapper.find(IssuableForm).exists()).toBe(true);
+ });
+
+ it('renders contents for slot "actions" within issuable-form component', () => {
+ const buttonEl = wrapper.find(IssuableForm).find('button.js-issuable-save');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('Submit issuable');
+ });
+ });
+});
diff --git a/spec/frontend/issuable_create/components/issuable_form_spec.js b/spec/frontend/issuable_create/components/issuable_form_spec.js
new file mode 100644
index 00000000000..0d922727209
--- /dev/null
+++ b/spec/frontend/issuable_create/components/issuable_form_spec.js
@@ -0,0 +1,118 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+
+import IssuableForm from '~/issuable_create/components/issuable_form.vue';
+
+const createComponent = ({
+ descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown',
+ descriptionHelpPath = '/help/user/markdown',
+ labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json',
+ labelsManagePath = '/gitlab-org/gitlab-shell/-/labels',
+} = {}) => {
+ return shallowMount(IssuableForm, {
+ propsData: {
+ descriptionPreviewPath,
+ descriptionHelpPath,
+ labelsFetchPath,
+ labelsManagePath,
+ },
+ slots: {
+ actions: `
+ <button class="js-issuable-save">Submit issuable</button>
+ `,
+ },
+ });
+};
+
+describe('IssuableForm', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('methods', () => {
+ describe('handleUpdateSelectedLabels', () => {
+ it('sets provided `labels` param to prop `selectedLabels`', () => {
+ const labels = [
+ {
+ id: 1,
+ color: '#BADA55',
+ text_color: '#ffffff',
+ title: 'Documentation',
+ },
+ ];
+
+ wrapper.vm.handleUpdateSelectedLabels(labels);
+
+ expect(wrapper.vm.selectedLabels).toBe(labels);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders issuable title input field', () => {
+ const titleFieldEl = wrapper.find('[data-testid="issuable-title"]');
+
+ expect(titleFieldEl.exists()).toBe(true);
+ expect(titleFieldEl.find('label').text()).toBe('Title');
+ expect(titleFieldEl.find(GlFormInput).exists()).toBe(true);
+ expect(titleFieldEl.find(GlFormInput).attributes('placeholder')).toBe('Title');
+ });
+
+ it('renders issuable description input field', () => {
+ const descriptionFieldEl = wrapper.find('[data-testid="issuable-description"]');
+
+ expect(descriptionFieldEl.exists()).toBe(true);
+ expect(descriptionFieldEl.find('label').text()).toBe('Description');
+ expect(descriptionFieldEl.find(MarkdownField).exists()).toBe(true);
+ expect(descriptionFieldEl.find(MarkdownField).props()).toMatchObject({
+ markdownPreviewPath: wrapper.vm.descriptionPreviewPath,
+ markdownDocsPath: wrapper.vm.descriptionHelpPath,
+ addSpacingClasses: false,
+ showSuggestPopover: true,
+ });
+ expect(descriptionFieldEl.find('textarea').exists()).toBe(true);
+ expect(descriptionFieldEl.find('textarea').attributes('placeholder')).toBe(
+ 'Write a comment or drag your files hereā€¦',
+ );
+ });
+
+ it('renders labels select field', () => {
+ const labelsSelectEl = wrapper.find('[data-testid="issuable-labels"]');
+
+ expect(labelsSelectEl.exists()).toBe(true);
+ expect(labelsSelectEl.find('label').text()).toBe('Labels');
+ expect(labelsSelectEl.find(LabelsSelect).exists()).toBe(true);
+ expect(labelsSelectEl.find(LabelsSelect).props()).toMatchObject({
+ allowLabelEdit: true,
+ allowLabelCreate: true,
+ allowMultiselect: true,
+ allowScopedLabels: true,
+ labelsFetchPath: wrapper.vm.labelsFetchPath,
+ labelsManagePath: wrapper.vm.labelsManagePath,
+ selectedLabels: wrapper.vm.selectedLabels,
+ labelsListTitle: 'Select label',
+ footerCreateLabelTitle: 'Create project label',
+ footerManageLabelTitle: 'Manage project labels',
+ variant: 'embedded',
+ });
+ });
+
+ it('renders contents for slot "actions"', () => {
+ const buttonEl = wrapper
+ .find('[data-testid="issuable-create-actions"]')
+ .find('button.js-issuable-save');
+
+ expect(buttonEl.exists()).toBe(true);
+ expect(buttonEl.text()).toBe('Submit issuable');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index a1e0db4d29e..6cd7ed9970a 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -65,6 +65,33 @@ describe('LabelsSelectRoot', () => {
]),
);
});
+
+ it('calls `handleDropdownClose` with state.labels filterd using `set` prop when dropdown variant is `embedded`', () => {
+ wrapper = createComponent({
+ ...mockConfig,
+ variant: 'embedded',
+ });
+
+ jest.spyOn(wrapper.vm, 'handleDropdownClose').mockImplementation();
+
+ wrapper.vm.handleVuexActionDispatch(
+ { type: 'toggleDropdownContents' },
+ {
+ showDropdownButton: false,
+ showDropdownContents: false,
+ labels: [{ id: 1 }, { id: 2, set: true }],
+ },
+ );
+
+ expect(wrapper.vm.handleDropdownClose).toHaveBeenCalledWith(
+ expect.arrayContaining([
+ {
+ id: 2,
+ set: true,
+ },
+ ]),
+ );
+ });
});
describe('handleDropdownClose', () => {
diff --git a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
index e982f0eb015..83a37655ea9 100644
--- a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
+++ b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
@@ -18,6 +18,17 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do
expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
end
+ context 'when FF ci_new_artifact_file_reader is disabled' do
+ before do
+ stub_feature_flags(ci_new_artifact_file_reader: false)
+ end
+
+ it 'returns the content at the path' do
+ is_expected.to be_present
+ expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
+ end
+ end
+
context 'when path does not exist' do
let(:path) { 'file/does/not/exist.txt' }
let(:expected_error) do
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index a0583c860cd..c73a9a7aab1 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -210,6 +210,27 @@ RSpec.describe ObjectStorage do
end
end
+ describe '#use_open_file' do
+ context 'when file is stored locally' do
+ it "returns the file" do
+ expect { |b| uploader.use_open_file(&b) }.to yield_with_args(an_instance_of(ObjectStorage::Concern::OpenFile))
+ end
+ end
+
+ context 'when file is stored remotely' do
+ let(:store) { described_class::Store::REMOTE }
+
+ before do
+ stub_artifacts_object_storage
+ stub_request(:get, %r{s3.amazonaws.com/#{uploader.path}}).to_return(status: 200, body: '')
+ end
+
+ it "returns the file" do
+ expect { |b| uploader.use_open_file(&b) }.to yield_with_args(an_instance_of(ObjectStorage::Concern::OpenFile))
+ end
+ end
+ end
+
describe '#migrate!' do
subject { uploader.migrate!(new_store) }
@@ -844,4 +865,19 @@ RSpec.describe ObjectStorage do
end
end
end
+
+ describe 'OpenFile' do
+ subject { ObjectStorage::Concern::OpenFile.new(file) }
+
+ let(:file) { double(read: true, size: true, path: true) }
+
+ it 'delegates read and size methods' do
+ expect(subject.read).to eq(true)
+ expect(subject.size).to eq(true)
+ end
+
+ it 'does not delegate path method' do
+ expect { subject.path }.to raise_error(NoMethodError)
+ end
+ end
end