Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-16 21:12:05 +00:00
parent c1436508fa
commit f9e0126cad
52 changed files with 994 additions and 236 deletions

View File

@ -2606,7 +2606,6 @@ Style/OpenStructUse:
- 'spec/models/design_management/design_at_version_spec.rb' - 'spec/models/design_management/design_at_version_spec.rb'
- 'spec/models/user_spec.rb' - 'spec/models/user_spec.rb'
- 'spec/presenters/packages/nuget/search_results_presenter_spec.rb' - 'spec/presenters/packages/nuget/search_results_presenter_spec.rb'
- 'spec/requests/api/graphql/mutations/design_management/delete_spec.rb'
- 'spec/requests/api/import_github_spec.rb' - 'spec/requests/api/import_github_spec.rb'
- 'spec/services/packages/nuget/metadata_extraction_service_spec.rb' - 'spec/services/packages/nuget/metadata_extraction_service_spec.rb'
- 'spec/services/projects/import_service_spec.rb' - 'spec/services/projects/import_service_spec.rb'

View File

@ -2,6 +2,7 @@
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue'; import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
import { import {
@ -177,16 +178,16 @@ export default {
'saveDiffDiscussion', 'saveDiffDiscussion',
'setSuggestPopoverDismissed', 'setSuggestPopoverDismissed',
]), ]),
handleCancelCommentForm(shouldConfirm, isDirty) { async handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) { if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
// eslint-disable-next-line no-alert const confirmed = await confirmAction(msg);
if (!window.confirm(msg)) {
if (!confirmed) {
return; return;
} }
} }
this.cancelCommentForm({ this.cancelCommentForm({
lineCode: this.line.line_code, lineCode: this.line.line_code,
fileHash: this.diffFileHash, fileHash: this.diffFileHash,

View File

@ -1,25 +1,9 @@
import Vue from 'vue'; import Vue from 'vue';
export function confirmViaGlModal(message, element) { export function confirmAction(message, { primaryBtnVariant, primaryBtnText } = {}) {
return new Promise((resolve) => { return new Promise((resolve) => {
let confirmed = false; let confirmed = false;
const props = {};
const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
if (confirmBtnVariant) {
props.primaryVariant = confirmBtnVariant;
}
const screenReaderText =
element.querySelector('.gl-sr-only')?.textContent ||
element.querySelector('.sr-only')?.textContent ||
element.getAttribute('aria-label');
if (screenReaderText) {
props.primaryText = screenReaderText;
}
const component = new Vue({ const component = new Vue({
components: { components: {
ConfirmModal: () => import('./confirm_modal.vue'), ConfirmModal: () => import('./confirm_modal.vue'),
@ -28,7 +12,10 @@ export function confirmViaGlModal(message, element) {
return h( return h(
'confirm-modal', 'confirm-modal',
{ {
props, props: {
primaryVariant: primaryBtnVariant,
primaryText: primaryBtnText,
},
on: { on: {
confirmed() { confirmed() {
confirmed = true; confirmed = true;
@ -45,3 +32,24 @@ export function confirmViaGlModal(message, element) {
}).$mount(); }).$mount();
}); });
} }
export function confirmViaGlModal(message, element) {
const primaryBtnConfig = {};
const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
if (confirmBtnVariant) {
primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
}
const screenReaderText =
element.querySelector('.gl-sr-only')?.textContent ||
element.querySelector('.sr-only')?.textContent ||
element.getAttribute('aria-label');
if (screenReaderText) {
primaryBtnConfig.primaryBtnText = screenReaderText;
}
return confirmAction(message, primaryBtnConfig);
}

View File

@ -36,6 +36,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
scrollToCommitForm: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
@ -52,6 +57,13 @@ export default {
return !(this.message && this.targetBranch); return !(this.message && this.targetBranch);
}, },
}, },
watch: {
scrollToCommitForm(flag) {
if (flag) {
this.scrollIntoView();
}
},
},
methods: { methods: {
onSubmit() { onSubmit() {
this.$emit('submit', { this.$emit('submit', {
@ -63,6 +75,10 @@ export default {
onReset() { onReset() {
this.$emit('cancel'); this.$emit('cancel');
}, },
scrollIntoView() {
this.$el.scrollIntoView({ behavior: 'smooth' });
this.$emit('scrolled-to-commit-form');
},
}, },
i18n: { i18n: {
commitMessage: __('Commit message'), commitMessage: __('Commit message'),

View File

@ -45,6 +45,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
scrollToCommitForm: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
@ -146,6 +151,8 @@ export default {
:current-branch="currentBranch" :current-branch="currentBranch"
:default-message="defaultCommitMessage" :default-message="defaultCommitMessage"
:is-saving="isSaving" :is-saving="isSaving"
:scroll-to-commit-form="scrollToCommitForm"
v-on="$listeners"
@cancel="onCommitCancel" @cancel="onCommitCancel"
@submit="onCommitSubmit" @submit="onCommitSubmit"
/> />

View File

@ -2,6 +2,7 @@
import { GlButton, GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { experiment } from '~/experimentation/utils';
import { DRAWER_EXPANDED_KEY } from '../../constants'; import { DRAWER_EXPANDED_KEY } from '../../constants';
import FirstPipelineCard from './cards/first_pipeline_card.vue'; import FirstPipelineCard from './cards/first_pipeline_card.vue';
import GettingStartedCard from './cards/getting_started_card.vue'; import GettingStartedCard from './cards/getting_started_card.vue';
@ -53,12 +54,23 @@ export default {
}, },
methods: { methods: {
setInitialExpandState() { setInitialExpandState() {
let isExpanded;
experiment('pipeline_editor_walkthrough', {
control: () => {
isExpanded = true;
},
candidate: () => {
isExpanded = false;
},
});
// We check in the local storage and if no value is defined, we want the default // We check in the local storage and if no value is defined, we want the default
// to be true. We want to explicitly set it to true here so that the drawer // to be true. We want to explicitly set it to true here so that the drawer
// animates to open on load. // animates to open on load.
const localValue = localStorage.getItem(this.$options.localDrawerKey); const localValue = localStorage.getItem(this.$options.localDrawerKey);
if (localValue === null) { if (localValue === null) {
this.isExpanded = true; this.isExpanded = isExpanded;
} }
}, },
setTopPosition() { setTopPosition() {

View File

@ -112,7 +112,7 @@ export default {
isBranchesLoading() { isBranchesLoading() {
return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches; return this.$apollo.queries.availableBranches.loading || this.isSearchingBranches;
}, },
showBranchSwitcher() { enableBranchSwitcher() {
return this.branches.length > 0 || this.searchTerm.length > 0; return this.branches.length > 0 || this.searchTerm.length > 0;
}, },
}, },
@ -230,11 +230,11 @@ export default {
<template> <template>
<gl-dropdown <gl-dropdown
v-if="showBranchSwitcher"
v-gl-tooltip.hover v-gl-tooltip.hover
:title="$options.i18n.dropdownHeader" :title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader" :header-text="$options.i18n.dropdownHeader"
:text="currentBranch" :text="currentBranch"
:disabled="!enableBranchSwitcher"
icon="branch" icon="branch"
data-qa-selector="branch_selector_button" data-qa-selector="branch_selector_button"
data-testid="branch-selector" data-testid="branch-selector"

View File

@ -4,6 +4,7 @@ import { s__ } from '~/locale';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import { import {
CREATE_TAB, CREATE_TAB,
EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_EMPTY,
@ -22,6 +23,7 @@ import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue'; import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue'; import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue'; import EditorTab from './ui/editor_tab.vue';
import WalkthroughPopover from './walkthrough_popover.vue';
export default { export default {
i18n: { i18n: {
@ -63,6 +65,8 @@ export default {
GlTabs, GlTabs,
PipelineGraph, PipelineGraph,
TextEditor, TextEditor,
GitlabExperiment,
WalkthroughPopover,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
@ -79,6 +83,10 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
isNewCiConfigFile: {
type: Boolean,
required: true,
},
}, },
apollo: { apollo: {
appStatus: { appStatus: {
@ -136,11 +144,17 @@ export default {
> >
<editor-tab <editor-tab
class="gl-mb-3" class="gl-mb-3"
title-link-class="js-walkthrough-popover-target"
:title="$options.i18n.tabEdit" :title="$options.i18n.tabEdit"
lazy lazy
data-testid="editor-tab" data-testid="editor-tab"
@click="setCurrentTab($options.tabConstants.CREATE_TAB)" @click="setCurrentTab($options.tabConstants.CREATE_TAB)"
> >
<gitlab-experiment name="pipeline_editor_walkthrough">
<template #candidate>
<walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" />
</template>
</gitlab-experiment>
<ci-editor-header /> <ci-editor-header />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" /> <text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab> </editor-tab>

View File

@ -0,0 +1,83 @@
<script>
import { GlButton, GlPopover, GlSprintf, GlOutsideDirective as Outside } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
directives: { Outside },
i18n: {
title: s__('pipelineEditorWalkthrough|See how GitLab pipelines work'),
description: s__(
'pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline.',
),
instruction: s__(
'pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline.',
),
ctaText: s__("pipelineEditorWalkthrough|Let's do this!"),
},
components: {
GlButton,
GlPopover,
GlSprintf,
},
data() {
return {
show: true,
};
},
computed: {
targetElement() {
return document.querySelector('.js-walkthrough-popover-target');
},
},
methods: {
close() {
this.show = false;
},
handleClickCta() {
this.close();
this.$emit('walkthrough-popover-cta-clicked');
},
},
};
</script>
<template>
<gl-popover
:show.sync="show"
:title="$options.i18n.title"
:target="targetElement"
placement="right"
triggers="focus"
>
<div v-outside="close" class="gl-display-flex gl-flex-direction-column">
<p>
<gl-sprintf :message="$options.i18n.description">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="$options.i18n.instruction">
<template #bold="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</p>
<gl-button
class="gl-align-self-end"
category="tertiary"
data-testid="ctaBtn"
variant="confirm"
@click="handleClickCta"
>
<gl-emoji data-name="rocket" />
{{ this.$options.i18n.ctaText }}
</gl-button>
</div>
</gl-popover>
</template>

View File

@ -58,6 +58,7 @@ export default {
data() { data() {
return { return {
currentTab: CREATE_TAB, currentTab: CREATE_TAB,
scrollToCommitForm: false,
shouldLoadNewBranch: false, shouldLoadNewBranch: false,
showSwitchBranchModal: false, showSwitchBranchModal: false,
}; };
@ -81,6 +82,9 @@ export default {
setCurrentTab(tabName) { setCurrentTab(tabName) {
this.currentTab = tabName; this.currentTab = tabName;
}, },
setScrollToCommitForm(newValue = true) {
this.scrollToCommitForm = newValue;
},
}, },
}; };
</script> </script>
@ -117,8 +121,10 @@ export default {
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
:ci-file-content="ciFileContent" :ci-file-content="ciFileContent"
:commit-sha="commitSha" :commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
v-on="$listeners" v-on="$listeners"
@set-current-tab="setCurrentTab" @set-current-tab="setCurrentTab"
@walkthrough-popover-cta-clicked="setScrollToCommitForm"
/> />
<commit-section <commit-section
v-if="showCommitForm" v-if="showCommitForm"
@ -126,6 +132,8 @@ export default {
:ci-file-content="ciFileContent" :ci-file-content="ciFileContent"
:commit-sha="commitSha" :commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile" :is-new-ci-config-file="isNewCiConfigFile"
:scroll-to-commit-form="scrollToCommitForm"
@scrolled-to-commit-form="setScrollToCommitForm(false)"
v-on="$listeners" v-on="$listeners"
/> />
<pipeline-editor-drawer /> <pipeline-editor-drawer />

View File

@ -1,9 +1,9 @@
<script> <script>
import { GlLink } from '@gitlab/ui'; import { GlBadge, GlLink } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility'; import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
@ -14,7 +14,13 @@ import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import { statusTokenConfig } from '../components/search_tokens/status_token_config'; import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config'; import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants'; import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
I18N_FETCH_ERROR,
} from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql'; import getRunnersQuery from '../graphql/get_runners.query.graphql';
import { import {
fromUrlQueryToSearch, fromUrlQueryToSearch,
@ -26,6 +32,7 @@ import { captureException } from '../sentry_utils';
export default { export default {
name: 'AdminRunnersApp', name: 'AdminRunnersApp',
components: { components: {
GlBadge,
GlLink, GlLink,
RegistrationDropdown, RegistrationDropdown,
RunnerFilteredSearchBar, RunnerFilteredSearchBar,
@ -35,11 +42,27 @@ export default {
RunnerTypeTabs, RunnerTypeTabs,
}, },
props: { props: {
activeRunnersCount: { registrationToken: {
type: Number, type: String,
required: true, required: true,
}, },
registrationToken: { activeRunnersCount: {
type: String,
required: true,
},
allRunnersCount: {
type: String,
required: true,
},
instanceRunnersCount: {
type: String,
required: true,
},
groupRunnersCount: {
type: String,
required: true,
},
projectRunnersCount: {
type: String, type: String,
required: true, required: true,
}, },
@ -89,7 +112,7 @@ export default {
}, },
activeRunnersMessage() { activeRunnersMessage() {
return sprintf(__('Runners currently online: %{active_runners_count}'), { return sprintf(__('Runners currently online: %{active_runners_count}'), {
active_runners_count: formatNumber(this.activeRunnersCount), active_runners_count: this.activeRunnersCount,
}); });
}, },
searchTokens() { searchTokens() {
@ -118,6 +141,20 @@ export default {
this.reportToSentry(error); this.reportToSentry(error);
}, },
methods: { methods: {
tabCount({ runnerType }) {
switch (runnerType) {
case null:
return this.allRunnersCount;
case INSTANCE_TYPE:
return this.instanceRunnersCount;
case GROUP_TYPE:
return this.groupRunnersCount;
case PROJECT_TYPE:
return this.projectRunnersCount;
default:
return null;
}
},
reportToSentry(error) { reportToSentry(error) {
captureException({ error, component: this.$options.name }); captureException({ error, component: this.$options.name });
}, },
@ -128,15 +165,25 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<div class="gl-display-flex gl-align-items-center"> <div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
<runner-type-tabs <runner-type-tabs
v-model="search" v-model="search"
class="gl-w-full"
content-class="gl-display-none" content-class="gl-display-none"
nav-class="gl-border-none!" nav-class="gl-border-none!"
/> >
<template #title="{ tab }">
{{ tab.title }}
<gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm">
{{ tabCount(tab) }}
</gl-badge>
</template>
</runner-type-tabs>
<registration-dropdown <registration-dropdown
class="gl-ml-auto" class="gl-w-full gl-sm-w-auto gl-mr-auto"
:registration-token="registrationToken" :registration-token="registrationToken"
:type="$options.INSTANCE_TYPE" :type="$options.INSTANCE_TYPE"
right right

View File

@ -16,7 +16,16 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
// TODO `activeRunnersCount` should be implemented using a GraphQL API // TODO `activeRunnersCount` should be implemented using a GraphQL API
// https://gitlab.com/gitlab-org/gitlab/-/issues/333806 // https://gitlab.com/gitlab-org/gitlab/-/issues/333806
const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset; const {
runnerInstallHelpPage,
registrationToken,
activeRunnersCount,
allRunnersCount,
instanceRunnersCount,
groupRunnersCount,
projectRunnersCount,
} = el.dataset;
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(),
@ -31,8 +40,15 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
render(h) { render(h) {
return h(AdminRunnersApp, { return h(AdminRunnersApp, {
props: { props: {
activeRunnersCount: parseInt(activeRunnersCount, 10),
registrationToken, registrationToken,
// All runner counts are returned as formatted
// strings, we do not use `parseInt`.
activeRunnersCount,
allRunnersCount,
instanceRunnersCount,
groupRunnersCount,
projectRunnersCount,
}, },
}); });
}, },

View File

@ -51,13 +51,16 @@ export default {
}; };
</script> </script>
<template> <template>
<gl-tabs v-bind="$attrs"> <gl-tabs v-bind="$attrs" data-testid="runner-type-tabs">
<gl-tab <gl-tab
v-for="tab in $options.tabs" v-for="tab in $options.tabs"
:key="`${tab.runnerType}`" :key="`${tab.runnerType}`"
:active="isTabActive(tab)" :active="isTabActive(tab)"
:title="tab.title"
@click="onTabSelected(tab)" @click="onTabSelected(tab)"
/> >
<template #title>
<slot name="title" :tab="tab">{{ tab.title }}</slot>
</template>
</gl-tab>
</gl-tabs> </gl-tabs>
</template> </template>

View File

@ -8,7 +8,6 @@ class Admin::RunnersController < Admin::ApplicationController
feature_category :runner feature_category :runner
def index def index
@active_runners_count = Ci::Runner.online.count
end end
def show def show

View File

@ -116,7 +116,9 @@ class Import::BitbucketController < Import::BaseController
redirect_to oauth_client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url) redirect_to oauth_client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
end end
def bitbucket_unauthorized def bitbucket_unauthorized(exception)
log_exception(exception)
go_to_bitbucket_for_permissions go_to_bitbucket_for_permissions
end end

View File

@ -2,6 +2,7 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate! before_action :check_can_collaborate!
before_action :setup_walkthrough_experiment, only: :show
before_action do before_action do
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml) push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end end
@ -16,4 +17,10 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
def check_can_collaborate! def check_can_collaborate!
render_404 unless can_collaborate_with_project?(@project) render_404 unless can_collaborate_with_project?(@project)
end end
def setup_walkthrough_experiment
experiment(:pipeline_editor_walkthrough, actor: current_user) do |e|
e.candidate {}
end
end
end end

View File

@ -23,13 +23,14 @@ git push -uf origin <%= @project.default_branch_or_main %>
## Integrate with your tools ## Integrate with your tools
- [ ] [Set up project integrations](<%= redirect("https://docs.gitlab.com/ee/user/project/integrations/") %>) - [ ] [Set up project integrations](<%= redirect(project_settings_integrations_url(@project)) %>)
## Collaborate with your team ## Collaborate with your team
- [ ] [Invite team members and collaborators](<%= redirect("https://docs.gitlab.com/ee/user/project/members/") %>) - [ ] [Invite team members and collaborators](<%= redirect("https://docs.gitlab.com/ee/user/project/members/") %>)
- [ ] [Create a new merge request](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html") %>) - [ ] [Create a new merge request](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html") %>)
- [ ] [Automatically close issues from merge requests](<%= redirect("https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically") %>) - [ ] [Automatically close issues from merge requests](<%= redirect("https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically") %>)
- [ ] [Enable merge request approvals](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/approvals/") %>)
- [ ] [Automatically merge when pipeline succeeds](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html") %>) - [ ] [Automatically merge when pipeline succeeds](<%= redirect("https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html") %>)
## Test and Deploy ## Test and Deploy
@ -40,6 +41,7 @@ Use the built-in continuous integration in GitLab.
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](<%= redirect("https://docs.gitlab.com/ee/user/application_security/sast/") %>) - [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](<%= redirect("https://docs.gitlab.com/ee/user/application_security/sast/") %>)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](<%= redirect("https://docs.gitlab.com/ee/topics/autodevops/requirements.html") %>) - [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](<%= redirect("https://docs.gitlab.com/ee/topics/autodevops/requirements.html") %>)
- [ ] [Use pull-based deployments for improved Kubernetes management](<%= redirect("https://docs.gitlab.com/ee/user/clusters/agent/") %>) - [ ] [Use pull-based deployments for improved Kubernetes management](<%= redirect("https://docs.gitlab.com/ee/user/clusters/agent/") %>)
- [ ] [Set up protected environments](<%= redirect("https://docs.gitlab.com/ee/ci/environments/protected_environments.html") %>)
*** ***

View File

@ -60,6 +60,22 @@ module Ci
end end
end end
def admin_runners_data_attributes
{
# Runner install help page is external, located at
# https://gitlab.com/gitlab-org/gitlab-runner
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
registration_token: Gitlab::CurrentSettings.runners_registration_token,
# All runner counts are returned as formatted strings
active_runners_count: Ci::Runner.online.count.to_s,
all_runners_count: limited_counter_with_delimiter(Ci::Runner),
instance_runners_count: limited_counter_with_delimiter(Ci::Runner.instance_type),
group_runners_count: limited_counter_with_delimiter(Ci::Runner.group_type),
project_runners_count: limited_counter_with_delimiter(Ci::Runner.project_type)
}
end
def group_shared_runners_settings_data(group) def group_shared_runners_settings_data(group)
{ {
update_path: api_v4_groups_path(id: group.id), update_path: api_v4_groups_path(id: group.id),

View File

@ -1,4 +1,4 @@
- breadcrumb_title _('Runners') - breadcrumb_title _('Runners')
- page_title _('Runners') - page_title _('Runners')
#js-admin-runners{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } } #js-admin-runners{ data: admin_runners_data_attributes }

View File

@ -18,6 +18,36 @@ module Gitlab
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 6.1 config.load_defaults 6.1
# This section contains configuration from Rails upgrades to override the new defaults so that we
# keep existing behavior.
#
# For boolean values, the new default is the opposite of the value being set in this section.
# For other types, the new default is noted in the comments. These are also documented in
# https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
#
# To switch a setting to the new default value, we just need to delete the specific line here.
# Rails 6.1
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
ActiveSupport.utc_to_local_returns_utc_offset_times = false
config.action_controller.urlsafe_csrf_tokens = false
config.action_view.preload_links_header = false
# Rails 5.2
config.action_dispatch.use_authenticated_cookie_encryption = false
config.active_support.use_authenticated_message_encryption = false
config.active_support.hash_digest_class = ::Digest::MD5 # New default is ::Digest::SHA1
config.action_controller.default_protect_from_forgery = false
config.action_view.form_with_generates_ids = false
# Rails 5.1
config.assets.unknown_asset_fallback = true
# Rails 5.0
config.action_controller.per_form_csrf_tokens = false
config.action_controller.forgery_protection_origin_check = false
ActiveSupport.to_time_preserves_timezone = false
require_dependency Rails.root.join('lib/gitlab') require_dependency Rails.root.join('lib/gitlab')
require_dependency Rails.root.join('lib/gitlab/utils') require_dependency Rails.root.join('lib/gitlab/utils')
require_dependency Rails.root.join('lib/gitlab/action_cable/config') require_dependency Rails.root.join('lib/gitlab/action_cable/config')

View File

@ -0,0 +1,8 @@
---
name: pipeline_editor_walkthrough
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73050
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345558
milestone: '14.5'
type: experiment
group: group::activation
default_enabled: false

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
# This contains configuration from Rails upgrades to override the new defaults so that we
# keep existing behavior.
#
# For boolean values, the new default is the opposite of the value being set in this file.
# For other types, the new default is noted in the comments. These are also documented in
# https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
#
# To switch a setting to the new default value, we just need to delete the specific line here.
Rails.application.configure do
# Rails 6.1
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
ActiveSupport.utc_to_local_returns_utc_offset_times = false
config.action_controller.urlsafe_csrf_tokens = false
config.action_view.preload_links_header = false
# Rails 5.2
config.action_dispatch.use_authenticated_cookie_encryption = false
config.active_support.use_authenticated_message_encryption = false
config.active_support.hash_digest_class = ::Digest::MD5 # New default is ::Digest::SHA1
config.action_controller.default_protect_from_forgery = false
config.action_view.form_with_generates_ids = false
# Rails 5.1
config.assets.unknown_asset_fallback = true
# Rails 5.0
config.action_controller.per_form_csrf_tokens = false
config.action_controller.forgery_protection_origin_check = false
ActiveSupport.to_time_preserves_timezone = false
end

View File

@ -48,10 +48,18 @@ Please consider creating a merge request to
for them. for them.
MARKDOWN MARKDOWN
def group_not_available_template(slack_channel, gitlab_group)
<<~TEMPLATE
No engineer is available for automated assignment, please reach out to `#{slack_channel}` slack channel or mention `#{gitlab_group}` for assistance.
TEMPLATE
end
OPTIONAL_REVIEW_TEMPLATE = '%{role} review is optional for %{category}' OPTIONAL_REVIEW_TEMPLATE = '%{role} review is optional for %{category}'
NOT_AVAILABLE_TEMPLATES = { NOT_AVAILABLE_TEMPLATES = {
default: 'No %{role} available', default: 'No %{role} available',
product_intelligence: "No engineer is available for automated assignment, please reach out to `#g_product_intelligence` slack channel or mention `@gitlab-org/growth/product-intelligence/engineers` for assistance." product_intelligence: group_not_available_template('#g_product_intelligence', '@gitlab-org/growth/product-intelligence/engineers'),
integrations_be: group_not_available_template('#g_ecosystem_integrations', '@gitlab-org/ecosystem-stage/integrations'),
integrations_fe: group_not_available_template('#g_ecosystem_integrations', '@gitlab-org/ecosystem-stage/integrations')
}.freeze }.freeze
def note_for_spins_role(spins, role, category) def note_for_spins_role(spins, role, category)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@ -115,32 +115,28 @@ your chosen [provider](#supported-providers).
## Enable OmniAuth for an existing user ## Enable OmniAuth for an existing user
Existing users can enable OmniAuth for specific providers after the account is If you're an existing user, after your GitLab account is
created. For example, if the user originally signed in with LDAP, an OmniAuth created, you can activate an OmniAuth provider. For example, if you originally signed in with LDAP, you can enable an OmniAuth
provider such as Twitter can be enabled. Follow the steps below to enable an provider like Twitter.
OmniAuth provider for an existing user.
1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider. 1. Sign in to GitLab with your GitLab credentials, LDAP, or another OmniAuth provider.
1. In the top-right corner, select your avatar. 1. On the top bar, in the top right corner, select your avatar.
1. Select **Edit profile**. 1. Select **Edit profile**.
1. On the left sidebar, select **Account**. 1. On the left sidebar, select **Account**.
1. In the **Connected Accounts** section, select the desired OmniAuth provider, such as Twitter. 1. In the **Connected Accounts** section, select the OmniAuth provider, such as Twitter.
1. The user is redirected to the provider. After the user authorizes GitLab, 1. You are redirected to the provider. After you authorize GitLab,
they are redirected back to GitLab. you are redirected back to GitLab.
The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. You can now use your chosen OmniAuth provider to sign in to GitLab.
## Link existing users to OmniAuth users ## Link existing users to OmniAuth users
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36664) in GitLab 13.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36664) in GitLab 13.4.
You can automatically link OmniAuth users with existing GitLab users if their email addresses match. You can automatically link OmniAuth users with existing GitLab users if their email addresses match.
Automatic linking using this method works for all providers
[except the SAML provider](https://gitlab.com/gitlab-org/gitlab/-/issues/338293). For automatic
linking using the SAML provider, see [SAML-specific](saml.md#general-setup) instructions.
As an example, the following configuration is used to enable the auto link The following example enables automatic linking
feature for both an **OpenID Connect provider** and a **Twitter OAuth provider**. for the OpenID Connect provider and the Twitter OAuth provider.
- **For Omnibus installations** - **For Omnibus installations**
@ -155,18 +151,23 @@ feature for both an **OpenID Connect provider** and a **Twitter OAuth provider**
auto_link_user: ["openid_connect", "twitter"] auto_link_user: ["openid_connect", "twitter"]
``` ```
## Configure OmniAuth providers as external This method of enabling automatic linking works for all providers
[except SAML](https://gitlab.com/gitlab-org/gitlab/-/issues/338293).
To enable automatic linking for SAML, see the [SAML setup instructions](saml.md#general-setup).
You can define which OmniAuth providers you want to be `external`. Users ## Create an external providers list
creating accounts, or logging in by using these `external` providers cannot have
access to internal projects. You must use the full name of the provider, You can define a list of external OmniAuth providers.
like `google_oauth2` for Google. Refer to the examples for the full names of the Users who create accounts or sign in to GitLab through the listed providers do not get access to [internal projects](../public_access/public_access.md#internal-projects-and-groups).
supported providers.
To define the external providers list, use the full name of the provider,
for example, `google_oauth2` for Google. For provider names, see the
**OmniAuth provider name** column in the [supported providers table](#supported-providers).
NOTE: NOTE:
If you decide to remove an OmniAuth provider from the external providers list, If you remove an OmniAuth provider from the external providers list,
you must manually update the users that use this method to sign in if you want you must manually update the users that use this sign-in method so their
their accounts to be upgraded to full internal accounts. accounts are upgraded to full internal accounts.
- **For Omnibus installations** - **For Omnibus installations**
@ -184,70 +185,67 @@ their accounts to be upgraded to full internal accounts.
## Use a custom OmniAuth provider ## Use a custom OmniAuth provider
NOTE: NOTE:
The following information only applies for installations from source. The following information only applies to installations from source.
GitLab uses [OmniAuth](https://github.com/omniauth/omniauth) for authentication and already ships If you have to integrate with an authentication solution other than the [OmniAuth](https://github.com/omniauth/omniauth) providers included with GitLab,
with a few providers pre-installed, such as LDAP, GitHub, and Twitter. You may also you can use a custom OmniAuth provider.
have to integrate with other authentication solutions. For
these cases, you can use the OmniAuth provider.
These steps are fairly general and you must figure out the exact details These steps are general. Read the OmniAuth provider's documentation for the exact
from the OmniAuth provider's documentation. implementation details.
- Stop GitLab: 1. Stop GitLab:
```shell ```shell
sudo service gitlab stop sudo service gitlab stop
``` ```
- Add the gem to your [`Gemfile`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile): 1. Add the gem to your [`Gemfile`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile):
```shell ```shell
gem "omniauth-your-auth-provider" gem "omniauth-your-auth-provider"
``` ```
- Install the new OmniAuth provider gem by running the following command: 1. Install the new OmniAuth provider gem:
```shell ```shell
sudo -u git -H bundle install --without development test mysql --path vendor/bundle --no-deployment sudo -u git -H bundle install --without development test mysql --path vendor/bundle --no-deployment
``` ```
> These are the same commands you used during initial installation in the [Install Gems section](../install/installation.md#install-gems) with `--path vendor/bundle --no-deployment` instead of `--deployment`. These commands are the same as the commands for [installing gems](../install/installation.md#install-gems)
during initial installation, with `--path vendor/bundle --no-deployment` instead of `--deployment`.
- Start GitLab: 1. Start GitLab:
```shell ```shell
sudo service gitlab start sudo service gitlab start
``` ```
### Custom OmniAuth provider examples ### Custom OmniAuth provider examples
If you have successfully set up a provider that is not shipped with GitLab itself, If you have successfully set up a provider that is not already integrated with GitLab,
please let us know. let us know.
While we can't officially support every possible authentication mechanism out there, We can't officially support every possible authentication mechanism available,
we'd like to at least help those with specific needs. but we'd like to at least help those with specific needs.
## Enable or disable sign-in with an OmniAuth provider without disabling import sources ## Enable or disable sign-in with an OmniAuth provider without disabling import sources
Administrators are able to enable or disable **Sign In** by using some OmniAuth providers. Administrators can enable or disable sign-in for some OmniAuth providers.
NOTE: NOTE:
By default, **Sign In** is enabled by using all the OAuth Providers that have been configured in `config/gitlab.yml`. By default, sign-in is enabled for all the OAuth providers configured in `config/gitlab.yml`.
To enable/disable an OmniAuth provider: To enable or disable an OmniAuth provider:
1. On the top bar, select **Menu > Admin**. 1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, go to **Settings**. 1. On the left sidebar, select **Settings**.
1. Scroll to the **Sign-in Restrictions** section, and click **Expand**. 1. Expand **Sign-in restrictions**.
1. Below **Enabled OAuth Sign-In sources**, select the checkbox for each provider you want to enable or disable. 1. In the **Enabled OAuth authentication sources** section, select or clear the checkbox for each provider you want to enable or disable.
![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources_v13_10.png)
## Disable OmniAuth ## Disable OmniAuth
Starting from version 11.4 of GitLab, OmniAuth is enabled by default. This only In GitLab 11.4 and later, OmniAuth is enabled by default. However, OmniAuth only works
has an effect if providers are configured and [enabled](#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources). if providers are configured and [enabled](#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources).
If OmniAuth providers are causing problems even when individually disabled, you If OmniAuth providers are causing problems even when individually disabled, you
can disable the entire OmniAuth subsystem by modifying the configuration file: can disable the entire OmniAuth subsystem by modifying the configuration file:
@ -267,7 +265,8 @@ can disable the entire OmniAuth subsystem by modifying the configuration file:
## Keep OmniAuth user profiles up to date ## Keep OmniAuth user profiles up to date
You can enable profile syncing from selected OmniAuth providers and for all or for specific user information. You can enable profile syncing from selected OmniAuth providers. You can sync
all or specific user information.
When authenticating using LDAP, the user's name and email are always synced. When authenticating using LDAP, the user's name and email are always synced.
@ -288,13 +287,20 @@ When authenticating using LDAP, the user's name and email are always synced.
## Bypass two-factor authentication ## Bypass two-factor authentication
In GitLab 12.3 or later, users can sign in with specified providers _without_ > Introduced in GitLab 12.3.
using two factor authentication.
Define the allowed providers using an array (for example, `["twitter", 'google_oauth2']`), With certain OmniAuth providers, users can sign in without
or as `true` or `false` to allow all providers (or none). This option should be using two-factor authentication.
configured only for providers which already have two factor authentication
(default: false). This configuration doesn't apply to SAML. To bypass two-factor authentication, you can either:
- Define the allowed providers using an array (for example, `['twitter', 'google_oauth2']`).
- Specify `true` to allow all providers, or `false` to allow none.
This option should be configured only for providers that already have
two-factor authentication. The default is `false`.
This configuration doesn't apply to SAML.
- **For Omnibus package** - **For Omnibus package**
@ -313,10 +319,10 @@ configured only for providers which already have two factor authentication
You can add the `auto_sign_in_with_provider` setting to your GitLab You can add the `auto_sign_in_with_provider` setting to your GitLab
configuration to redirect login requests to your OmniAuth provider for configuration to redirect login requests to your OmniAuth provider for
authentication. This removes the need to click a button before actually signing in. authentication. This removes the need to select the provider before signing in.
For example, when using the [Azure v2 integration](azure.md#microsoft-azure-oauth-20-omniauth-provider-v2), set the following to enable auto For example, to enable automatic sign-in for the
sign-in: [Azure v2 integration](azure.md#microsoft-azure-oauth-20-omniauth-provider-v2):
- **For Omnibus package** - **For Omnibus package**
@ -332,10 +338,10 @@ sign-in:
``` ```
Keep in mind that every sign-in attempt is redirected to the OmniAuth Keep in mind that every sign-in attempt is redirected to the OmniAuth
provider; you can't sign in using local credentials. Ensure at least provider, so you can't sign in using local credentials. Ensure at least
one of the OmniAuth users has an administrator role. one of the OmniAuth users is an administrator.
You may also bypass the auto sign in feature by browsing to You can also bypass automatic sign-in by browsing to
`https://gitlab.example.com/users/sign_in?auto_sign_in=false`. `https://gitlab.example.com/users/sign_in?auto_sign_in=false`.
## Passwords for users created via OmniAuth ## Passwords for users created via OmniAuth
@ -344,11 +350,12 @@ The [Generated passwords for users created through integrated authentication](..
guide provides an overview about how GitLab generates and sets passwords for guide provides an overview about how GitLab generates and sets passwords for
users created with OmniAuth. users created with OmniAuth.
## Custom OmniAuth provider icon ## Use a custom OmniAuth provider icon
Most supported providers include a built-in icon for the rendered sign-in button. Most supported providers include a built-in icon for the rendered sign-in button.
After you ensure your image is optimized for rendering at 64 x 64 pixels,
you can override this icon in one of two ways: To use your own icon, ensure your image is optimized for rendering at 64 x 64 pixels,
then override the icon in one of two ways:
- **Provide a custom image path**: - **Provide a custom image path**:
@ -359,11 +366,11 @@ you can override this icon in one of two ways:
to your GitLab configuration file. Read [OpenID Connect OmniAuth provider](../administration/auth/oidc.md) to your GitLab configuration file. Read [OpenID Connect OmniAuth provider](../administration/auth/oidc.md)
for an example for the OpenID Connect provider. for an example for the OpenID Connect provider.
- **Directly embed an image in a configuration file**: This example creates a Base64-encoded - **Embed an image directly in a configuration file**: This example creates a Base64-encoded
version of your image you can serve through a version of your image you can serve through a
[Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs): [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs):
1. Encode your image file with GNU `base64` command (such as `base64 -w 0 <logo.png>`) 1. Encode your image file with a GNU `base64` command (such as `base64 -w 0 <logo.png>`)
which returns a single-line `<base64-data>` string. which returns a single-line `<base64-data>` string.
1. Add the Base64-encoded data to a custom `icon` parameter in your GitLab 1. Add the Base64-encoded data to a custom `icon` parameter in your GitLab
configuration file: configuration file:

View File

@ -68,7 +68,7 @@ You can set a global prefix for all generated Personal Access Tokens.
A prefix can help you identify PATs visually, as well as with automation tools. A prefix can help you identify PATs visually, as well as with automation tools.
NOTE: NOTE:
For GitLab.com and new self-managed instances, the default prefix is `glpat-`. For GitLab.com and self-managed instances, the default prefix is `glpat-`.
### Set a prefix ### Set a prefix

View File

@ -41,6 +41,10 @@ The Agent can be configured to enable access to the CI/CD Tunnel to other projec
You can read more on how to [authorize access in the Agent configuration reference](repository.md#authorize-projects-and-groups-to-use-an-agent). You can read more on how to [authorize access in the Agent configuration reference](repository.md#authorize-projects-and-groups-to-use-an-agent).
## Restrict access of authorized projects and groups **(PREMIUM)**
You can [configure various impersonations](repository.md#use-impersonation-to-restrict-project-and-group-access) to restrict the permissions of a shared CI/CD Tunnel.
## Example for a `kubectl` command using the CI/CD Tunnel ## Example for a `kubectl` command using the CI/CD Tunnel
The following example shows a CI/CD job that runs a `kubectl` command using the CI/CD Tunnel. The following example shows a CI/CD job that runs a `kubectl` command using the CI/CD Tunnel.

View File

@ -198,6 +198,87 @@ To grant access to all projects within a group:
- id: path/to/group/subgroup - id: path/to/group/subgroup
``` ```
### Use impersonation to restrict project and group access **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345014) in GitLab 14.5.
By default, the [CI/CD Tunnel](ci_cd_tunnel.md) inherits all the permissions from the service account used to install the
Agent in the cluster.
To restrict access to your cluster, you can use [impersonation](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation).
To specify impersonations, use the `access_as` attribute in your Agent's configuration file and use Kubernetes RBAC rules to manage impersonated account permissions.
You can impersonate:
- The Agent itself (default).
- The CI job that accesses the cluster.
- A specific user or system account defined within the cluster.
#### Impersonate the Agent
The Agent is impersonated by default. You don't need to do anything to impersonate it.
#### Impersonate the CI job that accesses the cluster
To impersonate the CI job that accesses the cluster, add the `ci_job: {}` key-value
under the `access_as` key.
When the agent makes the request to the actual Kubernetes API, it sets the
impersonation credentials in the following way:
- `UserName` is set to `gitlab:ci_job:<job id>`. Example: `gitlab:ci_job:1074499489`.
- `Groups` is set to:
- `gitlab:ci_job` to identify all requests coming from CI jobs.
- The list of IDs of groups the project is in.
- The project ID.
- The slug of the environment this job belongs to.
Example: for a CI job in `group1/group1-1/project1` where:
- Group `group1` has ID 23.
- Group `group1/group1-1` has ID 25.
- Project `group1/group1-1/project1` has ID 150.
- Job running in a prod environment.
Group list would be `[gitlab:ci_job, gitlab:group:23, gitlab:group:25, gitlab:project:150, gitlab:project_env:150:prod]`.
- `Extra` carries extra information about the request. The following properties are set on the impersonated identity:
| Property | Description |
| -------- | ----------- |
| `agent.gitlab.com/id` | Contains the agent ID. |
| `agent.gitlab.com/config_project_id` | Contains the agent's configuration project ID. |
| `agent.gitlab.com/project_id` | Contains the CI project ID. |
| `agent.gitlab.com/ci_pipeline_id` | Contains the CI pipeline ID. |
| `agent.gitlab.com/ci_job_id` | Contains the CI job ID. |
| `agent.gitlab.com/username` | Contains the username of the user the CI job is running as. |
| `agent.gitlab.com/environment_slug` | Contains the slug of the environment. Only set if running in an environment. |
Example to restrict access by the CI job's identity:
```yaml
ci_access:
projects:
- id: path/to/project
access_as:
ci_job: {}
```
#### Impersonate a static identity
For the given CI/CD Tunnel connection, you can use a static identity for the impersonation.
Add the `impersonate` key under the `access_as` key to make the request using the provided identity.
The identity can be specified with the following keys:
- `username` (required)
- `uid`
- `groups`
- `extra`
See the [official Kubernetes documentation for more details](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation) on the usage of these keys.
## Surface network security alerts from cluster to GitLab **(ULTIMATE)** ## Surface network security alerts from cluster to GitLab **(ULTIMATE)**
The GitLab Agent provides an [integration with Cilium](index.md#kubernetes-network-security-alerts). The GitLab Agent provides an [integration with Cilium](index.md#kubernetes-network-security-alerts).

View File

@ -6,6 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container Host Security **(FREE)** # Container Host Security **(FREE)**
NOTE:
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
You can continue using Container Host Security, even though it relies on this certificate-based
method. The work to allow all aspects of Container Host Security to function through the [GitLab Kubernetes Agent](../../../../clusters/agent/index.md)
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350).
Container Host Security in GitLab provides Intrusion Detection and Prevention capabilities that can Container Host Security in GitLab provides Intrusion Detection and Prevention capabilities that can
monitor and (optionally) block activity inside the containers themselves. This is done by leveraging monitor and (optionally) block activity inside the containers themselves. This is done by leveraging
an integration with Falco to provide the monitoring capabilities and an integration with Pod an integration with Falco to provide the monitoring capabilities and an integration with Pod

View File

@ -6,6 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container Network Security **(FREE)** # Container Network Security **(FREE)**
NOTE:
In GitLab 14.5, using a certificate to connect GitLab to a Kubernetes cluster is [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8).
You can continue using Container Network Security, even though it relies on this certificate-based
method. The work to allow all aspects of Container Network Security to function through the [GitLab Kubernetes Agent](../../../../clusters/agent/index.md)
instead of the certificate-based method can be tracked [in this GitLab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/299350) and [this GitLab Epic](https://gitlab.com/groups/gitlab-org/-/epics/7057).
Container Network Security in GitLab provides basic firewall functionality by leveraging Cilium Container Network Security in GitLab provides basic firewall functionality by leveraging Cilium
NetworkPolicies to filter traffic going in and out of the cluster as well as traffic between pods NetworkPolicies to filter traffic going in and out of the cluster as well as traffic between pods
inside the cluster. Container Network Security can be used to enforce L3, L4, and L7 policies and inside the cluster. Container Network Security can be used to enforce L3, L4, and L7 policies and

View File

@ -34,7 +34,7 @@ For examples of how you can use a project access token to authenticate with the
[relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens). [relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens).
NOTE: NOTE:
For GitLab.com and new self-managed instances, the default prefix is `glpat-`. For GitLab.com and self-managed instances, the default prefix is `glpat-`.
## Creating a project access token ## Creating a project access token

View File

@ -41698,6 +41698,18 @@ msgstr ""
msgid "pipeline schedules documentation" msgid "pipeline schedules documentation"
msgstr "" msgstr ""
msgid "pipelineEditorWalkthrough|Let's do this!"
msgstr ""
msgid "pipelineEditorWalkthrough|See how GitLab pipelines work"
msgstr ""
msgid "pipelineEditorWalkthrough|This %{codeStart}.gitlab-ci.yml%{codeEnd} file creates a simple test pipeline."
msgstr ""
msgid "pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline."
msgstr ""
msgid "pod_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character" msgid "pod_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
msgstr "" msgstr ""

View File

@ -12,9 +12,11 @@ RSpec.describe Admin::RunnersController do
describe '#index' do describe '#index' do
render_views render_views
it 'lists all runners' do before do
get :index get :index
end
it 'renders index template' do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index) expect(response).to render_template(:index)
end end

View File

@ -252,6 +252,30 @@ RSpec.describe Import::BitbucketController do
end end
end end
end end
context "when exceptions occur" do
shared_examples "handles exceptions" do
it "logs an exception" do
expect(Bitbucket::Client).to receive(:new).and_raise(error)
expect(controller).to receive(:log_exception)
post :create, format: :json
end
end
context "for OAuth2 errors" do
let(:fake_response) { double('Faraday::Response', headers: {}, body: '', status: 403) }
let(:error) { OAuth2::Error.new(OAuth2::Response.new(fake_response)) }
it_behaves_like "handles exceptions"
end
context "for Bitbucket errors" do
let(:error) { Bitbucket::Error::Unauthorized.new("error") }
it_behaves_like "handles exceptions"
end
end
end end
context 'user has chosen an existing nested namespace and name for the project' do context 'user has chosen an existing nested namespace and name for the project' do

View File

@ -6,6 +6,8 @@ RSpec.describe Projects::Ci::PipelineEditorController do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
subject(:show_request) { get :show, params: { namespace_id: project.namespace, project_id: project } }
before do before do
sign_in(user) sign_in(user)
end end
@ -14,8 +16,7 @@ RSpec.describe Projects::Ci::PipelineEditorController do
context 'with enough privileges' do context 'with enough privileges' do
before do before do
project.add_developer(user) project.add_developer(user)
show_request
get :show, params: { namespace_id: project.namespace, project_id: project }
end end
it { expect(response).to have_gitlab_http_status(:ok) } it { expect(response).to have_gitlab_http_status(:ok) }
@ -28,13 +29,27 @@ RSpec.describe Projects::Ci::PipelineEditorController do
context 'without enough privileges' do context 'without enough privileges' do
before do before do
project.add_reporter(user) project.add_reporter(user)
show_request
get :show, params: { namespace_id: project.namespace, project_id: project }
end end
it 'responds with 404' do it 'responds with 404' do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
describe 'pipeline_editor_walkthrough experiment' do
before do
project.add_developer(user)
end
it 'tracks the assignment', :experiment do
expect(experiment(:pipeline_editor_walkthrough))
.to track(:assignment)
.with_context(actor: user)
.on_next_instance
show_request
end
end
end end
end end

View File

@ -66,11 +66,11 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
end end
it 'runner type can be selected' do it 'runner types tabs have total counts and can be selected' do
expect(page).to have_link('All') expect(page).to have_link('All 2')
expect(page).to have_link('Instance') expect(page).to have_link('Instance 2')
expect(page).to have_link('Group') expect(page).to have_link('Group 0')
expect(page).to have_link('Project') expect(page).to have_link('Project 0')
end end
it 'shows runners' do it 'shows runners' do
@ -162,10 +162,12 @@ RSpec.describe "Admin Runners" do
create(:ci_runner, :group, description: 'runner-group', groups: [group]) create(:ci_runner, :group, description: 'runner-group', groups: [group])
end end
it 'shows correct runner when type matches' do it '"All" tab is selected by default' do
visit admin_runners_path visit admin_runners_path
expect(page).to have_link('All', class: 'active') page.within('[data-testid="runner-type-tabs"]') do
expect(page).to have_link('All', class: 'active')
end
end end
it 'shows correct runner when type matches' do it 'shows correct runner when type matches' do
@ -174,9 +176,11 @@ RSpec.describe "Admin Runners" do
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group' expect(page).to have_content 'runner-group'
click_on 'Project' page.within('[data-testid="runner-type-tabs"]') do
click_on('Project')
expect(page).to have_link('Project', class: 'active') expect(page).to have_link('Project', class: 'active')
end
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group' expect(page).not_to have_content 'runner-group'
@ -185,9 +189,11 @@ RSpec.describe "Admin Runners" do
it 'shows no runner when type does not match' do it 'shows no runner when type does not match' do
visit admin_runners_path visit admin_runners_path
click_on 'Instance' page.within('[data-testid="runner-type-tabs"]') do
click_on 'Instance'
expect(page).to have_link('Instance', class: 'active') expect(page).to have_link('Instance', class: 'active')
end
expect(page).not_to have_content 'runner-project' expect(page).not_to have_content 'runner-project'
expect(page).not_to have_content 'runner-group' expect(page).not_to have_content 'runner-group'
@ -200,7 +206,9 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
click_on 'Project' page.within('[data-testid="runner-type-tabs"]') do
click_on 'Project'
end
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-2-project' expect(page).to have_content 'runner-2-project'
@ -224,7 +232,9 @@ RSpec.describe "Admin Runners" do
expect(page).to have_content 'runner-group' expect(page).to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project' expect(page).not_to have_content 'runner-paused-project'
click_on 'Project' page.within('[data-testid="runner-type-tabs"]') do
click_on 'Project'
end
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group' expect(page).not_to have_content 'runner-group'

View File

@ -238,8 +238,10 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil) def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side) write_comment_on_line(line_holder, diff_side)
accept_confirm do find('.js-close-discussion-note-form').click
find('.js-close-discussion-note-form').click
page.within('.modal') do
click_button 'OK'
end end
assert_comment_dismissal(line_holder) assert_comment_dismissal(line_holder)

View File

@ -1,10 +1,18 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue'; import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import { createStore } from '~/mr_notes/stores'; import { createStore } from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue'; import NoteForm from '~/notes/components/note_form.vue';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { noteableDataMock } from '../../notes/mock_data'; import { noteableDataMock } from '../../notes/mock_data';
import diffFileMockData from '../mock_data/diff_file'; import diffFileMockData from '../mock_data/diff_file';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
return {
confirmAction: jest.fn(),
};
});
describe('DiffLineNoteForm', () => { describe('DiffLineNoteForm', () => {
let wrapper; let wrapper;
let diffFile; let diffFile;
@ -36,49 +44,56 @@ describe('DiffLineNoteForm', () => {
}); });
}; };
const findNoteForm = () => wrapper.findComponent(NoteForm);
describe('methods', () => { describe('methods', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
}); });
describe('handleCancelCommentForm', () => { describe('handleCancelCommentForm', () => {
afterEach(() => {
confirmAction.mockReset();
});
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => { it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
jest.spyOn(window, 'confirm').mockReturnValue(false); confirmAction.mockResolvedValueOnce(false);
wrapper.vm.handleCancelCommentForm(true, true); findNoteForm().vm.$emit('cancelForm', true, true);
expect(window.confirm).toHaveBeenCalled(); expect(confirmAction).toHaveBeenCalled();
}); });
it('should ask for confirmation when one of the params false', () => { it('should not ask for confirmation when one of the params false', () => {
jest.spyOn(window, 'confirm').mockReturnValue(false); confirmAction.mockResolvedValueOnce(false);
wrapper.vm.handleCancelCommentForm(true, false); findNoteForm().vm.$emit('cancelForm', true, false);
expect(window.confirm).not.toHaveBeenCalled(); expect(confirmAction).not.toHaveBeenCalled();
wrapper.vm.handleCancelCommentForm(false, true); findNoteForm().vm.$emit('cancelForm', false, true);
expect(window.confirm).not.toHaveBeenCalled(); expect(confirmAction).not.toHaveBeenCalled();
}); });
it('should call cancelCommentForm with lineCode', (done) => { it('should call cancelCommentForm with lineCode', async () => {
jest.spyOn(window, 'confirm').mockImplementation(() => {}); confirmAction.mockResolvedValueOnce(true);
jest.spyOn(wrapper.vm, 'cancelCommentForm').mockImplementation(() => {}); jest.spyOn(wrapper.vm, 'cancelCommentForm').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'resetAutoSave').mockImplementation(() => {}); jest.spyOn(wrapper.vm, 'resetAutoSave').mockImplementation(() => {});
wrapper.vm.handleCancelCommentForm();
expect(window.confirm).not.toHaveBeenCalled(); findNoteForm().vm.$emit('cancelForm', true, true);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
lineCode: diffLines[1].line_code,
fileHash: wrapper.vm.diffFileHash,
});
expect(wrapper.vm.resetAutoSave).toHaveBeenCalled(); await nextTick();
done(); expect(confirmAction).toHaveBeenCalled();
await nextTick();
expect(wrapper.vm.cancelCommentForm).toHaveBeenCalledWith({
lineCode: diffLines[1].line_code,
fileHash: wrapper.vm.diffFileHash,
}); });
expect(wrapper.vm.resetAutoSave).toHaveBeenCalled();
}); });
}); });

View File

@ -5,6 +5,9 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data'; import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
describe('Pipeline Editor | Commit Form', () => { describe('Pipeline Editor | Commit Form', () => {
let wrapper; let wrapper;
@ -113,4 +116,20 @@ describe('Pipeline Editor | Commit Form', () => {
expect(findSubmitBtn().attributes('disabled')).toBe('disabled'); expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
}); });
}); });
describe('when scrollToCommitForm becomes true', () => {
beforeEach(async () => {
createComponent();
wrapper.setProps({ scrollToCommitForm: true });
await wrapper.vm.$nextTick();
});
it('scrolls into view', () => {
expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth' });
});
it('emits "scrolled-to-commit-form"', () => {
expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy();
});
});
}); });

View File

@ -277,4 +277,16 @@ describe('Pipeline Editor | Commit section', () => {
expect(wrapper.emitted('resetContent')).toHaveLength(1); expect(wrapper.emitted('resetContent')).toHaveLength(1);
}); });
}); });
it('sets listeners on commit form', () => {
const handler = jest.fn();
createComponent({ options: { listeners: { event: handler } } });
findCommitForm().vm.$emit('event');
expect(handler).toHaveBeenCalled();
});
it('passes down scroll-to-commit-form prop to commit form', () => {
createComponent({ props: { 'scroll-to-commit-form': true } });
expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
});
}); });

View File

@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { stubExperiments } from 'helpers/experimentation_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue'; import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
@ -33,19 +34,41 @@ describe('Pipeline editor drawer', () => {
const clickToggleBtn = async () => findToggleBtn().vm.$emit('click'); const clickToggleBtn = async () => findToggleBtn().vm.$emit('click');
const originalObjects = [];
beforeEach(() => {
originalObjects.push(window.gon, window.gl);
stubExperiments({ pipeline_editor_walkthrough: 'control' });
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
localStorage.clear(); localStorage.clear();
[window.gon, window.gl] = originalObjects;
}); });
it('it sets the drawer to be opened by default', async () => { describe('default expanded state', () => {
createComponent(); describe('when experiment control', () => {
it('sets the drawer to be opened by default', async () => {
createComponent();
expect(findDrawerContent().exists()).toBe(false);
await nextTick();
expect(findDrawerContent().exists()).toBe(true);
});
});
expect(findDrawerContent().exists()).toBe(false); describe('when experiment candidate', () => {
beforeEach(() => {
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
});
await nextTick(); it('sets the drawer to be closed by default', async () => {
createComponent();
expect(findDrawerContent().exists()).toBe(true); expect(findDrawerContent().exists()).toBe(false);
await nextTick();
expect(findDrawerContent().exists()).toBe(false);
});
});
}); });
describe('when the drawer is collapsed', () => { describe('when the drawer is collapsed', () => {

View File

@ -141,8 +141,8 @@ describe('Pipeline editor branch switcher', () => {
createComponentWithApollo(); createComponentWithApollo();
}); });
it('does not render dropdown', () => { it('disables the dropdown', () => {
expect(findDropdown().exists()).toBe(false); expect(findDropdown().props('disabled')).toBe(true);
}); });
}); });
@ -189,7 +189,7 @@ describe('Pipeline editor branch switcher', () => {
}); });
it('does not render dropdown', () => { it('does not render dropdown', () => {
expect(findDropdown().exists()).toBe(false); expect(findDropdown().props('disabled')).toBe(true);
}); });
it('shows an error message', () => { it('shows an error message', () => {

View File

@ -1,11 +1,13 @@
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue'; import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
import { stubExperiments } from 'helpers/experimentation_helper';
import { import {
CREATE_TAB, CREATE_TAB,
EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_EMPTY,
@ -19,6 +21,8 @@ import {
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data'; import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
Vue.config.ignoredElements = ['gl-emoji'];
describe('Pipeline editor tabs component', () => { describe('Pipeline editor tabs component', () => {
let wrapper; let wrapper;
const MockTextEditor = { const MockTextEditor = {
@ -26,6 +30,7 @@ describe('Pipeline editor tabs component', () => {
}; };
const createComponent = ({ const createComponent = ({
listeners = {},
props = {}, props = {},
provide = {}, provide = {},
appStatus = EDITOR_APP_STATUS_VALID, appStatus = EDITOR_APP_STATUS_VALID,
@ -35,6 +40,7 @@ describe('Pipeline editor tabs component', () => {
propsData: { propsData: {
ciConfigData: mockLintResponse, ciConfigData: mockLintResponse,
ciFileContent: mockCiYml, ciFileContent: mockCiYml,
isNewCiConfigFile: true,
...props, ...props,
}, },
data() { data() {
@ -47,6 +53,7 @@ describe('Pipeline editor tabs component', () => {
TextEditor: MockTextEditor, TextEditor: MockTextEditor,
EditorTab, EditorTab,
}, },
listeners,
}); });
}; };
@ -62,6 +69,7 @@ describe('Pipeline editor tabs component', () => {
const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
const findTextEditor = () => wrapper.findComponent(MockTextEditor); const findTextEditor = () => wrapper.findComponent(MockTextEditor);
const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview); const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
@ -236,4 +244,63 @@ describe('Pipeline editor tabs component', () => {
expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true); expect(findGlTabs().props('syncActiveTabWithQueryParams')).toBe(true);
}); });
}); });
describe('pipeline_editor_walkthrough experiment', () => {
describe('when in control path', () => {
beforeEach(() => {
stubExperiments({ pipeline_editor_walkthrough: 'control' });
});
it('does not show walkthrough popover', async () => {
createComponent({ mountFn: mount });
await nextTick();
expect(findWalkthroughPopover().exists()).toBe(false);
});
});
describe('when in candidate path', () => {
beforeEach(() => {
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
});
describe('when isNewCiConfigFile prop is true (default)', () => {
beforeEach(async () => {
createComponent({
mountFn: mount,
});
await nextTick();
});
it('shows walkthrough popover', async () => {
expect(findWalkthroughPopover().exists()).toBe(true);
});
});
describe('when isNewCiConfigFile prop is false', () => {
it('does not show walkthrough popover', async () => {
createComponent({ props: { isNewCiConfigFile: false }, mountFn: mount });
await nextTick();
expect(findWalkthroughPopover().exists()).toBe(false);
});
});
});
});
it('sets listeners on walkthrough popover', async () => {
stubExperiments({ pipeline_editor_walkthrough: 'candidate' });
const handler = jest.fn();
createComponent({
mountFn: mount,
listeners: {
event: handler,
},
});
await nextTick();
findWalkthroughPopover().vm.$emit('event');
expect(handler).toHaveBeenCalled();
});
}); });

View File

@ -0,0 +1,29 @@
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.config.ignoredElements = ['gl-emoji'];
describe('WalkthroughPopover component', () => {
let wrapper;
const createComponent = (mountFn = shallowMount) => {
return extendedWrapper(mountFn(WalkthroughPopover));
};
afterEach(() => {
wrapper.destroy();
});
describe('CTA button clicked', () => {
beforeEach(async () => {
wrapper = createComponent(mount);
await wrapper.findByTestId('ctaBtn').trigger('click');
});
it('emits "walkthrough-popover-cta-clicked" event', async () => {
expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy();
});
});
});

View File

@ -152,4 +152,27 @@ describe('Pipeline editor home wrapper', () => {
expect(findCommitSection().exists()).toBe(true); expect(findCommitSection().exists()).toBe(true);
}); });
}); });
describe('WalkthroughPopover events', () => {
beforeEach(() => {
createComponent();
});
describe('when "walkthrough-popover-cta-clicked" is emitted from pipeline editor tabs', () => {
it('passes down `scrollToCommitForm=true` to commit section', async () => {
expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
});
});
describe('when "scrolled-to-commit-form" is emitted from commit section', () => {
it('passes down `scrollToCommitForm=false` to commit section', async () => {
await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked');
expect(findCommitSection().props('scrollToCommitForm')).toBe(true);
await findCommitSection().vm.$emit('scrolled-to-commit-form');
expect(findCommitSection().props('scrollToCommitForm')).toBe(false);
});
});
});
}); });

View File

@ -10,6 +10,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility'; import { updateHistory } from '~/lib/utils/url_utility';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
import RunnerList from '~/runner/components/runner_list.vue'; import RunnerList from '~/runner/components/runner_list.vue';
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
@ -33,7 +34,11 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersDataPaginated } from '../mock_data'; import { runnersData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockActiveRunnersCount = 2; const mockActiveRunnersCount = '2';
const mockAllRunnersCount = '6';
const mockInstanceRunnersCount = '3';
const mockGroupRunnersCount = '2';
const mockProjectRunnersCount = '1';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/runner/sentry_utils'); jest.mock('~/runner/sentry_utils');
@ -50,6 +55,7 @@ describe('AdminRunnersApp', () => {
let mockRunnersQuery; let mockRunnersQuery;
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList); const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
const findRunnerPaginationPrev = () => const findRunnerPaginationPrev = () =>
@ -65,8 +71,12 @@ describe('AdminRunnersApp', () => {
localVue, localVue,
apolloProvider: createMockApollo(handlers), apolloProvider: createMockApollo(handlers),
propsData: { propsData: {
activeRunnersCount: mockActiveRunnersCount,
registrationToken: mockRegistrationToken, registrationToken: mockRegistrationToken,
activeRunnersCount: mockActiveRunnersCount,
allRunnersCount: mockAllRunnersCount,
instanceRunnersCount: mockInstanceRunnersCount,
groupRunnersCount: mockGroupRunnersCount,
projectRunnersCount: mockProjectRunnersCount,
...props, ...props,
}, },
}); });
@ -85,6 +95,16 @@ describe('AdminRunnersApp', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('shows the runner tabs with a runner count', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
expect(findRunnerTypeTabs().text()).toMatchInterpolatedText(
`All ${mockAllRunnersCount} Instance ${mockInstanceRunnersCount} Group ${mockGroupRunnersCount} Project ${mockProjectRunnersCount}`,
);
});
it('shows the runner setup instructions', () => { it('shows the runner setup instructions', () => {
expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken); expect(findRegistrationDropdown().props('registrationToken')).toBe(mockRegistrationToken);
expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE); expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE);

View File

@ -14,11 +14,16 @@ describe('RunnerTypeTabs', () => {
.filter((tab) => tab.attributes('active') === 'true') .filter((tab) => tab.attributes('active') === 'true')
.at(0); .at(0);
const createComponent = ({ value = mockSearch } = {}) => { const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(RunnerTypeTabs, { wrapper = shallowMount(RunnerTypeTabs, {
propsData: { propsData: {
value, value: mockSearch,
...props,
}, },
stubs: {
GlTab,
},
...options,
}); });
}; };
@ -31,7 +36,7 @@ describe('RunnerTypeTabs', () => {
}); });
it('Renders options to filter runners', () => { it('Renders options to filter runners', () => {
expect(findTabs().wrappers.map((tab) => tab.attributes('title'))).toEqual([ expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
'All', 'All',
'Instance', 'Instance',
'Group', 'Group',
@ -40,18 +45,20 @@ describe('RunnerTypeTabs', () => {
}); });
it('"All" is selected by default', () => { it('"All" is selected by default', () => {
expect(findActiveTab().attributes('title')).toBe('All'); expect(findActiveTab().text()).toBe('All');
}); });
it('Another tab can be preselected by the user', () => { it('Another tab can be preselected by the user', () => {
createComponent({ createComponent({
value: { props: {
...mockSearch, value: {
runnerType: INSTANCE_TYPE, ...mockSearch,
runnerType: INSTANCE_TYPE,
},
}, },
}); });
expect(findActiveTab().attributes('title')).toBe('Instance'); expect(findActiveTab().text()).toBe('Instance');
}); });
describe('When the user selects a tab', () => { describe('When the user selects a tab', () => {
@ -72,7 +79,31 @@ describe('RunnerTypeTabs', () => {
const newValue = emittedValue(); const newValue = emittedValue();
await wrapper.setProps({ value: newValue }); await wrapper.setProps({ value: newValue });
expect(findActiveTab().attributes('title')).toBe('Group'); expect(findActiveTab().text()).toBe('Group');
});
});
describe('When using a custom slot', () => {
const mockContent = 'content';
beforeEach(() => {
createComponent({
scopedSlots: {
title: `
<span>
{{props.tab.title}} ${mockContent}
</span>`,
},
});
});
it('Renders tabs with additional information', () => {
expect(findTabs().wrappers.map((tab) => tab.text())).toEqual([
`All ${mockContent}`,
`Instance ${mockContent}`,
`Group ${mockContent}`,
`Project ${mockContent}`,
]);
}); });
}); });
}); });

View File

@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::RunnersHelper do RSpec.describe Ci::RunnersHelper do
let_it_be(:user, refind: true) { create(:user) } let_it_be(:user) { create(:user) }
before do before do
allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:current_user).and_return(user)
@ -12,22 +12,22 @@ RSpec.describe Ci::RunnersHelper do
describe '#runner_status_icon', :clean_gitlab_redis_cache do describe '#runner_status_icon', :clean_gitlab_redis_cache do
it "returns - not contacted yet" do it "returns - not contacted yet" do
runner = create(:ci_runner) runner = create(:ci_runner)
expect(runner_status_icon(runner)).to include("not connected yet") expect(helper.runner_status_icon(runner)).to include("not connected yet")
end end
it "returns offline text" do it "returns offline text" do
runner = create(:ci_runner, contacted_at: 1.day.ago, active: true) runner = create(:ci_runner, contacted_at: 1.day.ago, active: true)
expect(runner_status_icon(runner)).to include("Runner is offline") expect(helper.runner_status_icon(runner)).to include("Runner is offline")
end end
it "returns online text" do it "returns online text" do
runner = create(:ci_runner, contacted_at: 1.second.ago, active: true) runner = create(:ci_runner, contacted_at: 1.second.ago, active: true)
expect(runner_status_icon(runner)).to include("Runner is online") expect(helper.runner_status_icon(runner)).to include("Runner is online")
end end
it "returns paused text" do it "returns paused text" do
runner = create(:ci_runner, contacted_at: 1.second.ago, active: false) runner = create(:ci_runner, contacted_at: 1.second.ago, active: false)
expect(runner_status_icon(runner)).to include("Runner is paused") expect(helper.runner_status_icon(runner)).to include("Runner is paused")
end end
end end
@ -42,7 +42,7 @@ RSpec.describe Ci::RunnersHelper do
context 'without sorting' do context 'without sorting' do
it 'returns cached value' do it 'returns cached value' do
expect(runner_contacted_at(runner)).to eq(contacted_at_cached) expect(helper.runner_contacted_at(runner)).to eq(contacted_at_cached)
end end
end end
@ -52,7 +52,7 @@ RSpec.describe Ci::RunnersHelper do
end end
it 'returns cached value' do it 'returns cached value' do
expect(runner_contacted_at(runner)).to eq(contacted_at_cached) expect(helper.runner_contacted_at(runner)).to eq(contacted_at_cached)
end end
end end
@ -62,11 +62,33 @@ RSpec.describe Ci::RunnersHelper do
end end
it 'returns stored value' do it 'returns stored value' do
expect(runner_contacted_at(runner)).to eq(contacted_at_stored) expect(helper.runner_contacted_at(runner)).to eq(contacted_at_stored)
end end
end end
end end
describe '#admin_runners_data_attributes' do
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:instance_runner) { create(:ci_runner, :instance) }
let_it_be(:project_runner) { create(:ci_runner, :project ) }
before do
allow(helper).to receive(:current_user).and_return(admin)
end
it 'returns the data in format' do
expect(helper.admin_runners_data_attributes).to eq({
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
registration_token: Gitlab::CurrentSettings.runners_registration_token,
active_runners_count: '0',
all_runners_count: '2',
instance_runners_count: '1',
group_runners_count: '0',
project_runners_count: '1'
})
end
end
describe '#group_shared_runners_settings_data' do describe '#group_shared_runners_settings_data' do
let_it_be(:parent) { create(:group) } let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) } let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) }
@ -86,7 +108,7 @@ RSpec.describe Ci::RunnersHelper do
parent_shared_runners_availability: nil parent_shared_runners_availability: nil
}.merge(runner_constants) }.merge(runner_constants)
expect(group_shared_runners_settings_data(parent)).to eq result expect(helper.group_shared_runners_settings_data(parent)).to eq result
end end
it 'returns group data for child group' do it 'returns group data for child group' do
@ -96,7 +118,7 @@ RSpec.describe Ci::RunnersHelper do
parent_shared_runners_availability: Namespace::SR_ENABLED parent_shared_runners_availability: Namespace::SR_ENABLED
}.merge(runner_constants) }.merge(runner_constants)
expect(group_shared_runners_settings_data(group)).to eq result expect(helper.group_shared_runners_settings_data(group)).to eq result
end end
end end
@ -104,7 +126,7 @@ RSpec.describe Ci::RunnersHelper do
let(:group) { create(:group) } let(:group) { create(:group) }
it 'returns group data to render a runner list' do it 'returns group data to render a runner list' do
data = group_runners_data_attributes(group) data = helper.group_runners_data_attributes(group)
expect(data[:registration_token]).to eq(group.runners_token) expect(data[:registration_token]).to eq(group.runners_token)
expect(data[:group_id]).to eq(group.id) expect(data[:group_id]).to eq(group.id)

View File

@ -53,7 +53,7 @@ RSpec.describe "deleting designs" do
context 'the designs list contains filenames we cannot find' do context 'the designs list contains filenames we cannot find' do
it_behaves_like 'a failed request' do it_behaves_like 'a failed request' do
let(:designs) { %w/foo bar baz/.map { |fn| OpenStruct.new(filename: fn) } } let(:designs) { %w/foo bar baz/.map { |fn| instance_double('file', filename: fn) } }
let(:the_error) { a_string_matching %r/filenames were not found/ } let(:the_error) { a_string_matching %r/filenames were not found/ }
end end
end end

View File

@ -201,8 +201,8 @@ RSpec.configure do |config|
# Do not retry controller tests because rspec-retry cannot properly # Do not retry controller tests because rspec-retry cannot properly
# reset the controller which may contain data from last attempt. See # reset the controller which may contain data from last attempt. See
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73360 # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73360
config.around(:each, type: :controller) do |example| config.prepend_before(:each, type: :controller) do |example|
example.run_with_retry(retry: 1) example.metadata[:retry] = 1
end end
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation] config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]

View File

@ -193,6 +193,54 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'config/metrics/schema.json' | [:product_intelligence] 'config/metrics/schema.json' | [:product_intelligence]
'doc/api/usage_data.md' | [:product_intelligence] 'doc/api/usage_data.md' | [:product_intelligence]
'spec/lib/gitlab/usage_data_spec.rb' | [:product_intelligence] 'spec/lib/gitlab/usage_data_spec.rb' | [:product_intelligence]
'app/models/integration.rb' | [:integrations_be, :backend]
'ee/app/models/integrations/github.rb' | [:integrations_be, :backend]
'ee/app/models/ee/integrations/jira.rb' | [:integrations_be, :backend]
'app/models/integrations/chat_message/pipeline_message.rb' | [:integrations_be, :backend]
'app/models/jira_connect_subscription.rb' | [:integrations_be, :backend]
'app/models/hooks/service_hook.rb' | [:integrations_be, :backend]
'ee/app/models/ee/hooks/system_hook.rb' | [:integrations_be, :backend]
'app/services/concerns/integrations/project_test_data.rb' | [:integrations_be, :backend]
'ee/app/services/ee/integrations/test/project_service.rb' | [:integrations_be, :backend]
'app/controllers/concerns/integrations/actions.rb' | [:integrations_be, :backend]
'ee/app/controllers/concerns/ee/integrations/params.rb' | [:integrations_be, :backend]
'ee/app/controllers/projects/integrations/jira/issues_controller.rb' | [:integrations_be, :backend]
'app/controllers/projects/hooks_controller.rb' | [:integrations_be, :backend]
'app/controllers/admin/hook_logs_controller.rb' | [:integrations_be, :backend]
'app/controllers/groups/settings/integrations_controller.rb' | [:integrations_be, :backend]
'app/controllers/jira_connect/branches_controller.rb' | [:integrations_be, :backend]
'app/controllers/oauth/jira/authorizations_controller.rb' | [:integrations_be, :backend]
'ee/app/finders/projects/integrations/jira/by_ids_finder.rb' | [:integrations_be, :database, :backend]
'app/workers/jira_connect/sync_merge_request_worker.rb' | [:integrations_be, :backend]
'app/workers/propagate_integration_inherit_worker.rb' | [:integrations_be, :backend]
'app/workers/web_hooks/log_execution_worker.rb' | [:integrations_be, :backend]
'app/workers/web_hook_worker.rb' | [:integrations_be, :backend]
'app/workers/project_service_worker.rb' | [:integrations_be, :backend]
'lib/atlassian/jira_connect/serializers/commit_entity.rb' | [:integrations_be, :backend]
'lib/api/entities/project_integration.rb' | [:integrations_be, :backend]
'lib/gitlab/hook_data/note_builder.rb' | [:integrations_be, :backend]
'lib/gitlab/data_builder/note.rb' | [:integrations_be, :backend]
'ee/lib/ee/gitlab/integrations/sti_type.rb' | [:integrations_be, :backend]
'ee/lib/ee/api/helpers/integrations_helpers.rb' | [:integrations_be, :backend]
'ee/app/serializers/integrations/jira_serializers/issue_entity.rb' | [:integrations_be, :backend]
'lib/api/github/entities.rb' | [:integrations_be, :backend]
'lib/api/v3/github.rb' | [:integrations_be, :backend]
'app/models/clusters/integrations/elastic_stack.rb' | [:backend]
'app/controllers/clusters/integrations_controller.rb' | [:backend]
'app/services/clusters/integrations/prometheus_health_check_service.rb' | [:backend]
'app/graphql/types/alert_management/integration_type.rb' | [:backend]
'app/views/jira_connect/branches/new.html.haml' | [:integrations_fe, :frontend]
'app/views/layouts/jira_connect.html.haml' | [:integrations_fe, :frontend]
'app/assets/javascripts/jira_connect/branches/pages/index.vue' | [:integrations_fe, :frontend]
'ee/app/views/projects/integrations/jira/issues/show.html.haml' | [:integrations_fe, :frontend]
'ee/app/assets/javascripts/integrations/zentao/issues_list/graphql/queries/get_zentao_issues.query.graphql' | [:integrations_fe, :frontend]
'app/assets/javascripts/pages/projects/settings/integrations/show/index.js' | [:integrations_fe, :frontend]
'ee/app/assets/javascripts/pages/groups/hooks/index.js' | [:integrations_fe, :frontend]
'app/views/clusters/clusters/_integrations_tab.html.haml' | [:frontend]
'app/assets/javascripts/alerts_settings/graphql/fragments/integration_item.fragment.graphql' | [:frontend]
'app/assets/javascripts/filtered_search/droplab/hook_input.js' | [:frontend]
end end
with_them do with_them do
@ -212,6 +260,11 @@ RSpec.describe Tooling::Danger::ProjectHelper do
[:backend, :product_intelligence] | '+ count(User.active)' | ['lib/gitlab/usage_data/topology.rb'] [:backend, :product_intelligence] | '+ count(User.active)' | ['lib/gitlab/usage_data/topology.rb']
[:backend, :product_intelligence] | '+ foo_count(User.active)' | ['lib/gitlab/usage_data.rb'] [:backend, :product_intelligence] | '+ foo_count(User.active)' | ['lib/gitlab/usage_data.rb']
[:backend] | '+ count(User.active)' | ['user.rb'] [:backend] | '+ count(User.active)' | ['user.rb']
[:integrations_be, :database, :migration] | '+ add_column :integrations, :foo, :text' | ['db/migrate/foo.rb']
[:integrations_be, :database, :migration] | '+ create_table :zentao_tracker_data do |t|' | ['ee/db/post_migrate/foo.rb']
[:integrations_be, :backend] | '+ Integrations::Foo' | ['app/foo/bar.rb']
[:integrations_be, :backend] | '+ project.execute_hooks(foo, :bar)' | ['ee/lib/ee/foo.rb']
[:integrations_be, :backend] | '+ project.execute_integrations(foo, :bar)' | ['app/foo.rb']
end end
with_them do with_them do

View File

@ -44,6 +44,28 @@ module Tooling
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs, %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
%r{\Adata/whats_new/} => :docs, %r{\Adata/whats_new/} => :docs,
%r{\A((ee|jh)/)?app/finders/(.+/)?integrations/} => [:integrations_be, :database, :backend],
[%r{\A((ee|jh)/)?db/(geo/)?(migrate|post_migrate)/}, %r{(:integrations|:\w+_tracker_data)\b}] => [:integrations_be, :database, :migration],
[%r{\A((ee|jh)/)?(app|lib)/.+\.rb}, %r{\b(Integrations::|\.execute_(integrations|hooks))\b}] => [:integrations_be, :backend],
%r{\A(
((ee|jh)/)?app/((?!.*clusters)(?!.*alert_management)(?!.*views)(?!.*assets).+/)?integration.+ |
((ee|jh)/)?app/((?!.*search).+/)?project_service.+ |
((ee|jh)/)?app/(models|helpers|workers|services|controllers)/(.+/)?(jira_connect.+|.*hook.+) |
((ee|jh)/)?app/controllers/(.+/)?oauth/jira/.+ |
((ee|jh)/)?app/services/(.+/)?jira.+ |
((ee|jh)/)?app/workers/(.+/)?(propagate_integration.+|irker_worker\.rb) |
((ee|jh)/)?lib/(.+/)?(atlassian|data_builder|hook_data)/.+ |
((ee|jh)/)?lib/(.+/)?.*integration.+ |
((ee|jh)/)?lib/(.+/)?api/v3/github\.rb |
((ee|jh)/)?lib/(.+/)?api/github/entities\.rb
)\z}x => [:integrations_be, :backend],
%r{\A(
((ee|jh)/)?app/(views|assets)/((?!.*clusters)(?!.*alerts_settings).+/)?integration.+ |
((ee|jh)/)?app/(views|assets)/(.+/)?jira_connect.+ |
((ee|jh)/)?app/(views|assets)/((?!.*filtered_search).+/)?hooks?.+
)\z}x => [:integrations_fe, :frontend],
%r{\A( %r{\A(
app/assets/javascripts/tracking/.*\.js | app/assets/javascripts/tracking/.*\.js |
spec/frontend/tracking/.*\.js | spec/frontend/tracking/.*\.js |