Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-16 09:09:43 +00:00
parent 6b9b8a52ba
commit 984357420a
47 changed files with 1146 additions and 197 deletions

View file

@ -45,6 +45,7 @@
"Debian",
"DevOps",
"Docker",
"DockerSlim",
"Elasticsearch",
"Facebook",
"fastlane",

View file

@ -1,4 +1,4 @@
#import "./issue.fragment.graphql"
#import "ee_else_ce/boards/queries/issue.fragment.graphql"
mutation IssueMoveList(
$projectPath: ID!

View file

@ -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>

View file

@ -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>

View file

@ -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();
};

View file

@ -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'));
});
};

View file

@ -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';

View file

@ -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;
},
};

View file

@ -4,4 +4,5 @@ export default () => ({
newBranchName: '',
submitCommitLoading: false,
shouldCreateMR: true,
commitError: null,
});

View file

@ -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>

View file

@ -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>

View file

@ -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" />

View file

@ -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(

View file

@ -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%;

View file

@ -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
#

View file

@ -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'

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: Fix error reporting for Web IDE commits
merge_request: 42383
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Replace LoadingButton with GlButton for the comment dismissal modal
merge_request: 40882
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Store object counts periodically for instance statistics
merge_request: 42433
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Modify DevOps Score UI Text
merge_request: 42256
author:
type: other

View file

@ -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

View file

@ -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

View file

@ -76,6 +76,7 @@ exceptions:
- SCSS
- SDK
- SHA
- SLA
- SMTP
- SQL
- SSH

View file

@ -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).

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View file

@ -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/)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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 ""

View file

@ -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."

View file

@ -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],
]);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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',

View file

@ -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);
});
});
});

View file

@ -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');
});
});
});

View file

@ -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');
});
});
});

View file

@ -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', () => {

View file

@ -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

View file

@ -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