Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
900a0a5a4a
commit
2e31a394c5
48 changed files with 325 additions and 657 deletions
|
@ -829,7 +829,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
|
|||
/app/assets/javascripts/runner/components/registration/registration_token.vue @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/runner/components/search_tokens/ @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/token_access/components/ @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/javascripts/token_access/index.js @gitlab-org/manage/authentication-and-authorization
|
||||
/app/assets/stylesheets/page_bundles/profile_two_factor_auth.scss @gitlab-org/manage/authentication-and-authorization
|
||||
|
|
|
@ -1383,7 +1383,6 @@ RSpec/ContextWording:
|
|||
- 'spec/controllers/projects/settings/repository_controller_spec.rb'
|
||||
- 'spec/controllers/projects/snippets_controller_spec.rb'
|
||||
- 'spec/controllers/projects/starrers_controller_spec.rb'
|
||||
- 'spec/controllers/projects/static_site_editor_controller_spec.rb'
|
||||
- 'spec/controllers/projects/tags_controller_spec.rb'
|
||||
- 'spec/controllers/projects/todos_controller_spec.rb'
|
||||
- 'spec/controllers/projects/tree_controller_spec.rb'
|
||||
|
@ -2511,7 +2510,6 @@ RSpec/ContextWording:
|
|||
- 'spec/lib/gitlab/spamcheck/client_spec.rb'
|
||||
- 'spec/lib/gitlab/sql/pattern_spec.rb'
|
||||
- 'spec/lib/gitlab/ssh_public_key_spec.rb'
|
||||
- 'spec/lib/gitlab/static_site_editor/config/file_config/entry/mount_spec.rb'
|
||||
- 'spec/lib/gitlab/submodule_links_spec.rb'
|
||||
- 'spec/lib/gitlab/subscription_portal_spec.rb'
|
||||
- 'spec/lib/gitlab/suggestions/commit_message_spec.rb'
|
||||
|
@ -3513,7 +3511,6 @@ RSpec/ContextWording:
|
|||
- 'spec/services/spam/ham_service_spec.rb'
|
||||
- 'spec/services/spam/spam_action_service_spec.rb'
|
||||
- 'spec/services/spam/spam_verdict_service_spec.rb'
|
||||
- 'spec/services/static_site_editor/config_service_spec.rb'
|
||||
- 'spec/services/submodules/update_service_spec.rb'
|
||||
- 'spec/services/suggestions/apply_service_spec.rb'
|
||||
- 'spec/services/suggestions/create_service_spec.rb'
|
||||
|
|
|
@ -286,7 +286,6 @@ RSpec/ReturnFromStub:
|
|||
- 'spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb'
|
||||
- 'spec/services/projects/update_remote_mirror_service_spec.rb'
|
||||
- 'spec/services/projects/update_service_spec.rb'
|
||||
- 'spec/services/static_site_editor/config_service_spec.rb'
|
||||
- 'spec/services/suggestions/apply_service_spec.rb'
|
||||
- 'spec/services/suggestions/create_service_spec.rb'
|
||||
- 'spec/services/verify_pages_domain_service_spec.rb'
|
||||
|
|
|
@ -225,7 +225,6 @@ Style/ClassAndModuleChildren:
|
|||
- 'app/controllers/projects/snippets/blobs_controller.rb'
|
||||
- 'app/controllers/projects/snippets_controller.rb'
|
||||
- 'app/controllers/projects/starrers_controller.rb'
|
||||
- 'app/controllers/projects/static_site_editor_controller.rb'
|
||||
- 'app/controllers/projects/tags/releases_controller.rb'
|
||||
- 'app/controllers/projects/tags_controller.rb'
|
||||
- 'app/controllers/projects/templates_controller.rb'
|
||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
noteIsConfidential: {
|
||||
isInternalNote: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
|
@ -44,7 +44,7 @@ export default {
|
|||
return this.noteableData.issue_email_participants?.map(({ email }) => email) || [];
|
||||
},
|
||||
showEmailParticipantsWarning() {
|
||||
return this.emailParticipants.length && !this.noteIsConfidential;
|
||||
return this.emailParticipants.length && !this.isInternalNote;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -60,7 +60,7 @@ export default {
|
|||
note: '',
|
||||
noteType: constants.COMMENT,
|
||||
errors: [],
|
||||
noteIsConfidential: false,
|
||||
noteIsInternal: false,
|
||||
isSubmitting: false,
|
||||
};
|
||||
},
|
||||
|
@ -91,13 +91,13 @@ export default {
|
|||
},
|
||||
commentButtonTitle() {
|
||||
const { comment, internalComment, startThread, startInternalThread } = this.$options.i18n;
|
||||
if (this.noteIsConfidential) {
|
||||
if (this.noteIsInternal) {
|
||||
return this.noteType === constants.COMMENT ? internalComment : startInternalThread;
|
||||
}
|
||||
return this.noteType === constants.COMMENT ? comment : startThread;
|
||||
},
|
||||
textareaPlaceholder() {
|
||||
return this.noteIsConfidential
|
||||
return this.noteIsInternal
|
||||
? this.$options.i18n.bodyPlaceholderInternal
|
||||
: this.$options.i18n.bodyPlaceholder;
|
||||
},
|
||||
|
@ -110,7 +110,7 @@ export default {
|
|||
canCreateNote() {
|
||||
return this.getNoteableData.current_user.can_create_note;
|
||||
},
|
||||
canSetConfidential() {
|
||||
canSetInternalNote() {
|
||||
return this.getNoteableData.current_user.can_update && (this.isIssue || this.isEpic);
|
||||
},
|
||||
issueActionButtonTitle() {
|
||||
|
@ -172,7 +172,7 @@ export default {
|
|||
trackingLabel() {
|
||||
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
|
||||
},
|
||||
confidentialNotesEnabled() {
|
||||
internalNotesEnabled() {
|
||||
return Boolean(this.glFeatures.confidentialNotes);
|
||||
},
|
||||
disableSubmitButton() {
|
||||
|
@ -217,7 +217,11 @@ export default {
|
|||
note: {
|
||||
noteable_type: this.noteableType,
|
||||
noteable_id: this.getNoteableData.id,
|
||||
confidential: this.noteIsConfidential,
|
||||
// Internal notes were identified as `confidential`
|
||||
// before we decided to treat them as _internal_
|
||||
// so now until API is updated we need to use `confidential`
|
||||
// in request payload.
|
||||
confidential: this.noteIsInternal,
|
||||
note: this.note,
|
||||
},
|
||||
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
|
||||
|
@ -292,7 +296,7 @@ export default {
|
|||
|
||||
if (shouldClear) {
|
||||
this.note = '';
|
||||
this.noteIsConfidential = false;
|
||||
this.noteIsInternal = false;
|
||||
this.resizeTextarea();
|
||||
this.$refs.markdownField.previewMarkdown = false;
|
||||
}
|
||||
|
@ -356,7 +360,7 @@ export default {
|
|||
<comment-field-layout
|
||||
:with-alert-container="true"
|
||||
:noteable-data="getNoteableData"
|
||||
:note-is-confidential="noteIsConfidential"
|
||||
:is-internal-note="noteIsInternal"
|
||||
:noteable-type="noteableType"
|
||||
>
|
||||
<markdown-field
|
||||
|
@ -410,17 +414,17 @@ export default {
|
|||
</template>
|
||||
<template v-else>
|
||||
<gl-form-checkbox
|
||||
v-if="confidentialNotesEnabled && canSetConfidential"
|
||||
v-model="noteIsConfidential"
|
||||
v-if="internalNotesEnabled && canSetInternalNote"
|
||||
v-model="noteIsInternal"
|
||||
class="gl-mb-6"
|
||||
data-testid="confidential-note-checkbox"
|
||||
data-testid="internal-note-checkbox"
|
||||
>
|
||||
{{ $options.i18n.confidential }}
|
||||
{{ $options.i18n.internal }}
|
||||
<gl-icon
|
||||
v-gl-tooltip:tooltipcontainer.bottom
|
||||
name="question"
|
||||
:size="16"
|
||||
:title="$options.i18n.confidentialVisibility"
|
||||
:title="$options.i18n.internalVisibility"
|
||||
class="gl-text-gray-500"
|
||||
/>
|
||||
</gl-form-checkbox>
|
||||
|
@ -429,7 +433,7 @@ export default {
|
|||
class="gl-mr-3"
|
||||
:disabled="disableSubmitButton"
|
||||
:tracking-label="trackingLabel"
|
||||
:is-internal-note="noteIsConfidential"
|
||||
:is-internal-note="noteIsInternal"
|
||||
:noteable-display-name="noteableDisplayName"
|
||||
:discussions-require-resolution="discussionsRequireResolution"
|
||||
@click="handleSave"
|
||||
|
|
|
@ -8,7 +8,7 @@ import { __ } from '~/locale';
|
|||
import '~/behaviors/markdown/render_gfm';
|
||||
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
|
||||
import autosave from '../mixins/autosave';
|
||||
import { CONFIDENTIAL_CLASSES } from '../constants';
|
||||
import { INTERNAL_NOTE_CLASSES } from '../constants';
|
||||
import noteAttachment from './note_attachment.vue';
|
||||
import noteAwardsList from './note_awards_list.vue';
|
||||
import noteEditedText from './note_edited_text.vue';
|
||||
|
@ -55,7 +55,7 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
isConfidential: {
|
||||
isInternalNote: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
|
@ -101,9 +101,9 @@ export default {
|
|||
|
||||
return escape(suggestion);
|
||||
},
|
||||
confidentialContainerClasses() {
|
||||
if (this.isConfidential && !this.isEditing) {
|
||||
return CONFIDENTIAL_CLASSES;
|
||||
internalNoteContainerClasses() {
|
||||
if (this.isInternalNote && !this.isEditing) {
|
||||
return INTERNAL_NOTE_CLASSES;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
@ -179,7 +179,7 @@ export default {
|
|||
}"
|
||||
class="note-body"
|
||||
>
|
||||
<div :class="confidentialContainerClasses" data-testid="note-confidential-container">
|
||||
<div :class="internalNoteContainerClasses" data-testid="note-internal-container">
|
||||
<suggestions
|
||||
v-if="hasSuggestion && !isEditing"
|
||||
:suggestions="note.suggestions"
|
||||
|
|
|
@ -329,7 +329,7 @@ export default {
|
|||
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
|
||||
<comment-field-layout
|
||||
:noteable-data="getNoteableData"
|
||||
:note-is-confidential="discussion.confidential"
|
||||
:is-internal-note="discussion.confidential"
|
||||
>
|
||||
<markdown-field
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
|
|
|
@ -67,7 +67,7 @@ export default {
|
|||
required: false,
|
||||
default: true,
|
||||
},
|
||||
isConfidential: {
|
||||
isInternalNote: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
|
@ -110,7 +110,7 @@ export default {
|
|||
authorName() {
|
||||
return this.author.name;
|
||||
},
|
||||
noteConfidentialityTooltip() {
|
||||
internalNoteTooltip() {
|
||||
return s__('Notes|This internal note will always remain confidential');
|
||||
},
|
||||
},
|
||||
|
@ -231,13 +231,13 @@ export default {
|
|||
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
|
||||
</template>
|
||||
<gl-badge
|
||||
v-if="isConfidential"
|
||||
v-if="isInternalNote"
|
||||
v-gl-tooltip:tooltipcontainer.bottom
|
||||
data-testid="internalNoteIndicator"
|
||||
variant="warning"
|
||||
size="sm"
|
||||
class="gl-mb-3 gl-ml-2"
|
||||
:title="noteConfidentialityTooltip"
|
||||
:title="internalNoteTooltip"
|
||||
>
|
||||
{{ __('Internal note') }}
|
||||
</gl-badge>
|
||||
|
|
|
@ -447,7 +447,7 @@ export default {
|
|||
:author="author"
|
||||
:created-at="note.created_at"
|
||||
:note-id="note.id"
|
||||
:is-confidential="note.confidential"
|
||||
:is-internal-note="note.confidential"
|
||||
:noteable-type="noteableType"
|
||||
>
|
||||
<template #note-header-info>
|
||||
|
@ -494,7 +494,7 @@ export default {
|
|||
ref="noteBody"
|
||||
:note="note"
|
||||
:can-edit="note.current_user.can_edit"
|
||||
:is-confidential="note.confidential"
|
||||
:is-internal-note="note.confidential"
|
||||
:line="line"
|
||||
:file="diffFile"
|
||||
:is-editing="isEditing"
|
||||
|
|
|
@ -52,4 +52,4 @@ export const toggleStateErrorMessage = {
|
|||
},
|
||||
};
|
||||
|
||||
export const CONFIDENTIAL_CLASSES = ['gl-bg-orange-50', 'gl-px-4', 'gl-py-2'];
|
||||
export const INTERNAL_NOTE_CLASSES = ['gl-bg-orange-50', 'gl-px-4', 'gl-py-2'];
|
||||
|
|
|
@ -14,8 +14,8 @@ export const COMMENT_FORM = {
|
|||
epic: __('epic'),
|
||||
bodyPlaceholder: __('Write a comment or drag your files here…'),
|
||||
bodyPlaceholderInternal: __('Write an internal note or drag your files here…'),
|
||||
confidential: s__('Notes|Make this an internal note'),
|
||||
confidentialVisibility: s__(
|
||||
internal: s__('Notes|Make this an internal note'),
|
||||
internalVisibility: s__(
|
||||
'Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher',
|
||||
),
|
||||
discussionThatNeedsResolution: __(
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
<script>
|
||||
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import getPipelineWarnings from '../../graphql/queries/get_pipeline_warnings.query.graphql';
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
expectedMessage: 'will be removed in',
|
||||
i18n: {
|
||||
title: __('Found warning in your .gitlab-ci.yml'),
|
||||
rootTypesWarning: __(
|
||||
'%{codeStart}types%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stages%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}',
|
||||
),
|
||||
typeWarning: __(
|
||||
'%{codeStart}type%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stage%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}',
|
||||
),
|
||||
},
|
||||
components: {
|
||||
GlAlert,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
inject: ['deprecatedKeywordsDocPath', 'fullPath', 'pipelineIid'],
|
||||
apollo: {
|
||||
warnings: {
|
||||
query: getPipelineWarnings,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
iid: this.pipelineIid,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data?.project?.pipeline?.warningMessages || [];
|
||||
},
|
||||
error() {
|
||||
this.hasError = true;
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
warnings: [],
|
||||
hasError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
deprecationWarnings() {
|
||||
return this.warnings.filter(({ content }) => {
|
||||
return content.includes(this.$options.expectedMessage);
|
||||
});
|
||||
},
|
||||
formattedWarnings() {
|
||||
// The API doesn't have a mechanism currently to return a
|
||||
// type instead of just the error message. To work around this,
|
||||
// we check if the deprecation message is found within the warnings
|
||||
// and show a FE version of that message with the link to the documentation
|
||||
// and translated. We can have only 2 types of warnings: root types and individual
|
||||
// type. If the word `root` is present, then we know it's the root type deprecation
|
||||
// and if not, it's the normal type. This has to be deleted in 15.0.
|
||||
// Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/350810
|
||||
return this.deprecationWarnings.map(({ content }) => {
|
||||
if (content.includes('root')) {
|
||||
return this.$options.i18n.rootTypesWarning;
|
||||
}
|
||||
return this.$options.i18n.typeWarning;
|
||||
});
|
||||
},
|
||||
hasDeprecationWarning() {
|
||||
return this.formattedWarnings.length > 0;
|
||||
},
|
||||
showWarning() {
|
||||
return (
|
||||
!this.$apollo.queries.warnings?.loading && !this.hasError && this.hasDeprecationWarning
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-alert
|
||||
v-if="showWarning"
|
||||
:title="$options.i18n.title"
|
||||
variant="warning"
|
||||
:dismissible="false"
|
||||
>
|
||||
<ul class="gl-mb-0">
|
||||
<li v-for="warning in formattedWarnings" :key="warning">
|
||||
<gl-sprintf :message="warning">
|
||||
<template #code="{ content }">
|
||||
<code> {{ content }}</code>
|
||||
</template>
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="deprecatedKeywordsDocPath" target="_blank"> {{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</li>
|
||||
</ul>
|
||||
</gl-alert>
|
||||
</div>
|
||||
</template>
|
|
@ -19,7 +19,10 @@ export default {
|
|||
},
|
||||
testCase: {
|
||||
type: Object,
|
||||
required: true,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -49,6 +52,7 @@ export default {
|
|||
},
|
||||
text: {
|
||||
name: __('Name'),
|
||||
file: __('File'),
|
||||
duration: __('Execution time'),
|
||||
history: __('History'),
|
||||
trace: __('System output'),
|
||||
|
@ -74,11 +78,24 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="testCase.file" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
|
||||
<strong class="gl-text-right col-sm-3">{{ $options.text.file }}</strong>
|
||||
<div class="col-sm-9" data-testid="test-case-file">
|
||||
<gl-link v-if="testCase.filePath" :href="testCase.filePath">
|
||||
{{ testCase.file }}
|
||||
</gl-link>
|
||||
<span v-else>{{ testCase.file }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
|
||||
<strong class="gl-text-right col-sm-3">{{ $options.text.duration }}</strong>
|
||||
<div class="col-sm-9" data-testid="test-case-duration">
|
||||
<div v-if="testCase.formattedTime" class="col-sm-9" data-testid="test-case-duration">
|
||||
{{ testCase.formattedTime }}
|
||||
</div>
|
||||
<div v-else-if="testCase.execution_time" class="col-sm-9" data-testid="test-case-duration">
|
||||
{{ sprintf('%{value} s', { value: testCase.execution_time }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="testCase.recent_failures" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
query getPipelineWarnings($fullPath: ID!, $iid: ID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
pipeline(iid: $iid) {
|
||||
id
|
||||
warningMessages {
|
||||
content
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ import { __, s__ } from '~/locale';
|
|||
import createDagApp from './pipeline_details_dag';
|
||||
import { createPipelinesDetailApp } from './pipeline_details_graph';
|
||||
import { createPipelineHeaderApp } from './pipeline_details_header';
|
||||
import { createPipelineNotificationApp } from './pipeline_details_notification';
|
||||
import { createPipelineJobsApp } from './pipeline_details_jobs';
|
||||
import { createPipelineFailedJobsApp } from './pipeline_details_failed_jobs';
|
||||
import { apolloProvider } from './pipeline_shared_client';
|
||||
|
@ -13,7 +12,6 @@ const SELECTORS = {
|
|||
PIPELINE_DETAILS: '.js-pipeline-details-vue',
|
||||
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
|
||||
PIPELINE_HEADER: '#js-pipeline-header-vue',
|
||||
PIPELINE_NOTIFICATION: '#js-pipeline-notification',
|
||||
PIPELINE_TABS: '#js-pipeline-tabs',
|
||||
PIPELINE_TESTS: '#js-pipeline-tests-detail',
|
||||
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
|
||||
|
@ -31,14 +29,6 @@ export default async function initPipelineDetailsBundle() {
|
|||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading a section of this page.'),
|
||||
});
|
||||
}
|
||||
|
||||
if (gon.features?.pipelineTabsVue) {
|
||||
const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
|
||||
const { createPipelineTabs } = await import('./pipeline_tabs');
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import DeprecatedKeywordNotification from './components/notification/deprecated_type_keyword_notification.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
export const createPipelineNotificationApp = (elSelector, apolloProvider) => {
|
||||
const el = document.querySelector(elSelector);
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el.dataset;
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
components: {
|
||||
DeprecatedKeywordNotification,
|
||||
},
|
||||
provide: {
|
||||
deprecatedKeywordsDocPath,
|
||||
fullPath,
|
||||
pipelineIid,
|
||||
},
|
||||
apolloProvider,
|
||||
render(createElement) {
|
||||
return createElement('deprecated-keyword-notification');
|
||||
},
|
||||
});
|
||||
};
|
|
@ -159,11 +159,7 @@ export default {
|
|||
},
|
||||
method: 'fetchData',
|
||||
successCallback: (response) => {
|
||||
const headers = normalizeHeaders(response.headers);
|
||||
|
||||
if (!headers['POLL-INTERVAL']) {
|
||||
allData.push(response.data);
|
||||
}
|
||||
this.headerCheck(response, (data) => allData.push(data));
|
||||
|
||||
if (allData.length === requests.length) {
|
||||
this.setCollapsedData(allData);
|
||||
|
@ -183,21 +179,23 @@ export default {
|
|||
fetchData: () => this.fetchCollapsedData(this),
|
||||
},
|
||||
method: 'fetchData',
|
||||
successCallback: ({ data }) => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
poll.stop();
|
||||
this.setCollapsedData(data);
|
||||
}
|
||||
successCallback: (response) => {
|
||||
this.headerCheck(response, (data) => this.setCollapsedData(data));
|
||||
},
|
||||
errorCallback: (e) => {
|
||||
poll.stop();
|
||||
|
||||
this.setCollapsedError(e);
|
||||
},
|
||||
});
|
||||
|
||||
poll.makeRequest();
|
||||
},
|
||||
headerCheck(response, callback) {
|
||||
const headers = normalizeHeaders(response.headers);
|
||||
|
||||
if (!headers['POLL-INTERVAL']) {
|
||||
callback(response.data);
|
||||
}
|
||||
},
|
||||
loadCollapsedData() {
|
||||
this.loadingState = LOADING_STATES.collapsedLoading;
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
|
||||
</div>
|
||||
<div v-if="data.modal">
|
||||
<gl-link v-gl-modal="modalId" @click="data.modal.onClick">
|
||||
<gl-link v-gl-modal="modalId" data-testid="modal-link" @click="data.modal.onClick">
|
||||
{{ data.modal.text }}
|
||||
</gl-link>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { uniqueId } from 'lodash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
|
||||
import { EXTENSION_ICONS } from '../../constants';
|
||||
import {
|
||||
summaryTextBuilder,
|
||||
|
@ -7,6 +8,7 @@ import {
|
|||
reportSubTextBuilder,
|
||||
countRecentlyFailedTests,
|
||||
recentFailuresTextBuilder,
|
||||
formatFilePath,
|
||||
} from './utils';
|
||||
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
|
||||
|
||||
|
@ -16,6 +18,7 @@ export default {
|
|||
i18n,
|
||||
expandEvent: 'i_testing_summary_widget_total',
|
||||
props: ['testResultsPath', 'headBlobPath', 'pipeline'],
|
||||
modalComponent: TestCaseDetails,
|
||||
computed: {
|
||||
summary(data) {
|
||||
if (data.parsingInProgress) {
|
||||
|
@ -53,8 +56,11 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.testResultsPath).then(({ data = {}, status }) => {
|
||||
return axios.get(this.testResultsPath).then((res) => {
|
||||
const { data = {}, status } = res;
|
||||
|
||||
return {
|
||||
...res,
|
||||
data: {
|
||||
hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
|
||||
parsingInProgress: status === 204,
|
||||
|
@ -94,8 +100,18 @@ export default {
|
|||
return {
|
||||
id: uniqueId('test-'),
|
||||
header: this.testHeader(test, sectionHeader, index),
|
||||
icon: { name: iconName },
|
||||
modal: {
|
||||
text: test.name,
|
||||
onClick: () => {
|
||||
this.modalData = {
|
||||
testCase: {
|
||||
filePath: test.file && `${this.headBlobPath}/${formatFilePath(test.file)}`,
|
||||
...test,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
icon: { name: iconName },
|
||||
};
|
||||
};
|
||||
},
|
||||
|
|
|
@ -82,3 +82,12 @@ export const countRecentlyFailedTests = (subject) => {
|
|||
})
|
||||
.reduce((total, count) => total + count, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes `./` from the beginning of a file path so it can be appended onto a blob path
|
||||
* @param {String} file
|
||||
* @returns {String} - formatted value
|
||||
*/
|
||||
export const formatFilePath = (file) => {
|
||||
return file.replace(/^\.?\/*/, '');
|
||||
};
|
||||
|
|
|
@ -74,21 +74,11 @@
|
|||
|
||||
> li:not(.empty-message):not(.no-border) {
|
||||
background-color: $white;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: $gl-padding;
|
||||
border-radius: $border-radius-default;
|
||||
border: 1px solid $gray-50;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.prioritized-labels:not(.is-not-draggable) & {
|
||||
box-shadow: 0 1px 2px $issue-boards-card-shadow;
|
||||
cursor: grab;
|
||||
border: 0;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
|
@ -111,11 +101,6 @@
|
|||
width: 109px;
|
||||
}
|
||||
|
||||
.labels-container {
|
||||
border-radius: $border-radius-default;
|
||||
padding: $gl-padding $gl-padding-8;
|
||||
}
|
||||
|
||||
.label-actions-list {
|
||||
list-style: none;
|
||||
flex-shrink: 0;
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
maskable_regex: ci_variable_maskable_regex,
|
||||
protected_by_default: ci_variable_protected_by_default?.to_s,
|
||||
aws_logo_svg_path: image_path('aws_logo.svg'),
|
||||
aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-the-aws-elastic-container-service-ecs'),
|
||||
aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'run-aws-commands-from-gitlab-cicd'),
|
||||
aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'aws'),
|
||||
aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-the-amazon-elastic-container-service-ecs'),
|
||||
aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'use-an-image-to-run-aws-commands'),
|
||||
aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md'),
|
||||
contains_variable_reference_link: help_page_path('ci/variables/index', anchor: 'use-variables-in-other-variables'),
|
||||
protected_environment_variables_link: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables'),
|
||||
masked_environment_variables_link: help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'),
|
||||
|
|
|
@ -4,6 +4,5 @@
|
|||
|
||||
%h1.page-title
|
||||
= _('Edit Label')
|
||||
%hr
|
||||
|
||||
= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
#js-promote-label-modal
|
||||
= render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
|
||||
|
||||
.labels-container.gl-mt-2.gl-bg-gray-10.gl-border-solid.gl-border-1.gl-border-gray-100
|
||||
.labels-container.gl-mt-5
|
||||
- if @labels.any?
|
||||
.text-muted
|
||||
.text-muted.gl-mb-5
|
||||
= _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuable_types.to_sentence }
|
||||
.other-labels
|
||||
%h5= _('Labels')
|
||||
%ul.content-list.manage-labels-list.js-other-labels
|
||||
%h4= _('Labels')
|
||||
%ul.manage-labels-list.js-other-labels
|
||||
= render partial: 'shared/label', collection: @labels, as: :label, locals: { use_label_priority: false, subject: @group }
|
||||
= paginate @labels, theme: 'gitlab'
|
||||
- elsif search.present?
|
||||
|
|
|
@ -4,6 +4,5 @@
|
|||
|
||||
%h1.page-title
|
||||
= _('New Label')
|
||||
%hr
|
||||
|
||||
= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
%h1.page-title
|
||||
= _('Edit Label')
|
||||
%hr
|
||||
|
||||
= render 'shared/labels/form', url: project_label_path(@project, @label), back_path: project_labels_path(@project)
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
#js-promote-label-modal
|
||||
= render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
|
||||
|
||||
.labels-container.gl-mt-3.gl-bg-gray-10.gl-border-solid.gl-border-1.gl-border-gray-100
|
||||
.labels-container.gl-mt-5
|
||||
- if can_admin_label && search.blank?
|
||||
%p.text-muted
|
||||
= _('Labels can be applied to issues and merge requests.')
|
||||
|
@ -19,8 +19,8 @@
|
|||
-# Only show it in the first page
|
||||
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
|
||||
.prioritized-labels.gl-mb-7{ class: [('hide' if hide), ('is-not-draggable' unless can_admin_label)] }
|
||||
%h5.gl-mt-3= _('Prioritized Labels')
|
||||
.content-list.manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
|
||||
%h4.gl-mt-3= _('Prioritized Labels')
|
||||
.manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
|
||||
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
|
||||
= render 'shared/empty_states/priority_labels'
|
||||
- if @prioritized_labels.any?
|
||||
|
@ -31,14 +31,14 @@
|
|||
|
||||
- if @labels.any?
|
||||
.other-labels
|
||||
%h5{ class: ('hide' if hide) }= _('Other Labels')
|
||||
.content-list.manage-labels-list.js-other-labels
|
||||
%h4{ class: ('hide' if hide) }= _('Other Labels')
|
||||
.manage-labels-list.js-other-labels
|
||||
= render partial: 'shared/label', collection: @labels, as: :label, locals: { subject: @project }
|
||||
= paginate @labels, theme: 'gitlab'
|
||||
- elsif search.present?
|
||||
.other-labels
|
||||
- if @available_labels.any?
|
||||
%h5
|
||||
%h4
|
||||
= _('Other Labels')
|
||||
.nothing-here-block
|
||||
= _('No other labels with such name or description')
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
%h1.page-title
|
||||
= _('New Label')
|
||||
%hr
|
||||
|
||||
= render 'shared/labels/form', url: project_labels_path(@project), back_path: project_labels_path(@project)
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
- lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url }
|
||||
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
|
||||
|
||||
#js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
|
||||
- if Feature.enabled?(:pipeline_tabs_vue, @project)
|
||||
#js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline) }
|
||||
- else
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
|
||||
- tooltip_title = label_status_tooltip(label, status) if status
|
||||
|
||||
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
|
||||
%li.label-list-item{ id: label_css_id, class: "gl-p-5 gl-border-b", data: { id: label.id } }
|
||||
= render "shared/label_row", label: label, force_priority: force_priority
|
||||
%ul.label-actions-list
|
||||
- if can?(current_user, :admin_label, @project)
|
||||
|
|
|
@ -2,21 +2,18 @@
|
|||
= form_errors(@label)
|
||||
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :title
|
||||
.col-sm-10
|
||||
= f.text_field :title, class: "gl-form-input form-control js-label-title qa-label-title", required: true, autofocus: true
|
||||
= render_if_exists 'shared/labels/create_label_help_text'
|
||||
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :description
|
||||
.col-sm-10
|
||||
= f.text_field :description, class: "gl-form-input form-control js-quick-submit qa-label-description"
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :color, _("Background color")
|
||||
.col-sm-10
|
||||
.input-group
|
||||
.input-group-prepend
|
||||
.input-group-text.label-color-preview
|
||||
|
@ -26,13 +23,13 @@
|
|||
%br
|
||||
= _("Or you can choose one of the suggested colors below")
|
||||
= render_suggested_colors
|
||||
.gl-display-flex.gl-justify-content-space-between.gl-p-5.gl-bg-gray-10.gl-border-t-solid.gl-border-t-gray-100.gl-border-t-1
|
||||
.gl-display-flex.gl-justify-content-space-between
|
||||
%div
|
||||
- if @label.persisted?
|
||||
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button'
|
||||
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
|
||||
- else
|
||||
= f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button'
|
||||
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel'
|
||||
= f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button gl-mr-2'
|
||||
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
|
||||
- if @label.persisted?
|
||||
- presented_label = @label.present
|
||||
%button.btn.btn-danger.gl-button.btn-danger-secondary.js-delete-label-modal-button{ type: 'button', data: { label_name: presented_label.name, subject_name: presented_label.subject_name, destroy_path: presented_label.destroy_path } }
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/359740
|
|||
milestone: '15.0'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -4,11 +4,10 @@ group: Release
|
|||
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/#assignments
|
||||
---
|
||||
|
||||
# Getting started with Continuous Deployment to AWS Elastic Container Service **(FREE)**
|
||||
# Deploy to Amazon Elastic Container Service **(FREE)**
|
||||
|
||||
This step-by-step guide helps you use [Continuous Deployment to ECS](../index.md#deploy-your-application-to-the-aws-elastic-container-service-ecs)
|
||||
that deploys a project hosted on GitLab.com to [Elastic Container Service](https://aws.amazon.com/ecs/)
|
||||
(ECS) on AWS.
|
||||
This step-by-step guide helps you deploy a project hosted on GitLab.com to
|
||||
the Amazon [Elastic Container Service (ECS)](https://aws.amazon.com/ecs/).
|
||||
|
||||
In this guide, you begin by creating an ECS cluster manually using the AWS console. You create and
|
||||
deploy a simple application that you create from a GitLab template.
|
||||
|
@ -68,7 +67,7 @@ and [Container Registry](../../../user/packages/container_registry/index.md).
|
|||
1. Select **Setup up CI/CD**. It brings you to a `.gitlab-ci.yml`
|
||||
creation form.
|
||||
1. Copy and paste the following content into the empty `.gitlab-ci.yml`. This defines
|
||||
[a pipeline for continuous deployment to ECS](../index.md#deploy-your-application-to-the-aws-elastic-container-service-ecs).
|
||||
a pipeline for continuous deployment to ECS.
|
||||
|
||||
```yaml
|
||||
include:
|
||||
|
|
|
@ -5,165 +5,101 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
type: howto
|
||||
---
|
||||
|
||||
# Cloud deployment **(FREE)**
|
||||
# Deploy to AWS from GitLab CI/CD **(FREE)**
|
||||
|
||||
Interacting with a major cloud provider may have become a much needed task that's
|
||||
part of your delivery process. With GitLab you can
|
||||
[deploy your application anywhere](https://about.gitlab.com/stages-devops-lifecycle/deploy-targets/).
|
||||
GitLab provides Docker images with the libraries and tools you need to deploy
|
||||
to AWS. You can reference these images in your CI/CD pipeline.
|
||||
|
||||
For some specific deployment targets, GitLab makes this process less painful by providing Docker
|
||||
images with the needed libraries and tools pre-installed. By referencing them in your
|
||||
CI/CD pipeline, you can interact with your chosen cloud provider more easily.
|
||||
If you're using GitLab.com and deploying to the [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (ECS),
|
||||
read about [deploying to ECS](ecs/quick_start_guide.md).
|
||||
|
||||
## AWS
|
||||
## Authenticate GitLab with AWS
|
||||
|
||||
GitLab provides Docker images that you can use to [run AWS commands from GitLab CI/CD](#run-aws-commands-from-gitlab-cicd), and a template to make
|
||||
it easier to [deploy to AWS](#deploy-your-application-to-the-aws-elastic-container-service-ecs).
|
||||
To use GitLab CI/CD to connect to AWS, you must authenticate.
|
||||
After you set up authentication, you can configure CI/CD to deploy.
|
||||
|
||||
### Quick start
|
||||
|
||||
If you're using GitLab.com, see the [quick start guide](ecs/quick_start_guide.md)
|
||||
for setting up Continuous Deployment to [AWS Elastic Container Service](https://aws.amazon.com/ecs/) (ECS).
|
||||
|
||||
### Run AWS commands from GitLab CI/CD
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31167) in GitLab 12.6.
|
||||
|
||||
The GitLab AWS Docker image provides the [AWS Command Line Interface](https://aws.amazon.com/cli/),
|
||||
which enables you to run `aws` commands. As part of your deployment strategy, you can run `aws` commands directly from
|
||||
`.gitlab-ci.yml` by specifying the [GitLab AWS Docker image](https://gitlab.com/gitlab-org/cloud-deploy).
|
||||
|
||||
Some credentials are required to be able to run `aws` commands:
|
||||
|
||||
1. Sign up for [an AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-set-up.html) if you don't have one yet.
|
||||
1. Log in onto the console and create [a new IAM user](https://console.aws.amazon.com/iam/home#/home).
|
||||
1. Select your newly created user to access its details. Navigate to **Security credentials > Create a new access key**.
|
||||
|
||||
NOTE:
|
||||
A new **Access key ID** and **Secret access key** are generated. Please take a note of them right away.
|
||||
|
||||
1. In your GitLab project, go to **Settings > CI/CD**. Set the following as
|
||||
[CI/CD variables](../variables/index.md)
|
||||
(see table below):
|
||||
|
||||
- Access key ID.
|
||||
- Secret access key.
|
||||
- Region code. You can check the [list of AWS regional endpoints](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints).
|
||||
You might want to check if the AWS service you intend to use is
|
||||
[available in the chosen region](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/).
|
||||
1. Sign on to your AWS account.
|
||||
1. Create [an IAM user](https://console.aws.amazon.com/iam/home#/home).
|
||||
1. Select your user to access its details. Go to **Security credentials > Create a new access key**.
|
||||
1. Note the **Access key ID** and **Secret access key**.
|
||||
1. In your GitLab project, go to **Settings > CI/CD**. Set the following
|
||||
[CI/CD variables](../variables/index.md):
|
||||
|
||||
| Environment variable name | Value |
|
||||
|:-------------------------------|:-----------------------|
|
||||
| `AWS_ACCESS_KEY_ID` | Your Access key ID |
|
||||
| `AWS_SECRET_ACCESS_KEY` | Your Secret access key |
|
||||
| `AWS_DEFAULT_REGION` | Your region code |
|
||||
|:-------------------------------|:------------------------|
|
||||
| `AWS_ACCESS_KEY_ID` | Your Access key ID. |
|
||||
| `AWS_SECRET_ACCESS_KEY` | Your secret access key. |
|
||||
| `AWS_DEFAULT_REGION` | Your region code. You might want to confirm that the AWS service you intend to use is [available in the chosen region](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/). |
|
||||
|
||||
NOTE:
|
||||
When you create a variable it's set to be [protected by default](../variables/index.md#protected-cicd-variables). If you want to use the `aws` commands on branches or tags that are not protected, make sure to uncheck the **Protect variable** checkbox.
|
||||
1. Variables are [protected by default](../variables/index.md#protected-cicd-variables).
|
||||
To use GitLab CI/CD with branches or tags that are not protected,
|
||||
clear the **Protect variable** checkbox.
|
||||
|
||||
1. You can now use `aws` commands in the `.gitlab-ci.yml` file of this project:
|
||||
## Use an image to run AWS commands
|
||||
|
||||
If an image contains the [AWS Command Line Interface](https://aws.amazon.com/cli/),
|
||||
you can reference the image in your project's `.gitlab-ci.yml` file. Then you can run
|
||||
`aws` commands in your CI/CD jobs.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest # see the note below
|
||||
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
|
||||
script:
|
||||
- aws s3 ...
|
||||
- aws create-deployment ...
|
||||
```
|
||||
|
||||
NOTE:
|
||||
The image used in the example above
|
||||
(`registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest`) is hosted on the [GitLab
|
||||
Container Registry](../../user/packages/container_registry/index.md) and is
|
||||
ready to use. Alternatively, replace the image with one hosted on AWS ECR.
|
||||
GitLab provides a Docker image that includes the AWS CLI:
|
||||
|
||||
### Use an AWS Elastic Container Registry (ECR) image in your CI/CD
|
||||
- Images are hosted in the GitLab Container Registry. The latest image is
|
||||
`registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest`.
|
||||
- [Images are stored in a GitLab repository](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws).
|
||||
|
||||
Instead of referencing an image hosted on the GitLab Registry, you can
|
||||
reference an image hosted on any third-party registry, such as the
|
||||
[Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/).
|
||||
Alternately, you can use an [Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) image.
|
||||
[Learn how to push an image to your ECR repository](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html).
|
||||
|
||||
To do so, [push your image into your ECR
|
||||
repository](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html).
|
||||
Then reference it in your `.gitlab-ci.yml` file and replace the `image`
|
||||
path to point to your ECR image.
|
||||
You can also use an image from any third-party registry.
|
||||
|
||||
### Deploy your application to the AWS Elastic Container Service (ECS)
|
||||
## Deploy your application to the Amazon Elastic Container Service (ECS)
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207962) in GitLab 12.9.
|
||||
> - The `Deploy-ECS.gitlab-ci.yml` template was [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/220821) to `AWS/Deploy-ECS.gitlab-ci.yml` in GitLab 13.2.
|
||||
|
||||
GitLab provides a series of [CI templates that you can include in your project](../yaml/index.md#include).
|
||||
To automate deployments of your application to your [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (AWS ECS)
|
||||
cluster, you can `include` the `AWS/Deploy-ECS.gitlab-ci.yml` template in your `.gitlab-ci.yml` file.
|
||||
You can automate deployments of your application to your [Amazon ECS](https://aws.amazon.com/ecs/)
|
||||
cluster.
|
||||
|
||||
GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `.gitlab-ci.yml` file to simplify working with AWS:
|
||||
Prerequisites:
|
||||
|
||||
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest` to use AWS CLI commands.
|
||||
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest` to deploy your application to AWS ECS.
|
||||
- [Authenticate AWS with GitLab](#authenticate-gitlab-with-aws).
|
||||
- Create a cluster on Amazon ECS.
|
||||
- Create related components, like an ECS service, a database on Amazon RDS, and so on.
|
||||
- Create an ECS task definition, where the value for the `containerDefinitions[].name` attribute is
|
||||
the same as the `Container name` defined in your targeted ECS service. The task definition can be:
|
||||
- An existing task definition in ECS.
|
||||
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/222618),
|
||||
a JSON file in your GitLab project. Use the
|
||||
[template in the AWS documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/create-task-definition.html#task-definition-template)
|
||||
and save the file in your project. For example `<project-root>/ci/aws/task-definition.json`.
|
||||
|
||||
Before getting started with this process, you need a cluster on AWS ECS, as well as related
|
||||
components, like an ECS service, ECS task definition, a database on Amazon RDS, and so on.
|
||||
[Read more about AWS ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html).
|
||||
To deploy to your ECS cluster:
|
||||
|
||||
The ECS task definition can be:
|
||||
1. In your GitLab project, go to **Settings > CI/CD**. Set the following
|
||||
[CI/CD variables](../variables/index.md). You can find these names by
|
||||
selecting the targeted cluster on your [Amazon ECS dashboard](https://console.aws.amazon.com/ecs/home).
|
||||
|
||||
- An existing task definition in AWS ECS
|
||||
- A JSON file containing a task definition. Create the JSON file by using the template provided in
|
||||
the [AWS documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/create-task-definition.html#task-definition-template).
|
||||
Copy the task definition into a new file in your project, for example `<project-root>/ci/aws/task-definition.json`.
|
||||
[Available](https://gitlab.com/gitlab-org/gitlab/-/issues/222618) in GitLab 13.3 and later.
|
||||
|
||||
After you have these prerequisites ready, follow these steps:
|
||||
|
||||
1. Make sure your AWS credentials are set up as CI/CD variables for your
|
||||
project. You can follow [the steps above](#run-aws-commands-from-gitlab-cicd) to complete this setup.
|
||||
1. Add these variables to your project's `.gitlab-ci.yml` file, or in the project's
|
||||
[CI/CD settings](../variables/index.md#custom-cicd-variables):
|
||||
|
||||
- `CI_AWS_ECS_CLUSTER`: The name of the AWS ECS cluster that you're targeting for your deployments.
|
||||
- `CI_AWS_ECS_SERVICE`: The name of the targeted service tied to your AWS ECS cluster.
|
||||
- `CI_AWS_ECS_TASK_DEFINITION`: The name of an existing task definition in ECS tied
|
||||
to the service mentioned above.
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
CI_AWS_ECS_CLUSTER: my-cluster
|
||||
CI_AWS_ECS_SERVICE: my-service
|
||||
CI_AWS_ECS_TASK_DEFINITION: my-task-definition
|
||||
```
|
||||
|
||||
You can find these names after selecting the targeted cluster on your [AWS ECS dashboard](https://console.aws.amazon.com/ecs/home):
|
||||
|
||||
![AWS ECS dashboard](../img/ecs_dashboard_v12_9.png)
|
||||
|
||||
Alternatively, if you want to use a task definition defined in a JSON file, use
|
||||
`CI_AWS_ECS_TASK_DEFINITION_FILE` instead:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
CI_AWS_ECS_CLUSTER: my-cluster
|
||||
CI_AWS_ECS_SERVICE: my-service
|
||||
CI_AWS_ECS_TASK_DEFINITION_FILE: ci/aws/my_task_definition.json
|
||||
```
|
||||
|
||||
You can create your `CI_AWS_ECS_TASK_DEFINITION_FILE` variable as a
|
||||
[file-typed CI/CD variable](../variables/index.md#cicd-variable-types) instead of a
|
||||
regular CI/CD variable. If you choose to do so, set the variable value to be the full contents of
|
||||
the JSON task definition. You can then remove the JSON file from your project.
|
||||
|
||||
In both cases, make sure that the value for the `containerDefinitions[].name` attribute is
|
||||
the same as the `Container name` defined in your targeted ECS service.
|
||||
| Environment variable name | Value |
|
||||
|:-------------------------------|:------------------------|
|
||||
| `CI_AWS_ECS_CLUSTER` | The name of the AWS ECS cluster that you're targeting for your deployments. |
|
||||
| `CI_AWS_ECS_SERVICE` | The name of the targeted service tied to your AWS ECS cluster. |
|
||||
| `CI_AWS_ECS_TASK_DEFINITION` | If the task definition is in ECS, the name of the task definition tied to the service. |
|
||||
| `CI_AWS_ECS_TASK_DEFINITION_FILE` | If the task definition is a JSON file in GitLab, the filename, including the path. For example, `ci/aws/my_task_definition.json`. If the name of the task definition in your JSON file is the same name as an existing task definition in ECS, then a new revision is created when CI/CD runs. Otherwise, a brand new task definition is created, starting at revision 1. |
|
||||
|
||||
WARNING:
|
||||
`CI_AWS_ECS_TASK_DEFINITION_FILE` takes precedence over `CI_AWS_ECS_TASK_DEFINITION` if both these
|
||||
variables are defined within your project.
|
||||
|
||||
NOTE:
|
||||
If the name of the task definition you wrote in your JSON file is the same name
|
||||
as an existing task definition on AWS, then a new revision is created for it.
|
||||
Otherwise, a brand new task definition is created, starting at revision 1.
|
||||
If you define both `CI_AWS_ECS_TASK_DEFINITION_FILE` and `CI_AWS_ECS_TASK_DEFINITION`,
|
||||
`CI_AWS_ECS_TASK_DEFINITION_FILE` takes precedence.
|
||||
|
||||
1. Include this template in `.gitlab-ci.yml`:
|
||||
|
||||
|
@ -172,16 +108,16 @@ After you have these prerequisites ready, follow these steps:
|
|||
- template: AWS/Deploy-ECS.gitlab-ci.yml
|
||||
```
|
||||
|
||||
The `AWS/Deploy-ECS` template ships with GitLab and is available [on
|
||||
GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml).
|
||||
The `AWS/Deploy-ECS` template ships with GitLab and is available
|
||||
[on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml).
|
||||
|
||||
1. Commit and push your updated `.gitlab-ci.yml` to your project's repository, and you're done!
|
||||
1. Commit and push your updated `.gitlab-ci.yml` to your project's repository.
|
||||
|
||||
Your application Docker image is rebuilt and pushed to the GitLab registry.
|
||||
Your application Docker image is rebuilt and pushed to the GitLab Container Registry.
|
||||
If your image is located in a private registry, make sure your task definition is
|
||||
[configured with a `repositoryCredentials` attribute](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html).
|
||||
|
||||
Then the targeted task definition is updated with the location of the new
|
||||
The targeted task definition is updated with the location of the new
|
||||
Docker image, and a new revision is created in ECS as result.
|
||||
|
||||
Finally, your AWS ECS service is updated with the new revision of the
|
||||
|
@ -190,28 +126,14 @@ After you have these prerequisites ready, follow these steps:
|
|||
|
||||
WARNING:
|
||||
The [`AWS/Deploy-ECS.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml)
|
||||
template includes both the [`Jobs/Build.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml)
|
||||
and [`Jobs/Deploy/ECS.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml)
|
||||
"sub-templates". Do not include these "sub-templates" on their own, and only include the main
|
||||
`AWS/Deploy-ECS.gitlab-ci.yml` template. The "sub-templates" are designed to only be
|
||||
used along with the main template. They may move or change unexpectedly causing your
|
||||
pipeline to fail if you didn't include the main template. Also, the job names within
|
||||
these templates may change. Do not override these jobs names in your own pipeline,
|
||||
as the override stops working when the name changes.
|
||||
template includes two templates: [`Jobs/Build.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml)
|
||||
and [`Jobs/Deploy/ECS.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml). Do not include these templates on their own. Only include the
|
||||
`AWS/Deploy-ECS.gitlab-ci.yml` template. These other templates are designed to be
|
||||
used only with the main template. They may move or change unexpectedly. Also, the job names within
|
||||
these templates may change. Do not override these job names in your own pipeline,
|
||||
because the override stops working when the name changes.
|
||||
|
||||
Alternatively, if you don't wish to use the `AWS/Deploy-ECS.gitlab-ci.yml` template
|
||||
to deploy to AWS ECS, you can always use our
|
||||
`aws-base` Docker image to run your own [AWS CLI commands for ECS](https://docs.aws.amazon.com/cli/latest/reference/ecs/index.html#cli-aws-ecs).
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
|
||||
script:
|
||||
- aws ecs register-task-definition ...
|
||||
```
|
||||
|
||||
### Provision and deploy to your AWS Elastic Compute Cloud (EC2)
|
||||
## Deploy your application to Amazon EC2
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201742) in GitLab 13.5.
|
||||
|
||||
|
@ -224,7 +146,7 @@ following actions within the same pipeline:
|
|||
|
||||
![CF-Provision-and-Deploy-EC2 diagram](../img/cf_ec2_diagram_v13_5.png)
|
||||
|
||||
#### Run the `AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml` template
|
||||
### Run the `AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml` template
|
||||
|
||||
To run the `AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml` template, you must
|
||||
pass three JSON input objects, based on existing templates:
|
||||
|
@ -289,12 +211,12 @@ When running your project pipeline at this point:
|
|||
on the related JSON object's content. The deployment job finishes whenever the deployment to EC2
|
||||
is done or has failed.
|
||||
|
||||
#### Custom build job for Auto DevOps
|
||||
### Custom build job for Auto DevOps
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216008) in GitLab 13.6.
|
||||
|
||||
To leverage [Auto DevOps](../../topics/autodevops/index.md) for your project when deploying to
|
||||
AWS EC2, first you must define [your AWS credentials as CI/CD variables](#run-aws-commands-from-gitlab-cicd).
|
||||
AWS EC2, first you must define [your AWS credentials as CI/CD variables](#authenticate-gitlab-with-aws).
|
||||
|
||||
Next, define a job for the `build` stage. To do so, you must reference the
|
||||
`Auto-DevOps.gitlab-ci.yml` template and include a job named `build_artifact` in your
|
||||
|
@ -323,8 +245,4 @@ For a video walkthrough of this configuration process, see [Auto Deploy to EC2](
|
|||
|
||||
### Deploy to Amazon EKS
|
||||
|
||||
- [How to deploy your application to a GitLab-managed Amazon EKS cluster with Auto DevOps](https://about.gitlab.com/blog/2020/05/05/deploying-application-eks/)
|
||||
|
||||
## Deploy to Google Cloud
|
||||
|
||||
- [Deploying with GitLab on Google Cloud](https://about.gitlab.com/partners/technology-partners/google-cloud-platform/)
|
||||
- [Deploy your application to a GitLab-managed Amazon EKS cluster with Auto DevOps](https://about.gitlab.com/blog/2020/05/05/deploying-application-eks/)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 38 KiB |
|
@ -191,7 +191,7 @@ To do so, follow these steps:
|
|||
- `ECS` if you're not enforcing any launch type check when deploying to ECS.
|
||||
|
||||
When you trigger a pipeline, if you have Auto DevOps enabled and if you have correctly
|
||||
[entered AWS credentials as variables](../../ci/cloud_deployment/index.md#deploy-your-application-to-the-aws-elastic-container-service-ecs),
|
||||
[entered AWS credentials as variables](../../ci/cloud_deployment/index.md#authenticate-gitlab-with-aws),
|
||||
your application is deployed to AWS ECS.
|
||||
|
||||
If you have both a valid `AUTO_DEVOPS_PLATFORM_TARGET` variable and a Kubernetes cluster tied to your project,
|
||||
|
|
|
@ -475,12 +475,6 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{codeStart}type%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stage%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{codeStart}types%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stages%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
|
||||
msgstr ""
|
||||
|
||||
|
@ -16349,9 +16343,6 @@ msgstr ""
|
|||
msgid "Found errors in your .gitlab-ci.yml:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Found warning in your .gitlab-ci.yml"
|
||||
msgstr ""
|
||||
|
||||
msgid "Framework successfully deleted"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -135,14 +135,14 @@ describe('Comment Field Layout Component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('issue has email participants, but note is confidential', () => {
|
||||
describe('issue has email participants, but note is internal', () => {
|
||||
it('does not show EmailParticipantsWarning', () => {
|
||||
createWrapper({
|
||||
noteableData: {
|
||||
...noteableDataMock,
|
||||
issue_email_participants: [{ email: 'someone@gitlab.com' }],
|
||||
},
|
||||
noteIsConfidential: true,
|
||||
isInternalNote: true,
|
||||
});
|
||||
|
||||
expect(findEmailParticipantsWarning().exists()).toBe(false);
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('issue_comment_form component', () => {
|
|||
const findTextArea = () => wrapper.findByTestId('comment-field');
|
||||
const findAddToReviewButton = () => wrapper.findByTestId('add-to-review-button');
|
||||
const findAddCommentNowButton = () => wrapper.findByTestId('add-comment-now-button');
|
||||
const findConfidentialNoteCheckbox = () => wrapper.findByTestId('confidential-note-checkbox');
|
||||
const findConfidentialNoteCheckbox = () => wrapper.findByTestId('internal-note-checkbox');
|
||||
const findCommentTypeDropdown = () => wrapper.findComponent(CommentTypeDropdown);
|
||||
const findCommentButton = () => findCommentTypeDropdown().find('button');
|
||||
const findErrorAlerts = () => wrapper.findAllComponents(GlAlert).wrappers;
|
||||
|
@ -249,15 +249,15 @@ describe('issue_comment_form component', () => {
|
|||
describe('textarea', () => {
|
||||
describe('general', () => {
|
||||
it.each`
|
||||
noteType | confidential | placeholder
|
||||
noteType | noteIsInternal | placeholder
|
||||
${'comment'} | ${false} | ${'Write a comment or drag your files here…'}
|
||||
${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
|
||||
`(
|
||||
'should render textarea with placeholder for $noteType',
|
||||
({ confidential, placeholder }) => {
|
||||
({ noteIsInternal, placeholder }) => {
|
||||
mountComponent({
|
||||
mountFunction: mount,
|
||||
initialData: { noteIsConfidential: confidential },
|
||||
initialData: { noteIsInternal },
|
||||
});
|
||||
|
||||
expect(findTextArea().attributes('placeholder')).toBe(placeholder);
|
||||
|
@ -389,14 +389,14 @@ describe('issue_comment_form component', () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
confidential | buttonText
|
||||
noteIsInternal | buttonText
|
||||
${false} | ${'Comment'}
|
||||
${true} | ${'Add internal note'}
|
||||
`('renders comment button with text "$buttonText"', ({ confidential, buttonText }) => {
|
||||
`('renders comment button with text "$buttonText"', ({ noteIsInternal, buttonText }) => {
|
||||
mountComponent({
|
||||
mountFunction: mount,
|
||||
noteableData: createNotableDataMock({ confidential }),
|
||||
initialData: { noteIsConfidential: confidential },
|
||||
noteableData: createNotableDataMock({ confidential: noteIsInternal }),
|
||||
initialData: { noteIsInternal },
|
||||
});
|
||||
|
||||
expect(findCommentButton().text()).toBe(buttonText);
|
||||
|
|
|
@ -7,7 +7,7 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue';
|
|||
import NoteForm from '~/notes/components/note_form.vue';
|
||||
import createStore from '~/notes/stores';
|
||||
import notes from '~/notes/stores/modules/index';
|
||||
import { CONFIDENTIAL_CLASSES } from '~/notes/constants';
|
||||
import { INTERNAL_NOTE_CLASSES } from '~/notes/constants';
|
||||
|
||||
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
|
||||
|
||||
|
@ -59,20 +59,20 @@ describe('issue_note_body component', () => {
|
|||
expect(wrapper.findComponent(NoteAwardsList).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not have confidential classes', () => {
|
||||
expect(wrapper.findByTestId('note-confidential-container').classes()).not.toEqual(
|
||||
CONFIDENTIAL_CLASSES,
|
||||
it('should not have internal note classes', () => {
|
||||
expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual(
|
||||
INTERNAL_NOTE_CLASSES,
|
||||
);
|
||||
});
|
||||
|
||||
describe('isConfidential', () => {
|
||||
describe('isInternalNote', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ props: { isConfidential: true } });
|
||||
wrapper = createComponent({ props: { isInternalNote: true } });
|
||||
});
|
||||
|
||||
it('should have confidential classes', () => {
|
||||
expect(wrapper.findByTestId('note-confidential-container').classes()).toEqual(
|
||||
CONFIDENTIAL_CLASSES,
|
||||
it('should have internal note classes', () => {
|
||||
expect(wrapper.findByTestId('note-internal-container').classes()).toEqual(
|
||||
INTERNAL_NOTE_CLASSES,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -106,14 +106,14 @@ describe('issue_note_body component', () => {
|
|||
expect(wrapper.vm.autosave.key).toEqual(autosaveKey);
|
||||
});
|
||||
|
||||
describe('isConfidential', () => {
|
||||
describe('isInternalNote', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ isConfidential: true });
|
||||
wrapper.setProps({ isInternalNote: true });
|
||||
});
|
||||
|
||||
it('should not have confidential classes', () => {
|
||||
expect(wrapper.findByTestId('note-confidential-container').classes()).not.toEqual(
|
||||
CONFIDENTIAL_CLASSES,
|
||||
it('should not have internal note classes', () => {
|
||||
expect(wrapper.findByTestId('note-internal-container').classes()).not.toEqual(
|
||||
INTERNAL_NOTE_CLASSES,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('NoteHeader component', () => {
|
|||
const findActionText = () => wrapper.find({ ref: 'actionText' });
|
||||
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
|
||||
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
|
||||
const findConfidentialIndicator = () => wrapper.findByTestId('internalNoteIndicator');
|
||||
const findInternalNoteIndicator = () => wrapper.findByTestId('internalNoteIndicator');
|
||||
const findSpinner = () => wrapper.find({ ref: 'spinner' });
|
||||
const findAuthorStatus = () => wrapper.find({ ref: 'authorStatus' });
|
||||
|
||||
|
@ -283,20 +283,20 @@ describe('NoteHeader component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with confidentiality indicator', () => {
|
||||
describe('with internal note badge', () => {
|
||||
it.each`
|
||||
status | condition
|
||||
${true} | ${'shows'}
|
||||
${false} | ${'hides'}
|
||||
`('$condition icon indicator when isConfidential is $status', ({ status }) => {
|
||||
createComponent({ isConfidential: status });
|
||||
expect(findConfidentialIndicator().exists()).toBe(status);
|
||||
`('$condition badge when isInternalNote is $status', ({ status }) => {
|
||||
createComponent({ isInternalNote: status });
|
||||
expect(findInternalNoteIndicator().exists()).toBe(status);
|
||||
});
|
||||
|
||||
it('shows confidential indicator tooltip for project context', () => {
|
||||
createComponent({ isConfidential: true, noteableType: 'issue' });
|
||||
it('shows internal note badge tooltip for project context', () => {
|
||||
createComponent({ isInternalNote: true, noteableType: 'issue' });
|
||||
|
||||
expect(findConfidentialIndicator().attributes('title')).toBe(
|
||||
expect(findInternalNoteIndicator().attributes('title')).toBe(
|
||||
'This internal note will always remain confidential',
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { GlAlert, GlSprintf } from '@gitlab/ui';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import DeprecatedTypeKeywordNotification from '~/pipelines/components/notification/deprecated_type_keyword_notification.vue';
|
||||
import getPipelineWarnings from '~/pipelines/graphql/queries/get_pipeline_warnings.query.graphql';
|
||||
import {
|
||||
mockWarningsWithoutDeprecation,
|
||||
mockWarningsRootType,
|
||||
mockWarningsType,
|
||||
mockWarningsTypesAll,
|
||||
} from './mock_data';
|
||||
|
||||
const defaultProvide = {
|
||||
deprecatedKeywordsDocPath: '/help/ci/yaml/index.md#deprecated-keywords',
|
||||
fullPath: '/namespace/my-project',
|
||||
pipelineIid: 4,
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
|
||||
const mockWarnings = jest.fn();
|
||||
|
||||
const createComponent = ({ isLoading = false, options = {} } = {}) => {
|
||||
return shallowMount(DeprecatedTypeKeywordNotification, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
provide: {
|
||||
...defaultProvide,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
queries: {
|
||||
warnings: {
|
||||
loading: isLoading,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const createComponentWithApollo = () => {
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
const handlers = [[getPipelineWarnings, mockWarnings]];
|
||||
const mockApollo = createMockApollo(handlers);
|
||||
|
||||
return createComponent({
|
||||
options: {
|
||||
localVue,
|
||||
apolloProvider: mockApollo,
|
||||
mocks: {},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findAlertItems = () => findAlert().findAll('li');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('Deprecated keyword notification', () => {
|
||||
describe('while loading the pipeline warnings', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ isLoading: true });
|
||||
});
|
||||
|
||||
it('does not display the notification', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if there is an error in the query', () => {
|
||||
beforeEach(async () => {
|
||||
mockWarnings.mockResolvedValue({ errors: ['It didnt work'] });
|
||||
wrapper = createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not display the notification', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a valid query result', () => {
|
||||
describe('if there are no deprecation warnings', () => {
|
||||
beforeEach(async () => {
|
||||
mockWarnings.mockResolvedValue(mockWarningsWithoutDeprecation);
|
||||
wrapper = createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
});
|
||||
it('does not show the notification', () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a root type deprecation message', () => {
|
||||
beforeEach(async () => {
|
||||
mockWarnings.mockResolvedValue(mockWarningsRootType);
|
||||
wrapper = createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
});
|
||||
it('shows the notification with one item', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlertItems()).toHaveLength(1);
|
||||
expect(findAlertItems().at(0).text()).toContain('types');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a job type deprecation message', () => {
|
||||
beforeEach(async () => {
|
||||
mockWarnings.mockResolvedValue(mockWarningsType);
|
||||
wrapper = createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
});
|
||||
it('shows the notification with one item', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlertItems()).toHaveLength(1);
|
||||
expect(findAlertItems().at(0).text()).toContain('type');
|
||||
expect(findAlertItems().at(0).text()).not.toContain('types');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with both the root types and job type deprecation message', () => {
|
||||
beforeEach(async () => {
|
||||
mockWarnings.mockResolvedValue(mockWarningsTypesAll);
|
||||
wrapper = createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
});
|
||||
it('shows the notification with two items', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlertItems()).toHaveLength(2);
|
||||
expect(findAlertItems().at(0).text()).toContain('types');
|
||||
expect(findAlertItems().at(1).text()).toContain('type');
|
||||
expect(findAlertItems().at(1).text()).not.toContain('types');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { GlModal, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
|
||||
|
@ -9,6 +9,8 @@ describe('Test case details', () => {
|
|||
const defaultTestCase = {
|
||||
classname: 'spec.test_spec',
|
||||
name: 'Test#something cool',
|
||||
file: '~/index.js',
|
||||
filePath: '/src/javascripts/index.js',
|
||||
formattedTime: '10.04ms',
|
||||
recent_failures: {
|
||||
count: 2,
|
||||
|
@ -19,6 +21,8 @@ describe('Test case details', () => {
|
|||
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findName = () => wrapper.findByTestId('test-case-name');
|
||||
const findFile = () => wrapper.findByTestId('test-case-file');
|
||||
const findFileLink = () => wrapper.findComponent(GlLink);
|
||||
const findDuration = () => wrapper.findByTestId('test-case-duration');
|
||||
const findRecentFailures = () => wrapper.findByTestId('test-case-recent-failures');
|
||||
const findAttachmentUrl = () => wrapper.findByTestId('test-case-attachment-url');
|
||||
|
@ -57,11 +61,26 @@ describe('Test case details', () => {
|
|||
expect(findName().text()).toBe(defaultTestCase.name);
|
||||
});
|
||||
|
||||
it('renders the test case file', () => {
|
||||
expect(findFile().text()).toBe(defaultTestCase.file);
|
||||
expect(findFileLink().attributes('href')).toBe(defaultTestCase.filePath);
|
||||
});
|
||||
|
||||
it('renders the test case duration', () => {
|
||||
expect(findDuration().text()).toBe(defaultTestCase.formattedTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when test case has execution time instead of formatted time', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ ...defaultTestCase, formattedTime: null, execution_time: 17 });
|
||||
});
|
||||
|
||||
it('renders the test case duration', () => {
|
||||
expect(findDuration().text()).toBe('17 s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when test case has recent failures', () => {
|
||||
describe('has only 1 recent failure', () => {
|
||||
it('renders the recent failure', () => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
|
||||
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
|
||||
|
||||
import { failedReport } from 'jest/reports/mock_data/mock_data';
|
||||
import mixedResultsTestReports from 'jest/reports/mock_data/new_and_fixed_failures_report.json';
|
||||
|
@ -39,6 +40,7 @@ describe('Test report extension', () => {
|
|||
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
|
||||
const findTertiaryButton = () => wrapper.find(GlButton);
|
||||
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
|
||||
const findModal = () => wrapper.find(TestCaseDetails);
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = mountExtended(extensionsContainer, {
|
||||
|
@ -190,4 +192,19 @@ describe('Test report extension', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal link', () => {
|
||||
beforeEach(async () => {
|
||||
await createExpandedWidgetWithData();
|
||||
|
||||
wrapper.findByTestId('modal-link').trigger('click');
|
||||
});
|
||||
|
||||
it('opens a modal to display test case details', () => {
|
||||
expect(findModal().exists()).toBe(true);
|
||||
expect(findModal().props('testCase')).toMatchObject(
|
||||
mixedResultsTestReports.suites[0].new_failures[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -134,16 +134,13 @@ describe('Terraform extension', () => {
|
|||
|
||||
describe('polling', () => {
|
||||
let pollRequest;
|
||||
let pollStop;
|
||||
|
||||
beforeEach(() => {
|
||||
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
|
||||
pollStop = jest.spyOn(Poll.prototype, 'stop');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
pollRequest.mockRestore();
|
||||
pollStop.mockRestore();
|
||||
});
|
||||
|
||||
describe('successful poll', () => {
|
||||
|
@ -155,7 +152,6 @@ describe('Terraform extension', () => {
|
|||
|
||||
it('does not make additional requests after poll is successful', () => {
|
||||
expect(pollRequest).toHaveBeenCalledTimes(1);
|
||||
expect(pollStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -171,7 +167,6 @@ describe('Terraform extension', () => {
|
|||
|
||||
it('does not make additional requests after poll is unsuccessful', () => {
|
||||
expect(pollRequest).toHaveBeenCalledTimes(1);
|
||||
expect(pollStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -983,27 +983,21 @@ describe('MrWidgetOptions', () => {
|
|||
|
||||
describe('mock polling extension', () => {
|
||||
let pollRequest;
|
||||
let pollStop;
|
||||
|
||||
const findWidgetTestExtension = () => wrapper.find('[data-testid="widget-extension"]');
|
||||
|
||||
beforeEach(() => {
|
||||
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
|
||||
pollStop = jest.spyOn(Poll.prototype, 'stop');
|
||||
|
||||
registeredExtensions.extensions = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
pollRequest.mockRestore();
|
||||
pollStop.mockRestore();
|
||||
|
||||
registeredExtensions.extensions = [];
|
||||
});
|
||||
|
||||
describe('success - multi polling', () => {
|
||||
const findWidgetTestExtension = () => wrapper.find('[data-testid="widget-extension"]');
|
||||
|
||||
// Clear all left-over timeouts that may be registered in the poll class
|
||||
afterEach(() => {
|
||||
let id = window.setTimeout(() => {}, 0);
|
||||
|
||||
while (id > 0) {
|
||||
|
@ -1012,6 +1006,7 @@ describe('MrWidgetOptions', () => {
|
|||
}
|
||||
});
|
||||
|
||||
describe('success - multi polling', () => {
|
||||
it('sets data when polling is complete', async () => {
|
||||
registerExtension(
|
||||
multiPollingExtension([
|
||||
|
@ -1052,21 +1047,34 @@ describe('MrWidgetOptions', () => {
|
|||
);
|
||||
|
||||
await createComponent();
|
||||
expect(findWidgetTestExtension().html()).toContain('Loading...');
|
||||
expect(findWidgetTestExtension().html()).toContain('Test extension loading...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
beforeEach(() => {
|
||||
it('does not make additional requests after poll is successful', async () => {
|
||||
registerExtension(pollingExtension);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('does not make additional requests after poll is successful', () => {
|
||||
await createComponent();
|
||||
// called two times due to parent component polling (mount) and extension polling
|
||||
expect(pollRequest).toHaveBeenCalledTimes(2);
|
||||
expect(pollStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps polling when poll-interval header is provided', async () => {
|
||||
registerExtension({
|
||||
...pollingExtension,
|
||||
methods: {
|
||||
...pollingExtension.methods,
|
||||
fetchCollapsedData() {
|
||||
return Promise.resolve({
|
||||
data: {},
|
||||
headers: { 'poll-interval': 1 },
|
||||
status: 204,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
await createComponent();
|
||||
expect(findWidgetTestExtension().html()).toContain('Test extension loading...');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1084,7 +1092,6 @@ describe('MrWidgetOptions', () => {
|
|||
it('does not make additional requests after poll has failed', () => {
|
||||
// called two times due to parent component polling (mount) and extension polling
|
||||
expect(pollRequest).toHaveBeenCalledTimes(2);
|
||||
expect(pollStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('captures sentry error and displays error when poll has failed', () => {
|
||||
|
|
|
@ -4,11 +4,14 @@ export const workingExtension = (shouldCollapse = true) => ({
|
|||
name: 'WidgetTestExtension',
|
||||
props: ['targetProjectFullPath'],
|
||||
expandEvent: 'test_expand_event',
|
||||
i18n: {
|
||||
loading: 'Test extension loading...',
|
||||
},
|
||||
computed: {
|
||||
summary({ count, targetProjectFullPath }) {
|
||||
summary({ count, targetProjectFullPath } = {}) {
|
||||
return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
|
||||
},
|
||||
statusIcon({ count }) {
|
||||
statusIcon({ count } = {}) {
|
||||
return count > 0 ? EXTENSION_ICONS.warning : EXTENSION_ICONS.success;
|
||||
},
|
||||
shouldCollapse() {
|
||||
|
@ -108,7 +111,10 @@ export const pollingExtension = {
|
|||
|
||||
export const multiPollingExtension = (endpointsToBePolled) => ({
|
||||
name: 'WidgetTestMultiPollingExtension',
|
||||
props: ['targetProjectFullPath'],
|
||||
props: [],
|
||||
i18n: {
|
||||
loading: 'Test extension loading...',
|
||||
},
|
||||
computed: {
|
||||
summary(data) {
|
||||
return `Multi polling test extension reports: ${data?.[0]?.reports}, count: ${data.length}`;
|
||||
|
|
Loading…
Reference in a new issue