Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b70394d26f
commit
c70a70ea42
|
|
@ -2113,6 +2113,7 @@ Gitlab/NamespacedClass:
|
|||
- 'ee/app/models/weight_note.rb'
|
||||
- 'ee/app/policies/approval_merge_request_rule_policy.rb'
|
||||
- 'ee/app/policies/approval_project_rule_policy.rb'
|
||||
- 'ee/app/policies/approval_state_policy.rb'
|
||||
- 'ee/app/policies/dast_scanner_profile_policy.rb'
|
||||
- 'ee/app/policies/dast_site_profile_policy.rb'
|
||||
- 'ee/app/policies/dast_site_validation_policy.rb'
|
||||
|
|
|
|||
|
|
@ -1 +1,19 @@
|
|||
export { BulletList as default } from '@tiptap/extension-bullet-list';
|
||||
import { BulletList } from '@tiptap/extension-bullet-list';
|
||||
import { getMarkdownSource } from '../services/markdown_sourcemap';
|
||||
|
||||
export default BulletList.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
|
||||
bullet: {
|
||||
default: '*',
|
||||
parseHTML(element) {
|
||||
const bullet = getMarkdownSource(element)?.charAt(0);
|
||||
|
||||
return { bullet: '*+-'.includes(bullet) ? bullet : '*' };
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,8 +118,6 @@ const defaultSerializerConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
|
||||
|
||||
/**
|
||||
* A markdown serializer converts arbitrary Markdown content
|
||||
* into a ProseMirror document and viceversa. To convert Markdown
|
||||
|
|
@ -144,15 +142,15 @@ export default ({ render = () => null, serializerConfig = {} } = {}) => ({
|
|||
deserialize: async ({ schema, content }) => {
|
||||
const html = await render(content);
|
||||
|
||||
if (!html) {
|
||||
return null;
|
||||
}
|
||||
if (!html) return null;
|
||||
|
||||
const parser = new DOMParser();
|
||||
const {
|
||||
body: { firstElementChild },
|
||||
} = parser.parseFromString(wrapHtmlPayload(html), 'text/html');
|
||||
const state = ProseMirrorDOMParser.fromSchema(schema).parse(firstElementChild);
|
||||
const { body } = parser.parseFromString(html, 'text/html');
|
||||
|
||||
// append original source as a comment that nodes can access
|
||||
body.append(document.createComment(content));
|
||||
|
||||
const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
|
||||
|
||||
return state.toJSON();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
const getFullSource = (element) => {
|
||||
const commentNode = element.ownerDocument.body.lastChild;
|
||||
|
||||
if (commentNode.nodeName === '#comment') {
|
||||
return commentNode.textContent.split('\n');
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const getRangeFromSourcePos = (sourcePos) => {
|
||||
const [start, end] = sourcePos.split('-');
|
||||
const [startRow, startCol] = start.split(':');
|
||||
const [endRow, endCol] = end.split(':');
|
||||
|
||||
return {
|
||||
start: { row: Number(startRow) - 1, col: Number(startCol) - 1 },
|
||||
end: { row: Number(endRow) - 1, col: Number(endCol) - 1 },
|
||||
};
|
||||
};
|
||||
|
||||
export const getMarkdownSource = (element) => {
|
||||
if (!element.dataset.sourcepos) return undefined;
|
||||
|
||||
const source = getFullSource(element);
|
||||
const range = getRangeFromSourcePos(element.dataset.sourcepos);
|
||||
let elSource = '';
|
||||
|
||||
for (let i = range.start.row; i <= range.end.row; i += 1) {
|
||||
if (i === range.start.row) {
|
||||
elSource += source[i]?.substring(range.start.col);
|
||||
} else if (i === range.end.row) {
|
||||
elSource += `\n${source[i]?.substring(0, range.start.col)}`;
|
||||
} else {
|
||||
elSource += `\n${source[i]}` || '';
|
||||
}
|
||||
}
|
||||
|
||||
return elSource.trim();
|
||||
};
|
||||
|
|
@ -98,12 +98,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'fetchCycleAnalyticsData',
|
||||
'fetchStageData',
|
||||
'setSelectedStage',
|
||||
'setDateRange',
|
||||
]),
|
||||
...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']),
|
||||
handleDateSelect(daysInPast) {
|
||||
this.setDateRange(daysInPast);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
fragment VersionListItem on DesignVersion {
|
||||
id
|
||||
sha
|
||||
createdAt
|
||||
author {
|
||||
__typename
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
#import "../fragments/design.fragment.graphql"
|
||||
#import "../fragments/version.fragment.graphql"
|
||||
|
||||
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
|
||||
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
|
||||
designs {
|
||||
...DesignItem
|
||||
versions {
|
||||
__typename
|
||||
nodes {
|
||||
id
|
||||
sha
|
||||
__typename
|
||||
...VersionListItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ export const designUploadOptimisticResponse = (files) => {
|
|||
__typename: 'DesignVersion',
|
||||
id: -uniqueId(),
|
||||
sha: -uniqueId(),
|
||||
createdAt: '',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: -uniqueId(),
|
||||
name: '',
|
||||
avatarUrl: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ const KNOWN_TYPES = [
|
|||
},
|
||||
];
|
||||
|
||||
export function isTextFile({ name, raw, content, mimeType = '' }) {
|
||||
export function isTextFile({ name, raw, binary, content, mimeType = '' }) {
|
||||
// some file objects already have a `binary` property set on them. If true, return false
|
||||
if (binary) return false;
|
||||
|
||||
const knownType = KNOWN_TYPES.find((type) => type.isMatch(mimeType, name));
|
||||
if (knownType) return knownType.isText;
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']),
|
||||
...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
|
||||
...mapGetters([
|
||||
'isLoading',
|
||||
'isImportingAnyRepo',
|
||||
|
|
@ -43,7 +43,7 @@ export default {
|
|||
]),
|
||||
|
||||
pagePaginationStateKey() {
|
||||
return `${this.filter}-${this.repositories.length}`;
|
||||
return `${this.filter}-${this.repositories.length}-${this.pageInfo.page}`;
|
||||
},
|
||||
|
||||
availableNamespaces() {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ const importAll = ({ state, dispatch }) => {
|
|||
|
||||
const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
|
||||
const nextPage = state.pageInfo.page + 1;
|
||||
commit(types.SET_PAGE, nextPage);
|
||||
commit(types.REQUEST_REPOS);
|
||||
|
||||
const { provider, filter } = state;
|
||||
|
|
@ -67,11 +66,10 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
|
|||
}),
|
||||
)
|
||||
.then(({ data }) => {
|
||||
commit(types.SET_PAGE, nextPage);
|
||||
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
|
||||
})
|
||||
.catch((e) => {
|
||||
commit(types.SET_PAGE, nextPage - 1);
|
||||
|
||||
if (hasRedirectInError(e)) {
|
||||
redirectToUrlInError(e);
|
||||
} else if (tooManyRequests(e)) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const makeNewImportedProject = (importedProject) => ({
|
|||
sanitizedName: importedProject.name,
|
||||
providerLink: importedProject.providerLink,
|
||||
},
|
||||
importedProject,
|
||||
importedProject: { ...importedProject },
|
||||
});
|
||||
|
||||
const makeNewIncompatibleProject = (project) => ({
|
||||
|
|
@ -63,15 +63,16 @@ export default {
|
|||
factory: makeNewIncompatibleProject,
|
||||
});
|
||||
|
||||
state.repositories = [
|
||||
...newImportedProjects,
|
||||
...state.repositories,
|
||||
...repositories.providerRepos.map((project) => ({
|
||||
const existingProjects = [...newImportedProjects, ...state.repositories];
|
||||
const existingProjectNames = new Set(existingProjects.map((p) => p.importSource.fullName));
|
||||
const newProjects = repositories.providerRepos
|
||||
.filter((project) => !existingProjectNames.has(project.fullName))
|
||||
.map((project) => ({
|
||||
importSource: project,
|
||||
importedProject: null,
|
||||
})),
|
||||
...newIncompatibleProjects,
|
||||
];
|
||||
}));
|
||||
|
||||
state.repositories = [...existingProjects, ...newProjects, ...newIncompatibleProjects];
|
||||
|
||||
if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) {
|
||||
state.pageInfo.page -= 1;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { GlBanner } from '@gitlab/ui';
|
|||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { setCookie } from '~/lib/utils/common_utils';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { EVENT_LABEL, DISMISS_EVENT, CLICK_EVENT } from '../constants';
|
||||
|
||||
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
|
||||
|
||||
export default {
|
||||
name: 'TerraformNotification',
|
||||
|
|
@ -16,6 +20,7 @@ export default {
|
|||
components: {
|
||||
GlBanner,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
inject: ['terraformImagePath', 'bannerDismissedKey'],
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -31,6 +36,10 @@ export default {
|
|||
handleClose() {
|
||||
setCookie(this.bannerDismissedKey, true);
|
||||
this.isVisible = false;
|
||||
this.track(DISMISS_EVENT);
|
||||
},
|
||||
buttonClick() {
|
||||
this.track(CLICK_EVENT);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -43,6 +52,7 @@ export default {
|
|||
:button-link="docsUrl"
|
||||
:svg-path="terraformImagePath"
|
||||
variant="promotion"
|
||||
@primary="buttonClick"
|
||||
@close="handleClose"
|
||||
>
|
||||
<p>{{ $options.i18n.description }}</p>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export const EVENT_LABEL = 'terraform_banner';
|
||||
export const DISMISS_EVENT = 'dismiss_banner';
|
||||
export const CLICK_EVENT = 'click_button';
|
||||
|
|
@ -3,293 +3,4 @@
|
|||
.cycle-analytics {
|
||||
margin: 24px auto 0;
|
||||
position: relative;
|
||||
|
||||
.landing {
|
||||
margin-top: 0;
|
||||
|
||||
.inner-content {
|
||||
white-space: normal;
|
||||
|
||||
h4,
|
||||
p {
|
||||
margin: 7px 0 0;
|
||||
max-width: 480px;
|
||||
padding: 0 $gl-padding;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svg-container svg {
|
||||
width: 136px;
|
||||
height: 136px;
|
||||
}
|
||||
}
|
||||
|
||||
.col-headers {
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
.content-block {
|
||||
padding: 24px 0;
|
||||
border-bottom: 0;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
padding: 6px 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
text-align: center;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 30px;
|
||||
line-height: 38px;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--gray-500, $gray-500);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@include media-breakpoint-down(xs) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stage-panel-body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stage-nav,
|
||||
.stage-entries {
|
||||
display: flex;
|
||||
vertical-align: top;
|
||||
font-size: $gl-font-size;
|
||||
}
|
||||
|
||||
.stage-nav {
|
||||
width: 40%;
|
||||
margin-bottom: 0;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.stage-nav-item {
|
||||
line-height: 65px;
|
||||
|
||||
&.active {
|
||||
background: var(--blue-50, $blue-50);
|
||||
border-color: var(--blue-300, $blue-300);
|
||||
box-shadow: inset 4px 0 0 0 var(--blue-500, $blue-500);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background-color: var(--gray-10, $gray-10);
|
||||
box-shadow: inset 2px 0 0 0 var(--border-color, $border-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stage-nav-item-cell.stage-name {
|
||||
width: 44.5%;
|
||||
}
|
||||
|
||||
.stage-nav-item-cell.stage-median {
|
||||
min-width: 43%;
|
||||
}
|
||||
|
||||
.stage-empty,
|
||||
.not-available {
|
||||
color: var(--gray-500, $gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stage-panel-container {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.stage-panel {
|
||||
min-width: 968px;
|
||||
|
||||
.card-header {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.events-description {
|
||||
line-height: 65px;
|
||||
}
|
||||
|
||||
.events-info {
|
||||
color: var(--gray-500, $gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.stage-events {
|
||||
min-height: 467px;
|
||||
}
|
||||
|
||||
.stage-event-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stage-event-item {
|
||||
@include clearfix;
|
||||
list-style-type: none;
|
||||
padding-bottom: $gl-padding;
|
||||
margin-bottom: $gl-padding;
|
||||
border-bottom: 1px solid var(--gray-50, $gray-50);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item-details,
|
||||
.item-time {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
margin: 0 0 2px;
|
||||
|
||||
&.issue-title,
|
||||
&.commit-title,
|
||||
&.merge-request-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-time {
|
||||
width: 25%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-time {
|
||||
font-size: $cycle-analytics-big-font;
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
|
||||
span {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
font-size: $gl-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.issue-date,
|
||||
.build-date {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
}
|
||||
|
||||
.mr-link,
|
||||
.issue-link,
|
||||
.commit-author-link,
|
||||
.issue-author-link {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
}
|
||||
|
||||
// Custom CSS for components
|
||||
.item-conmmit-component {
|
||||
.commit-icon {
|
||||
svg {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.merge-request-branch {
|
||||
a {
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Styles for stage items
|
||||
.item-build-component {
|
||||
.item-title {
|
||||
.icon-build-status {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.item-build-name {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
}
|
||||
|
||||
.pipeline-id {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
padding: 0 3px 0 0;
|
||||
}
|
||||
|
||||
.ref-name {
|
||||
color: var(--gray-900, $gray-900);
|
||||
display: inline-block;
|
||||
max-width: 180px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.commit-sha {
|
||||
color: var(--blue-600, $blue-600);
|
||||
line-height: 1.3;
|
||||
vertical-align: top;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,14 +62,10 @@ class Import::BitbucketController < Import::BaseController
|
|||
|
||||
protected
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
override :importable_repos
|
||||
def importable_repos
|
||||
already_added_projects_names = already_added_projects.map(&:import_source)
|
||||
|
||||
bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) || !repo.valid? }
|
||||
bitbucket_repos.filter { |repo| repo.valid? }
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
override :incompatible_repos
|
||||
def incompatible_repos
|
||||
|
|
|
|||
|
|
@ -62,16 +62,10 @@ class Import::BitbucketServerController < Import::BaseController
|
|||
|
||||
protected
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
override :importable_repos
|
||||
def importable_repos
|
||||
# Use the import URL to filter beyond what BaseService#find_already_added_projects
|
||||
already_added_projects = filter_added_projects('bitbucket_server', bitbucket_repos.map(&:browse_url))
|
||||
already_added_projects_names = already_added_projects.map(&:import_source)
|
||||
|
||||
bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.browse_url) || !repo.valid? }
|
||||
bitbucket_repos.filter { |repo| repo.valid? }
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
override :incompatible_repos
|
||||
def incompatible_repos
|
||||
|
|
@ -90,12 +84,6 @@ class Import::BitbucketServerController < Import::BaseController
|
|||
|
||||
private
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def filter_added_projects(import_type, import_sources)
|
||||
current_user.created_projects.where(import_type: import_type, import_source: import_sources).with_import_state
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def client
|
||||
@client ||= BitbucketServer::Client.new(credentials)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -74,16 +74,10 @@ class Import::FogbugzController < Import::BaseController
|
|||
|
||||
protected
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
override :importable_repos
|
||||
def importable_repos
|
||||
repos = client.repos
|
||||
|
||||
already_added_projects_names = already_added_projects.map(&:import_source)
|
||||
|
||||
repos.reject { |repo| already_added_projects_names.include? repo.name }
|
||||
client.repos
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
override :incompatible_repos
|
||||
def incompatible_repos
|
||||
|
|
|
|||
|
|
@ -64,9 +64,7 @@ class Import::GithubController < Import::BaseController
|
|||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
override :importable_repos
|
||||
def importable_repos
|
||||
already_added_projects_names = already_added_projects.pluck(:import_source)
|
||||
|
||||
client_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) }
|
||||
client_repos.to_a
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
|||
|
|
@ -39,16 +39,10 @@ class Import::GitlabController < Import::BaseController
|
|||
|
||||
protected
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
override :importable_repos
|
||||
def importable_repos
|
||||
repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
|
||||
|
||||
already_added_projects_names = already_added_projects.map(&:import_source)
|
||||
|
||||
repos.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
|
||||
client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
override :incompatible_repos
|
||||
def incompatible_repos
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class Import::ManifestController < Import::BaseController
|
|||
end
|
||||
|
||||
def create
|
||||
repository = repositories.find do |project|
|
||||
repository = importable_repos.find do |project|
|
||||
project[:id] == params[:repo_id].to_i
|
||||
end
|
||||
|
||||
|
|
@ -56,14 +56,10 @@ class Import::ManifestController < Import::BaseController
|
|||
|
||||
protected
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
override :importable_repos
|
||||
def importable_repos
|
||||
already_added_projects_names = already_added_projects.pluck(:import_url)
|
||||
|
||||
repositories.reject { |repo| already_added_projects_names.include?(repo[:url]) }
|
||||
@importable_repos ||= manifest_import_metadata.repositories
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
override :incompatible_repos
|
||||
def incompatible_repos
|
||||
|
|
@ -88,7 +84,7 @@ class Import::ManifestController < Import::BaseController
|
|||
private
|
||||
|
||||
def ensure_import_vars
|
||||
unless group && repositories.present?
|
||||
unless group && importable_repos.present?
|
||||
redirect_to(new_import_manifest_path)
|
||||
end
|
||||
end
|
||||
|
|
@ -103,10 +99,6 @@ class Import::ManifestController < Import::BaseController
|
|||
@manifest_import_status ||= Gitlab::ManifestImport::Metadata.new(current_user, fallback: session)
|
||||
end
|
||||
|
||||
def repositories
|
||||
@repositories ||= manifest_import_metadata.repositories
|
||||
end
|
||||
|
||||
def find_jobs
|
||||
find_already_added_projects.to_json(only: [:id], methods: [:import_status])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GroupsHelper
|
||||
def group_sidebar_links
|
||||
@group_sidebar_links ||= get_group_sidebar_links
|
||||
end
|
||||
|
||||
def group_sidebar_link?(link)
|
||||
group_sidebar_links.include?(link)
|
||||
end
|
||||
|
||||
def can_change_group_visibility_level?(group)
|
||||
can?(current_user, :change_visibility_level, group)
|
||||
end
|
||||
|
|
@ -33,20 +25,6 @@ module GroupsHelper
|
|||
Ability.allowed?(current_user, :admin_group_member, group)
|
||||
end
|
||||
|
||||
def group_issues_count(state:)
|
||||
IssuesFinder
|
||||
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
|
||||
.execute
|
||||
.count
|
||||
end
|
||||
|
||||
def group_merge_requests_count(state:)
|
||||
MergeRequestsFinder
|
||||
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
|
||||
.execute
|
||||
.count
|
||||
end
|
||||
|
||||
def group_dependency_proxy_image_prefix(group)
|
||||
# The namespace path can include uppercase letters, which
|
||||
# Docker doesn't allow. The proxy expects it to be downcased.
|
||||
|
|
@ -181,36 +159,6 @@ module GroupsHelper
|
|||
group.member_count > 1 || group.members_with_parents.count > 1
|
||||
end
|
||||
|
||||
def get_group_sidebar_links
|
||||
links = [:overview, :group_members]
|
||||
|
||||
resources = [:activity, :issues, :boards, :labels, :milestones,
|
||||
:merge_requests]
|
||||
links += resources.select do |resource|
|
||||
can?(current_user, "read_group_#{resource}".to_sym, @group)
|
||||
end
|
||||
|
||||
# TODO Proper policies, such as `read_group_runners, should be implemented per
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
|
||||
if can?(current_user, :admin_group, @group) && Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
|
||||
links << :runners
|
||||
end
|
||||
|
||||
if can?(current_user, :read_cluster, @group)
|
||||
links << :kubernetes
|
||||
end
|
||||
|
||||
if can?(current_user, :admin_group, @group)
|
||||
links << :settings
|
||||
end
|
||||
|
||||
if can?(current_user, :read_wiki, @group)
|
||||
links << :wiki
|
||||
end
|
||||
|
||||
links
|
||||
end
|
||||
|
||||
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
|
||||
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
|
||||
icon = group_icon(group, class: "avatar-tile", width: 15, height: 15) if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class OnboardingProgress < ApplicationRecord
|
|||
def onboard(namespace)
|
||||
return unless root_namespace?(namespace)
|
||||
|
||||
safe_find_or_create_by(namespace: namespace)
|
||||
create(namespace: namespace)
|
||||
end
|
||||
|
||||
def onboarding?(namespace)
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
- unless public_visibility_restricted?
|
||||
= link_to _("Explore"), explore_root_path
|
||||
= link_to _("Help"), help_path
|
||||
= link_to _("About GitLab"), "https://about.gitlab.com/"
|
||||
= link_to _("About GitLab"), "https://#{ApplicationHelper.promo_host}"
|
||||
= footer_message
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
-# We're migration the group sidebar to a logical model based structure. If you need to update
|
||||
-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_group_menus.html.haml.
|
||||
= render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(@group, current_user))
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
= render 'shared/sidebar_toggle_button'
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: add_namespace_and_project_to_snowplow_tracking
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68277
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338670
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::product intelligence
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: roadmap_daterange_filter
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55639
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323917
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::product planning
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FastGettext.add_text_domain 'gitlab',
|
||||
path: File.join(Rails.root, 'locale'),
|
||||
type: :po,
|
||||
ignore_fuzzy: true
|
||||
translation_repositories = [
|
||||
FastGettext::TranslationRepository.build(
|
||||
'gitlab',
|
||||
path: File.join(Rails.root, 'locale'),
|
||||
type: :po,
|
||||
ignore_fuzzy: true
|
||||
)
|
||||
]
|
||||
|
||||
Gitlab.jh do
|
||||
translation_repositories.unshift(
|
||||
FastGettext::TranslationRepository.build(
|
||||
'gitlab',
|
||||
path: File.join(Rails.root, 'jh', 'locale'),
|
||||
type: :po,
|
||||
ignore_fuzzy: true
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
FastGettext.add_text_domain(
|
||||
'gitlab',
|
||||
type: :chain,
|
||||
chain: translation_repositories,
|
||||
ignore_fuzzy: true
|
||||
)
|
||||
|
||||
FastGettext.default_text_domain = 'gitlab'
|
||||
FastGettext.default_locale = :en
|
||||
|
|
|
|||
|
|
@ -577,9 +577,9 @@ panel_groups:
|
|||
|
||||
See [Environment Dashboard](../ci/environments/environments_dashboard.md#adding-a-project-to-the-dashboard) for the maximum number of displayed projects.
|
||||
|
||||
## Environment data on Deploy Boards
|
||||
## Environment data on deploy boards
|
||||
|
||||
[Deploy Boards](../user/project/deploy_boards.md) load information from Kubernetes about
|
||||
[Deploy boards](../user/project/deploy_boards.md) load information from Kubernetes about
|
||||
Pods and Deployments. However, data over 10 MB for a certain environment read from
|
||||
Kubernetes won't be shown.
|
||||
|
||||
|
|
|
|||
|
|
@ -7568,9 +7568,19 @@ Describes a rule for who can approve merge requests.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="approvalruleapprovalsrequired"></a>`approvalsRequired` | [`Int`](#int) | Number of required approvals. |
|
||||
| <a id="approvalruleapproved"></a>`approved` | [`Boolean`](#boolean) | Indicates if the rule is satisfied. |
|
||||
| <a id="approvalruleapprovedby"></a>`approvedBy` | [`UserCoreConnection`](#usercoreconnection) | List of users defined in the rule that approved the merge request. (see [Connections](#connections)) |
|
||||
| <a id="approvalrulecontainshiddengroups"></a>`containsHiddenGroups` | [`Boolean`](#boolean) | Indicates if the rule contains approvers from a hidden group. |
|
||||
| <a id="approvalruleeligibleapprovers"></a>`eligibleApprovers` | [`UserCoreConnection`](#usercoreconnection) | List of all users eligible to approve the merge request (defined explicitly and from associated groups). (see [Connections](#connections)) |
|
||||
| <a id="approvalrulegroups"></a>`groups` | [`GroupConnection`](#groupconnection) | List of groups added as approvers for the rule. (see [Connections](#connections)) |
|
||||
| <a id="approvalruleid"></a>`id` | [`GlobalID!`](#globalid) | ID of the rule. |
|
||||
| <a id="approvalrulename"></a>`name` | [`String`](#string) | Name of the rule. |
|
||||
| <a id="approvalruleoverridden"></a>`overridden` | [`Boolean`](#boolean) | Indicates if the rule was overridden for the merge request. |
|
||||
| <a id="approvalrulesection"></a>`section` | [`String`](#string) | Named section of the Code Owners file that the rule applies to. |
|
||||
| <a id="approvalrulesourcerule"></a>`sourceRule` | [`ApprovalRule`](#approvalrule) | Source rule used to create the rule. |
|
||||
| <a id="approvalruletype"></a>`type` | [`ApprovalRuleType`](#approvalruletype) | Type of the rule. |
|
||||
| <a id="approvalruleusers"></a>`users` | [`UserCoreConnection`](#usercoreconnection) | List of users added as approvers for the rule. (see [Connections](#connections)) |
|
||||
|
||||
### `AwardEmoji`
|
||||
|
||||
|
|
@ -10600,6 +10610,7 @@ Maven metadata.
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mergerequestallowcollaboration"></a>`allowCollaboration` | [`Boolean`](#boolean) | Indicates if members of the target project can push to the fork. |
|
||||
| <a id="mergerequestapprovalstate"></a>`approvalState` | [`MergeRequestApprovalState!`](#mergerequestapprovalstate) | Information relating to rules that must be satisfied to merge this merge request. |
|
||||
| <a id="mergerequestapprovalsleft"></a>`approvalsLeft` | [`Int`](#int) | Number of approvals left. |
|
||||
| <a id="mergerequestapprovalsrequired"></a>`approvalsRequired` | [`Int`](#int) | Number of approvals required. |
|
||||
| <a id="mergerequestapproved"></a>`approved` | [`Boolean!`](#boolean) | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. |
|
||||
|
|
@ -10746,6 +10757,17 @@ Returns [`String!`](#string).
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="mergerequestreferencefull"></a>`full` | [`Boolean`](#boolean) | Boolean option specifying whether the reference should be returned in full. |
|
||||
|
||||
### `MergeRequestApprovalState`
|
||||
|
||||
Information relating to rules that must be satisfied to merge this merge request.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mergerequestapprovalstateapprovalrulesoverwritten"></a>`approvalRulesOverwritten` | [`Boolean`](#boolean) | Indicates if the merge request approval rules are overwritten for the merge request. |
|
||||
| <a id="mergerequestapprovalstaterules"></a>`rules` | [`[ApprovalRule!]`](#approvalrule) | List of approval rules associated with the merge request. |
|
||||
|
||||
### `MergeRequestAssignee`
|
||||
|
||||
A user assigned to a merge request.
|
||||
|
|
|
|||
|
|
@ -732,7 +732,7 @@ the `review/feature-1` spec takes precedence over `review/*` and `*` specs.
|
|||
|
||||
- [Use GitLab CI to deploy to multiple environments (blog post)](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/)
|
||||
- [Review Apps](../review_apps/index.md): Use dynamic environments to deploy your code for every branch.
|
||||
- [Deploy Boards](../../user/project/deploy_boards.md): View the status of your applications running on Kubernetes.
|
||||
- [Deploy boards](../../user/project/deploy_boards.md): View the status of your applications running on Kubernetes.
|
||||
- [Protected environments](protected_environments.md): Determine who can deploy code to your environments.
|
||||
- [Environments Dashboard](../environments/environments_dashboard.md): View a summary of each
|
||||
environment's operational health. **(PREMIUM)**
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ Its feature set is listed on the table below according to DevOps stages.
|
|||
| [Auto Deploy](../topics/autodevops/stages.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
|
||||
| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
|
||||
| [Canary Deployments](../user/project/canary_deployments.md) | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
|
||||
| [Deploy Boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
|
||||
| [Deploy boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
|
||||
| [Feature Flags](../operations/feature_flags.md) **(PREMIUM)** | Deploy your features behind Feature Flags. |
|
||||
| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
|
||||
| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ Do not use. Instead, use **select** with buttons, links, menu items, and lists.
|
|||
|
||||
Do not use when talking about the product or its features. The documentation describes the product as it is today. ([Vale](../testing.md#vale) rule: [`CurrentStatus.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/CurrentStatus.yml))
|
||||
|
||||
## deploy board
|
||||
|
||||
Lowercase.
|
||||
|
||||
## Developer
|
||||
|
||||
When writing about the Developer role:
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ you to common environment tasks:
|
|||
- **Stop environment** (**{stop}**) - For more information, see
|
||||
[Stopping an environment](../../ci/environments/index.md#stop-an-environment)
|
||||
|
||||
GitLab displays the [Deploy Board](../../user/project/deploy_boards.md) below the
|
||||
GitLab displays the [deploy board](../../user/project/deploy_boards.md) below the
|
||||
environment's information, with squares representing pods in your
|
||||
Kubernetes cluster, color-coded to show their status. Hovering over a square on
|
||||
the deploy board displays the state of the deployment, and selecting the square
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ In order to:
|
|||
[deploy to a Kubernetes cluster](../project/clusters/index.md#deploying-to-a-kubernetes-cluster)
|
||||
successfully.
|
||||
- Show pod usage correctly, you must
|
||||
[enable Deploy Boards](../project/deploy_boards.md#enabling-deploy-boards).
|
||||
[enable deploy boards](../project/deploy_boards.md#enabling-deploy-boards).
|
||||
|
||||
After you have successful deployments to your group-level or instance-level cluster:
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ With GitLab Enterprise Edition, you can also:
|
|||
- [Mirror a repository](project/repository/repository_mirroring.md) from elsewhere on your local server.
|
||||
- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipelines](../ci/pipelines/multi_project_pipelines.md).
|
||||
- [Lock files](project/file_lock.md) to prevent conflicts.
|
||||
- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](project/deploy_boards.md).
|
||||
- View the current health and status of each CI environment running on Kubernetes with [deploy boards](project/deploy_boards.md).
|
||||
- Leverage continuous delivery method with [Canary Deployments](project/canary_deployments.md).
|
||||
- Scan your code for vulnerabilities and [display them in merge requests](application_security/sast/index.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ percentage of users are affected and the change can either be fixed or quickly
|
|||
reverted.
|
||||
|
||||
Leveraging [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments), visualize your canary
|
||||
deployments right inside the [Deploy Board](deploy_boards.md), without the need to leave GitLab.
|
||||
deployments right inside the [deploy board](deploy_boards.md), without the need to leave GitLab.
|
||||
|
||||
## Use cases
|
||||
|
||||
|
|
@ -47,9 +47,9 @@ this document.
|
|||
|
||||
## Enabling Canary Deployments
|
||||
|
||||
Canary deployments require that you properly configure Deploy Boards:
|
||||
Canary deployments require that you properly configure deploy boards:
|
||||
|
||||
1. Follow the steps to [enable Deploy Boards](deploy_boards.md#enabling-deploy-boards).
|
||||
1. Follow the steps to [enable deploy boards](deploy_boards.md#enabling-deploy-boards).
|
||||
1. To track canary deployments you need to label your Kubernetes deployments and
|
||||
pods with `track: canary`. To get started quickly, you can use the [Auto Deploy](../../topics/autodevops/stages.md#auto-deploy)
|
||||
template for canary deployments that GitLab provides.
|
||||
|
|
@ -61,13 +61,13 @@ This allows GitLab to discover whether a deployment is stable or canary (tempora
|
|||
|
||||
Once all of the above are set up and the pipeline has run at least once,
|
||||
navigate to the environments page under **Pipelines > Environments**.
|
||||
As the pipeline executes, Deploy Boards clearly mark canary pods, enabling
|
||||
As the pipeline executes, deploy boards clearly mark canary pods, enabling
|
||||
quick and easy insight into the status of each environment and deployment.
|
||||
|
||||
Canary deployments are marked with a yellow dot in the Deploy Board so that you
|
||||
Canary deployments are marked with a yellow dot in the deploy board so that you
|
||||
can easily notice them.
|
||||
|
||||

|
||||

|
||||
|
||||
### Advanced traffic control with Canary Ingress
|
||||
|
||||
|
|
@ -104,17 +104,17 @@ Here's an example setup flow from scratch:
|
|||
|
||||
#### How to check the current traffic weight on a Canary Ingress
|
||||
|
||||
1. Visit the [Deploy Board](../../user/project/deploy_boards.md).
|
||||
1. Visit the [deploy board](../../user/project/deploy_boards.md).
|
||||
1. View the current weights on the right.
|
||||
|
||||

|
||||
|
||||
#### How to change the traffic weight on a Canary Ingress
|
||||
|
||||
You can change the traffic weight within your environment's Deploy Board by using [GraphiQL](../../api/graphql/getting_started.md#graphiql),
|
||||
You can change the traffic weight within your environment's deploy board by using [GraphiQL](../../api/graphql/getting_started.md#graphiql),
|
||||
or by sending requests to the [GraphQL API](../../api/graphql/getting_started.md#command-line).
|
||||
|
||||
To use your [Deploy Board](../../user/project/deploy_boards.md):
|
||||
To use your [deploy board](../../user/project/deploy_boards.md):
|
||||
|
||||
1. Navigate to **Deployments > Environments** for your project.
|
||||
1. Set the new weight with the dropdown on the right side.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ features such as:
|
|||
- Use [role-based or attribute-based access controls](cluster_access.md).
|
||||
- Run serverless workloads on [Kubernetes with Knative](serverless/index.md).
|
||||
- Connect GitLab to in-cluster applications using [cluster integrations](../../clusters/integrations.md).
|
||||
- Use [Deploy Boards](../deploy_boards.md) to see the health and status of each CI [environment](../../../ci/environments/index.md) running on your Kubernetes cluster.
|
||||
- Use [deploy boards](../deploy_boards.md) to see the health and status of each CI [environment](../../../ci/environments/index.md) running on your Kubernetes cluster.
|
||||
- Use [Canary deployments](../canary_deployments.md) to update only a portion of your fleet with the latest version of your application.
|
||||
- View your [Kubernetes podlogs](kubernetes_pod_logs.md) directly in GitLab.
|
||||
- Connect to your cluster through GitLab [web terminals](deploy_to_cluster.md#web-terminals-for-kubernetes-clusters).
|
||||
|
|
|
|||
|
|
@ -46,15 +46,15 @@ a [metrics dashboard](../../../operations/metrics/index.md) and select **View lo
|
|||
[permissions](../../permissions.md#project-members-permissions) in the project.
|
||||
1. To navigate to the **Log Explorer** from the sidebar menu, go to **Monitor > Logs**
|
||||
([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22011) in GitLab 12.5.).
|
||||
1. To navigate to the **Log Explorer** from a specific pod on a [Deploy Board](../deploy_boards.md):
|
||||
1. To navigate to the **Log Explorer** from a specific pod on a [deploy board](../deploy_boards.md):
|
||||
|
||||
1. Go to **Deployments > Environments** and find the environment
|
||||
which contains the desired pod, like `production`.
|
||||
1. On the **Environments** page, you should see the status of the environment's
|
||||
pods with [Deploy Boards](../deploy_boards.md).
|
||||
pods with [deploy boards](../deploy_boards.md).
|
||||
1. When mousing over the list of pods, GitLab displays a tooltip with the exact pod name
|
||||
and status.
|
||||

|
||||

|
||||
1. Click on the desired pod to display the **Log Explorer**.
|
||||
|
||||
### Logs view
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
type: howto, reference
|
||||
---
|
||||
|
||||
# Deploy Boards **(FREE)**
|
||||
# Deploy boards **(FREE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1589) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.0.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212320) to GitLab Free in 13.8.
|
||||
|
|
@ -16,7 +16,7 @@ type: howto, reference
|
|||
> This is [fixed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60525) in
|
||||
> GitLab 13.12.
|
||||
|
||||
GitLab Deploy Boards offer a consolidated view of the current health and
|
||||
GitLab deploy boards offer a consolidated view of the current health and
|
||||
status of each CI [environment](../../ci/environments/index.md) running on [Kubernetes](https://kubernetes.io), displaying the status
|
||||
of the pods in the deployment. Developers and other teammates can view the
|
||||
progress and status of a rollout, pod by pod, in the workflow they already use
|
||||
|
|
@ -28,23 +28,23 @@ environments by using [Auto DevOps](../../topics/autodevops/index.md).
|
|||
|
||||
## Overview
|
||||
|
||||
With Deploy Boards you can gain more insight into deploys with benefits such as:
|
||||
With deploy boards you can gain more insight into deploys with benefits such as:
|
||||
|
||||
- Following a deploy from the start, not just when it's done
|
||||
- Watching the rollout of a build across multiple servers
|
||||
- Finer state detail (Succeeded, Running, Failed, Pending, Unknown)
|
||||
- See [Canary Deployments](canary_deployments.md)
|
||||
|
||||
Here's an example of a Deploy Board of the production environment.
|
||||
Here's an example of a deploy board of the production environment.
|
||||
|
||||

|
||||

|
||||
|
||||
The squares represent pods in your Kubernetes cluster that are associated with
|
||||
the given environment. Hovering above each square you can see the state of a
|
||||
deploy rolling out. The percentage is the percent of the pods that are updated
|
||||
to the latest release.
|
||||
|
||||
Since Deploy Boards are tightly coupled with Kubernetes, there is some required
|
||||
Since deploy boards are tightly coupled with Kubernetes, there is some required
|
||||
knowledge. In particular, you should be familiar with:
|
||||
|
||||
- [Kubernetes pods](https://kubernetes.io/docs/concepts/workloads/pods/)
|
||||
|
|
@ -54,7 +54,7 @@ knowledge. In particular, you should be familiar with:
|
|||
|
||||
## Use cases
|
||||
|
||||
Since the Deploy Board is a visual representation of the Kubernetes pods for a
|
||||
Since the deploy board is a visual representation of the Kubernetes pods for a
|
||||
specific environment, there are a lot of use cases. To name a few:
|
||||
|
||||
- You want to promote what's running in staging, to production. You go to the
|
||||
|
|
@ -73,9 +73,9 @@ specific environment, there are a lot of use cases. To name a few:
|
|||
list, find the [Review App](../../ci/review_apps/index.md) you're interested in, and click the
|
||||
manual action to deploy it to staging.
|
||||
|
||||
## Enabling Deploy Boards
|
||||
## Enabling deploy boards
|
||||
|
||||
To display the Deploy Boards for a specific [environment](../../ci/environments/index.md) you should:
|
||||
To display the deploy boards for a specific [environment](../../ci/environments/index.md) you should:
|
||||
|
||||
1. Have [defined an environment](../../ci/environments/index.md) with a deploy stage.
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/
|
|||
|
||||
NOTE:
|
||||
If you're using OpenShift, ensure that you're using the `Deployment` resource
|
||||
instead of `DeploymentConfiguration`. Otherwise, the Deploy Boards don't render
|
||||
instead of `DeploymentConfiguration`. Otherwise, the deploy boards don't render
|
||||
correctly. For more information, read the
|
||||
[OpenShift docs](https://docs.openshift.com/container-platform/3.7/dev_guide/deployments/kubernetes_deployments.html#kubernetes-deployments-vs-deployment-configurations)
|
||||
and [GitLab issue #4584](https://gitlab.com/gitlab-org/gitlab/-/issues/4584).
|
||||
|
|
@ -114,17 +114,17 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/
|
|||
|
||||
If you use GCP to manage clusters, you can see the deployment details in GCP itself by navigating to **Workloads > deployment name > Details**:
|
||||
|
||||

|
||||

|
||||
|
||||
Once all of the above are set up and the pipeline has run at least once,
|
||||
navigate to the environments page under **Deployments > Environments**.
|
||||
|
||||
Deploy Boards are visible by default. You can explicitly click
|
||||
Deploy boards are visible by default. You can explicitly click
|
||||
the triangle next to their respective environment name in order to hide them.
|
||||
|
||||
### Example manifest file
|
||||
|
||||
The following example is an extract of a Kubernetes manifest deployment file, using the two annotations `app.gitlab.com/env` and `app.gitlab.com/app` to enable the **Deploy Boards**:
|
||||
The following example is an extract of a Kubernetes manifest deployment file, using the two annotations `app.gitlab.com/env` and `app.gitlab.com/app` to enable the **deploy boards**:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
|
|
|
|||
|
|
@ -19,14 +19,20 @@ issue itself and the first commit related to that issue.
|
|||
|
||||
If the issue and the code you're committing are both in the same project,
|
||||
add `#xxx` to the commit message, where `xxx` is the issue number.
|
||||
If they are not in the same project, you can add the full URL to the issue
|
||||
(`https://gitlab.com/<username>/<projectname>/issues/<xxx>`).
|
||||
|
||||
```shell
|
||||
git commit -m "this is my commit message. Ref #xxx"
|
||||
```
|
||||
|
||||
or
|
||||
If they are in different projects, but in the same group,
|
||||
add `projectname#xxx` to the commit message.
|
||||
|
||||
```shell
|
||||
git commit -m "this is my commit message. Ref projectname#xxx"
|
||||
```
|
||||
|
||||
If they are not in the same group, you can add the full URL to the issue
|
||||
(`https://gitlab.com/<username>/<projectname>/issues/<xxx>`).
|
||||
|
||||
```shell
|
||||
git commit -m "this is my commit message. Related to https://gitlab.com/<username>/<projectname>/issues/<xxx>"
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ module Gitlab
|
|||
module Pipeline
|
||||
module Chain
|
||||
class Build < Chain::Base
|
||||
include Gitlab::Allowable
|
||||
include Chain::Helpers
|
||||
|
||||
def perform!
|
||||
@pipeline.assign_attributes(
|
||||
source: @command.source,
|
||||
|
|
@ -23,35 +20,12 @@ module Gitlab
|
|||
pipeline_schedule: @command.schedule,
|
||||
merge_request: @command.merge_request,
|
||||
external_pull_request: @command.external_pull_request,
|
||||
locked: @command.project.default_pipeline_lock,
|
||||
variables_attributes: variables_attributes
|
||||
)
|
||||
locked: @command.project.default_pipeline_lock)
|
||||
end
|
||||
|
||||
def break?
|
||||
@pipeline.errors.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def variables_attributes
|
||||
variables = Array(@command.variables_attributes)
|
||||
|
||||
# We allow parent pipelines to pass variables to child pipelines since
|
||||
# these variables are coming from internal configurations. We will check
|
||||
# permissions to :set_pipeline_variables when those are injected upstream,
|
||||
# to the parent pipeline.
|
||||
# In other scenarios (e.g. multi-project pipelines or run pipeline via UI)
|
||||
# the variables are provided from the outside and those should be guarded.
|
||||
return variables if @command.creates_child_pipeline?
|
||||
|
||||
if variables.present? && !can?(@command.current_user, :set_pipeline_variables, @command.project)
|
||||
error("Insufficient permissions to set pipeline variables")
|
||||
variables = []
|
||||
end
|
||||
|
||||
variables
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,25 @@ module Gitlab
|
|||
module Chain
|
||||
class Build
|
||||
class Associations < Chain::Base
|
||||
include Gitlab::Allowable
|
||||
include Chain::Helpers
|
||||
|
||||
def perform!
|
||||
assign_pipeline_variables
|
||||
assign_source_pipeline
|
||||
end
|
||||
|
||||
def break?
|
||||
@pipeline.errors.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_pipeline_variables
|
||||
@pipeline.variables_attributes = variables_attributes
|
||||
end
|
||||
|
||||
def assign_source_pipeline
|
||||
return unless @command.bridge
|
||||
|
||||
@pipeline.build_source_pipeline(
|
||||
|
|
@ -17,8 +35,45 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
def break?
|
||||
false
|
||||
def variables_attributes
|
||||
variables = Array(@command.variables_attributes)
|
||||
variables = apply_permissions(variables)
|
||||
validate_uniqueness(variables)
|
||||
end
|
||||
|
||||
def apply_permissions(variables)
|
||||
# We allow parent pipelines to pass variables to child pipelines since
|
||||
# these variables are coming from internal configurations. We will check
|
||||
# permissions to :set_pipeline_variables when those are injected upstream,
|
||||
# to the parent pipeline.
|
||||
# In other scenarios (e.g. multi-project pipelines or run pipeline via UI)
|
||||
# the variables are provided from the outside and those should be guarded.
|
||||
return variables if @command.creates_child_pipeline?
|
||||
|
||||
if variables.present? && !can?(@command.current_user, :set_pipeline_variables, @command.project)
|
||||
error("Insufficient permissions to set pipeline variables")
|
||||
variables = []
|
||||
end
|
||||
|
||||
variables
|
||||
end
|
||||
|
||||
def validate_uniqueness(variables)
|
||||
duplicated_keys = variables
|
||||
.map { |var| var[:key] }
|
||||
.tally
|
||||
.filter_map { |key, count| key if count > 1 }
|
||||
|
||||
if duplicated_keys.empty?
|
||||
variables
|
||||
else
|
||||
error(duplicate_variables_message(duplicated_keys), config_error: true)
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def duplicate_variables_message(keys)
|
||||
"Duplicate variable #{'name'.pluralize(keys.size)}: #{keys.join(', ')}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ module Gitlab
|
|||
|
||||
def initialize(namespace: nil, project: nil, user: nil, **extra)
|
||||
@namespace = namespace
|
||||
@plan = @namespace&.actual_plan_name
|
||||
@plan = namespace&.actual_plan_name
|
||||
@project = project
|
||||
@extra = extra
|
||||
end
|
||||
|
||||
|
|
@ -34,14 +35,29 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
attr_accessor :namespace, :project, :extra, :plan
|
||||
|
||||
def to_h
|
||||
{
|
||||
environment: environment,
|
||||
source: source,
|
||||
plan: @plan,
|
||||
extra: @extra
|
||||
plan: plan,
|
||||
extra: extra
|
||||
}.merge(project_and_namespace)
|
||||
end
|
||||
|
||||
def project_and_namespace
|
||||
return {} unless ::Feature.enabled?(:add_namespace_and_project_to_snowplow_tracking, default_enabled: :yaml)
|
||||
|
||||
{
|
||||
namespace_id: namespace&.id,
|
||||
project_id: project_id
|
||||
}
|
||||
end
|
||||
|
||||
def project_id
|
||||
project.is_a?(Integer) ? project : project&.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,11 +16,6 @@ module Sidebars
|
|||
add_menu(Sidebars::Groups::Menus::SettingsMenu.new(context))
|
||||
end
|
||||
|
||||
override :render_raw_menus_partial
|
||||
def render_raw_menus_partial
|
||||
'layouts/nav/sidebar/group_menus'
|
||||
end
|
||||
|
||||
override :aria_label
|
||||
def aria_label
|
||||
context.group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
|
||||
|
|
|
|||
|
|
@ -15905,6 +15905,12 @@ msgstr ""
|
|||
msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupRoadmap|This quarter"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupRoadmap|This year"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupRoadmap|To make your epics appear in the roadmap, add start or due dates to them."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15917,6 +15923,9 @@ msgstr ""
|
|||
msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupRoadmap|Within 3 years"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSAML|%{strongOpen}Warning%{strongClose} - Enabling %{linkStart}SSO enforcement%{linkEnd} can reduce security risks."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -75,16 +75,6 @@ RSpec.describe Import::ManifestController, :clean_gitlab_redis_shared_state do
|
|||
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo1[:id])
|
||||
expect(json_response.dig("provider_repos", 1, "id")).to eq(repo2[:id])
|
||||
end
|
||||
|
||||
it "does not show already added project" do
|
||||
project = create(:project, import_type: 'manifest', namespace: user.namespace, import_status: :finished, import_url: repo1[:url])
|
||||
|
||||
get :status, format: :json
|
||||
|
||||
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
|
||||
expect(json_response.dig("provider_repos").length).to eq(1)
|
||||
expect(json_response.dig("provider_repos", 0, "id")).not_to eq(repo1[:id])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the data is stored via Gitlab::ManifestImport::Metadata' do
|
||||
|
|
|
|||
|
|
@ -76,9 +76,7 @@ describe('content_editor/extensions/attachment', () => {
|
|||
const base64EncodedFile = 'data:image/png;base64,Zm9v';
|
||||
|
||||
beforeEach(() => {
|
||||
renderMarkdown.mockResolvedValue(
|
||||
loadMarkdownApiResult('project_wiki_attachment_image').body,
|
||||
);
|
||||
renderMarkdown.mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image'));
|
||||
});
|
||||
|
||||
describe('when uploading succeeds', () => {
|
||||
|
|
@ -153,7 +151,7 @@ describe('content_editor/extensions/attachment', () => {
|
|||
});
|
||||
|
||||
describe('when the file has a zip (or any other attachment) mime type', () => {
|
||||
const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link').body;
|
||||
const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link');
|
||||
|
||||
beforeEach(() => {
|
||||
renderMarkdown.mockResolvedValue(markdownApiResult);
|
||||
|
|
|
|||
|
|
@ -11,10 +11,8 @@ describe('content_editor/extensions/code_block_highlight', () => {
|
|||
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
|
||||
|
||||
beforeEach(() => {
|
||||
const { html } = loadMarkdownApiResult('code_block');
|
||||
|
||||
tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
|
||||
codeBlockHtmlFixture = html;
|
||||
codeBlockHtmlFixture = loadMarkdownApiResult('code_block');
|
||||
parsedCodeBlockHtmlFixture = parseHTML(codeBlockHtmlFixture);
|
||||
|
||||
tiptapEditor.commands.setContent(codeBlockHtmlFixture);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { getJSONFixture } from 'helpers/fixtures';
|
|||
export const loadMarkdownApiResult = (testName) => {
|
||||
const fixturePathPrefix = `api/markdown/${testName}.json`;
|
||||
|
||||
return getJSONFixture(fixturePathPrefix);
|
||||
const fixture = getJSONFixture(fixturePathPrefix);
|
||||
return fixture.body || fixture.html;
|
||||
};
|
||||
|
||||
export const loadMarkdownApiExamples = () => {
|
||||
|
|
@ -16,3 +17,9 @@ export const loadMarkdownApiExamples = () => {
|
|||
|
||||
return apiMarkdownExampleObjects.map(({ name, context, markdown }) => [name, context, markdown]);
|
||||
};
|
||||
|
||||
export const loadMarkdownApiExample = (testName) => {
|
||||
return loadMarkdownApiExamples().find(([name, context]) => {
|
||||
return (context ? `${context}_${name}` : name) === testName;
|
||||
})[2];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ describe('markdown processing', () => {
|
|||
'correctly handles %s (context: %s)',
|
||||
async (name, context, markdown) => {
|
||||
const testName = context ? `${context}_${name}` : name;
|
||||
const { html, body } = loadMarkdownApiResult(testName);
|
||||
const contentEditor = createContentEditor({ renderMarkdown: () => html || body });
|
||||
const contentEditor = createContentEditor({
|
||||
renderMarkdown: () => loadMarkdownApiResult(testName),
|
||||
});
|
||||
await contentEditor.setSerializedContent(markdown);
|
||||
|
||||
expect(contentEditor.getSerializedContent()).toBe(markdown);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import BulletList from '~/content_editor/extensions/bullet_list';
|
||||
import ListItem from '~/content_editor/extensions/list_item';
|
||||
import Paragraph from '~/content_editor/extensions/paragraph';
|
||||
import markdownSerializer from '~/content_editor/services/markdown_serializer';
|
||||
import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap';
|
||||
import { loadMarkdownApiResult, loadMarkdownApiExample } from '../markdown_processing_examples';
|
||||
import { createTestEditor, createDocBuilder } from '../test_utils';
|
||||
|
||||
const SourcemapExtension = Extension.create({
|
||||
// lets add `source` attribute to every element using `getMarkdownSource`
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: [Paragraph.name, BulletList.name, ListItem.name],
|
||||
attributes: {
|
||||
source: {
|
||||
parseHTML: (element) => {
|
||||
const source = getMarkdownSource(element);
|
||||
if (source) return { source };
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const tiptapEditor = createTestEditor({
|
||||
extensions: [BulletList, ListItem, SourcemapExtension],
|
||||
});
|
||||
|
||||
const {
|
||||
builders: { doc, bulletList, listItem, paragraph },
|
||||
} = createDocBuilder({
|
||||
tiptapEditor,
|
||||
names: {
|
||||
bulletList: { nodeType: BulletList.name },
|
||||
listItem: { nodeType: ListItem.name },
|
||||
},
|
||||
});
|
||||
|
||||
describe('content_editor/services/markdown_sourcemap', () => {
|
||||
it('gets markdown source for a rendered HTML element', async () => {
|
||||
const deserialized = await markdownSerializer({
|
||||
render: () => loadMarkdownApiResult('bullet_list_style_3'),
|
||||
serializerConfig: {},
|
||||
}).deserialize({
|
||||
schema: tiptapEditor.schema,
|
||||
content: loadMarkdownApiExample('bullet_list_style_3'),
|
||||
});
|
||||
|
||||
const expected = doc(
|
||||
bulletList(
|
||||
{ bullet: '+', source: '+ list item 1\n+ list item 2' },
|
||||
listItem({ source: '+ list item 1' }, paragraph('list item 1')),
|
||||
listItem(
|
||||
{ source: '+ list item 2' },
|
||||
paragraph('list item 2'),
|
||||
bulletList(
|
||||
{ bullet: '-', source: '- embedded list item 3' },
|
||||
listItem({ source: '- embedded list item 3' }, paragraph('embedded list item 3')),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(deserialized).toEqual(expected.toJSON());
|
||||
});
|
||||
});
|
||||
|
|
@ -338,6 +338,13 @@ describe('Design management index page', () => {
|
|||
__typename: 'DesignVersion',
|
||||
id: expect.anything(),
|
||||
sha: expect.anything(),
|
||||
createdAt: '',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: expect.anything(),
|
||||
name: '',
|
||||
avatarUrl: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -101,7 +101,13 @@ describe('optimistic responses', () => {
|
|||
discussions: { __typename: 'DesignDiscussion', nodes: [] },
|
||||
versions: {
|
||||
__typename: 'DesignVersionConnection',
|
||||
nodes: { __typename: 'DesignVersion', id: -1, sha: -1 },
|
||||
nodes: {
|
||||
__typename: 'DesignVersion',
|
||||
id: expect.anything(),
|
||||
sha: expect.anything(),
|
||||
createdAt: '',
|
||||
author: { __typename: 'UserCore', avatarUrl: '', name: '', id: expect.anything() },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -66,11 +66,21 @@
|
|||
- name: thematic_break
|
||||
markdown: |-
|
||||
---
|
||||
- name: bullet_list
|
||||
- name: bullet_list_style_1
|
||||
markdown: |-
|
||||
* list item 1
|
||||
* list item 2
|
||||
* embedded list item 3
|
||||
- name: bullet_list_style_2
|
||||
markdown: |-
|
||||
- list item 1
|
||||
- list item 2
|
||||
* embedded list item 3
|
||||
- name: bullet_list_style_3
|
||||
markdown: |-
|
||||
+ list item 1
|
||||
+ list item 2
|
||||
- embedded list item 3
|
||||
- name: ordered_list
|
||||
markdown: |-
|
||||
1. list item 1
|
||||
|
|
|
|||
|
|
@ -86,6 +86,11 @@ describe('WebIDE utils', () => {
|
|||
expect(isTextFile({ name: 'abc.dat', content: '' })).toBe(true);
|
||||
expect(isTextFile({ name: 'abc.dat', content: ' ' })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if there is a `binary` property already set on the file object', () => {
|
||||
expect(isTextFile({ name: 'abc.txt', content: '' })).toBe(true);
|
||||
expect(isTextFile({ name: 'abc.txt', content: '', binary: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trimPathComponents', () => {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ describe('import_projects store actions', () => {
|
|||
|
||||
afterEach(() => mock.restore());
|
||||
|
||||
it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
|
||||
it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
|
||||
mock.onGet(MOCK_ENDPOINT).reply(200, payload);
|
||||
|
||||
return testAction(
|
||||
|
|
@ -93,8 +93,8 @@ describe('import_projects store actions', () => {
|
|||
null,
|
||||
localState,
|
||||
[
|
||||
{ type: SET_PAGE, payload: 1 },
|
||||
{ type: REQUEST_REPOS },
|
||||
{ type: SET_PAGE, payload: 1 },
|
||||
{
|
||||
type: RECEIVE_REPOS_SUCCESS,
|
||||
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
|
||||
|
|
@ -104,19 +104,14 @@ describe('import_projects store actions', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('commits SET_PAGE, REQUEST_REPOS, RECEIVE_REPOS_ERROR and SET_PAGE again mutations on an unsuccessful request', () => {
|
||||
it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
|
||||
mock.onGet(MOCK_ENDPOINT).reply(500);
|
||||
|
||||
return testAction(
|
||||
fetchRepos,
|
||||
null,
|
||||
localState,
|
||||
[
|
||||
{ type: SET_PAGE, payload: 1 },
|
||||
{ type: REQUEST_REPOS },
|
||||
{ type: SET_PAGE, payload: 0 },
|
||||
{ type: RECEIVE_REPOS_ERROR },
|
||||
],
|
||||
[{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
|
@ -135,7 +130,7 @@ describe('import_projects store actions', () => {
|
|||
expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localStateWithPage.pageInfo.page + 1}`);
|
||||
});
|
||||
|
||||
it('correctly updates current page on an unsuccessful request', () => {
|
||||
it('correctly keeps current page on an unsuccessful request', () => {
|
||||
mock.onGet(MOCK_ENDPOINT).reply(500);
|
||||
const CURRENT_PAGE = 5;
|
||||
|
||||
|
|
@ -143,10 +138,7 @@ describe('import_projects store actions', () => {
|
|||
fetchRepos,
|
||||
null,
|
||||
{ ...localState, pageInfo: { page: CURRENT_PAGE } },
|
||||
expect.arrayContaining([
|
||||
{ type: SET_PAGE, payload: CURRENT_PAGE + 1 },
|
||||
{ type: SET_PAGE, payload: CURRENT_PAGE },
|
||||
]),
|
||||
expect.arrayContaining([]),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
|
@ -159,12 +151,7 @@ describe('import_projects store actions', () => {
|
|||
fetchRepos,
|
||||
null,
|
||||
{ ...localState, filter: 'filter' },
|
||||
[
|
||||
{ type: SET_PAGE, payload: 1 },
|
||||
{ type: REQUEST_REPOS },
|
||||
{ type: SET_PAGE, payload: 0 },
|
||||
{ type: RECEIVE_REPOS_ERROR },
|
||||
],
|
||||
[{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
|
||||
[],
|
||||
);
|
||||
|
||||
|
|
@ -183,8 +170,8 @@ describe('import_projects store actions', () => {
|
|||
null,
|
||||
{ ...localState, filter: 'filter' },
|
||||
[
|
||||
{ type: SET_PAGE, payload: 1 },
|
||||
{ type: REQUEST_REPOS },
|
||||
{ type: SET_PAGE, payload: 1 },
|
||||
{
|
||||
type: RECEIVE_REPOS_SUCCESS,
|
||||
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { GlBanner } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import TerraformNotification from '~/projects/terraform_notification/components/terraform_notification.vue';
|
||||
import {
|
||||
EVENT_LABEL,
|
||||
DISMISS_EVENT,
|
||||
CLICK_EVENT,
|
||||
} from '~/projects/terraform_notification/constants';
|
||||
|
||||
jest.mock('~/lib/utils/common_utils');
|
||||
|
||||
|
|
@ -10,6 +16,7 @@ const bannerDismissedKey = 'terraform_notification_dismissed';
|
|||
|
||||
describe('TerraformNotificationBanner', () => {
|
||||
let wrapper;
|
||||
let trackingSpy;
|
||||
|
||||
const provideData = {
|
||||
terraformImagePath,
|
||||
|
|
@ -22,11 +29,13 @@ describe('TerraformNotificationBanner', () => {
|
|||
provide: provideData,
|
||||
stubs: { GlBanner },
|
||||
});
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
parseBoolean.mockReturnValue(false);
|
||||
unmockTracking();
|
||||
});
|
||||
|
||||
describe('when the dismiss cookie is not set', () => {
|
||||
|
|
@ -44,8 +53,26 @@ describe('TerraformNotificationBanner', () => {
|
|||
expect(setCookie).toHaveBeenCalledWith(bannerDismissedKey, true);
|
||||
});
|
||||
|
||||
it('should send the dismiss event', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, DISMISS_EVENT, {
|
||||
label: EVENT_LABEL,
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove the banner', () => {
|
||||
expect(findBanner().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when docs link is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
await findBanner().vm.$emit('primary');
|
||||
});
|
||||
|
||||
it('should send button click event', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, CLICK_EVENT, {
|
||||
label: EVENT_LABEL,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -267,61 +267,6 @@ RSpec.describe GroupsHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#group_sidebar_links' do
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
allow(helper).to receive(:current_user) { user }
|
||||
allow(helper).to receive(:can?) { |*args| Ability.allowed?(*args) }
|
||||
helper.instance_variable_set(:@group, group)
|
||||
end
|
||||
|
||||
it 'returns all the expected links' do
|
||||
links = [
|
||||
:overview, :activity, :issues, :labels, :milestones, :merge_requests,
|
||||
:runners, :group_members, :settings
|
||||
]
|
||||
|
||||
expect(helper.group_sidebar_links).to include(*links)
|
||||
end
|
||||
|
||||
it 'excludes runners when the user cannot admin the group' do
|
||||
expect(helper).to receive(:current_user) { user }
|
||||
# TODO Proper policies, such as `read_group_runners, should be implemented per
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
|
||||
expect(helper).to receive(:can?).twice.with(user, :admin_group, group) { false }
|
||||
|
||||
expect(helper.group_sidebar_links).not_to include(:runners)
|
||||
end
|
||||
|
||||
it 'excludes runners when the feature "runner_list_group_view_vue_ui" is disabled' do
|
||||
stub_feature_flags(runner_list_group_view_vue_ui: false)
|
||||
|
||||
expect(helper.group_sidebar_links).not_to include(:runners)
|
||||
end
|
||||
|
||||
it 'excludes settings when the user can admin the group' do
|
||||
expect(helper).to receive(:current_user) { user }
|
||||
expect(helper).to receive(:can?).twice.with(user, :admin_group, group) { false }
|
||||
|
||||
expect(helper.group_sidebar_links).not_to include(:settings)
|
||||
end
|
||||
|
||||
it 'excludes cross project features when the user cannot read cross project' do
|
||||
cross_project_features = [:activity, :issues, :labels, :milestones,
|
||||
:merge_requests]
|
||||
|
||||
allow(Ability).to receive(:allowed?).and_call_original
|
||||
cross_project_features.each do |feature|
|
||||
expect(Ability).to receive(:allowed?).with(user, "read_group_#{feature}".to_sym, group) { false }
|
||||
end
|
||||
|
||||
expect(helper.group_sidebar_links).not_to include(*cross_project_features)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parent_group_options' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, name: 'group') }
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ RSpec.describe Nav::NewDropdownHelper do
|
|||
it 'has project menu item' do
|
||||
expect(subject[:menu_sections]).to eq(
|
||||
expected_menu_section(
|
||||
title: 'GitLab',
|
||||
title: _('GitLab'),
|
||||
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
|
||||
id: 'general_new_project',
|
||||
title: 'New project/repository',
|
||||
|
|
@ -117,7 +117,7 @@ RSpec.describe Nav::NewDropdownHelper do
|
|||
it 'has group menu item' do
|
||||
expect(subject[:menu_sections]).to eq(
|
||||
expected_menu_section(
|
||||
title: 'GitLab',
|
||||
title: _('GitLab'),
|
||||
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
|
||||
id: 'general_new_group',
|
||||
title: 'New group',
|
||||
|
|
@ -135,7 +135,7 @@ RSpec.describe Nav::NewDropdownHelper do
|
|||
it 'has new snippet menu item' do
|
||||
expect(subject[:menu_sections]).to eq(
|
||||
expected_menu_section(
|
||||
title: 'GitLab',
|
||||
title: _('GitLab'),
|
||||
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
|
||||
id: 'general_new_snippet',
|
||||
title: 'New snippet',
|
||||
|
|
|
|||
|
|
@ -3,10 +3,16 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:user) { create(:user, developer_projects: [project]) }
|
||||
let_it_be_with_reload(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user, developer_projects: [project]) }
|
||||
|
||||
let(:pipeline) { Ci::Pipeline.new }
|
||||
let(:step) { described_class.new(pipeline, command) }
|
||||
let(:bridge) { nil }
|
||||
|
||||
let(:variables_attributes) do
|
||||
[{ key: 'first', secret_value: 'world' },
|
||||
{ key: 'second', secret_value: 'second_world' }]
|
||||
end
|
||||
|
||||
let(:command) do
|
||||
Gitlab::Ci::Pipeline::Chain::Command.new(
|
||||
|
|
@ -20,7 +26,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
|
|||
merge_request: nil,
|
||||
project: project,
|
||||
current_user: user,
|
||||
bridge: bridge)
|
||||
bridge: bridge,
|
||||
variables_attributes: variables_attributes)
|
||||
end
|
||||
|
||||
let(:step) { described_class.new(pipeline, command) }
|
||||
|
||||
shared_examples 'breaks the chain' do
|
||||
it 'returns true' do
|
||||
step.perform!
|
||||
|
||||
expect(step.break?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'does not break the chain' do
|
||||
it 'returns false' do
|
||||
step.perform!
|
||||
|
||||
expect(step.break?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a bridge is passed in to the pipeline creation' do
|
||||
|
|
@ -37,26 +62,83 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
|
|||
)
|
||||
end
|
||||
|
||||
it 'never breaks the chain' do
|
||||
step.perform!
|
||||
|
||||
expect(step.break?).to eq(false)
|
||||
end
|
||||
it_behaves_like 'does not break the chain'
|
||||
end
|
||||
|
||||
context 'when a bridge is not passed in to the pipeline creation' do
|
||||
let(:bridge) { nil }
|
||||
|
||||
it 'leaves the source pipeline empty' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.source_pipeline).to be_nil
|
||||
end
|
||||
|
||||
it 'never breaks the chain' do
|
||||
it_behaves_like 'does not break the chain'
|
||||
end
|
||||
|
||||
it 'sets pipeline variables' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
|
||||
.to eq variables_attributes.map(&:with_indifferent_access)
|
||||
end
|
||||
|
||||
context 'when project setting restrict_user_defined_variables is enabled' do
|
||||
before do
|
||||
project.update!(restrict_user_defined_variables: true)
|
||||
end
|
||||
|
||||
context 'when user is developer' do
|
||||
it_behaves_like 'breaks the chain'
|
||||
|
||||
it 'returns an error on variables_attributes', :aggregate_failures do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables'])
|
||||
expect(pipeline.variables).to be_empty
|
||||
end
|
||||
|
||||
context 'when variables_attributes is not specified' do
|
||||
let(:variables_attributes) { nil }
|
||||
|
||||
it_behaves_like 'does not break the chain'
|
||||
|
||||
it 'assigns empty variables' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.variables).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not break the chain'
|
||||
|
||||
it 'assigns variables_attributes' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
|
||||
.to eq variables_attributes.map(&:with_indifferent_access)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate pipeline variables' do
|
||||
let(:variables_attributes) do
|
||||
[{ key: 'first', secret_value: 'world' },
|
||||
{ key: 'first', secret_value: 'second_world' }]
|
||||
end
|
||||
|
||||
it_behaves_like 'breaks the chain'
|
||||
|
||||
it 'returns an error for variables_attributes' do
|
||||
step.perform!
|
||||
|
||||
expect(step.break?).to eq(false)
|
||||
expect(pipeline.errors.full_messages).to eq(['Duplicate variable name: first'])
|
||||
expect(pipeline.variables).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
|
|||
|
||||
let(:pipeline) { Ci::Pipeline.new }
|
||||
|
||||
let(:variables_attributes) do
|
||||
[{ key: 'first', secret_value: 'world' },
|
||||
{ key: 'second', secret_value: 'second_world' }]
|
||||
end
|
||||
|
||||
let(:command) do
|
||||
Gitlab::Ci::Pipeline::Chain::Command.new(
|
||||
source: :push,
|
||||
|
|
@ -24,100 +19,26 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Build do
|
|||
schedule: nil,
|
||||
merge_request: nil,
|
||||
project: project,
|
||||
current_user: user,
|
||||
variables_attributes: variables_attributes)
|
||||
current_user: user)
|
||||
end
|
||||
|
||||
let(:step) { described_class.new(pipeline, command) }
|
||||
|
||||
shared_examples 'builds pipeline' do
|
||||
it 'builds a pipeline with the expected attributes' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.sha).not_to be_empty
|
||||
expect(pipeline.sha).to eq project.commit.id
|
||||
expect(pipeline.ref).to eq 'master'
|
||||
expect(pipeline.tag).to be false
|
||||
expect(pipeline.user).to eq user
|
||||
expect(pipeline.project).to eq project
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'breaks the chain' do
|
||||
it 'returns true' do
|
||||
step.perform!
|
||||
|
||||
expect(step.break?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'does not break the chain' do
|
||||
it 'returns false' do
|
||||
step.perform!
|
||||
|
||||
expect(step.break?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not break the chain'
|
||||
it_behaves_like 'builds pipeline'
|
||||
|
||||
it 'sets pipeline variables' do
|
||||
it 'does not break the chain' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
|
||||
.to eq variables_attributes.map(&:with_indifferent_access)
|
||||
expect(step.break?).to be false
|
||||
end
|
||||
|
||||
context 'when project setting restrict_user_defined_variables is enabled' do
|
||||
before do
|
||||
project.update!(restrict_user_defined_variables: true)
|
||||
end
|
||||
it 'builds a pipeline with the expected attributes' do
|
||||
step.perform!
|
||||
|
||||
context 'when user is developer' do
|
||||
it_behaves_like 'breaks the chain'
|
||||
it_behaves_like 'builds pipeline'
|
||||
|
||||
it 'returns an error on variables_attributes', :aggregate_failures do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.errors.full_messages).to eq(['Insufficient permissions to set pipeline variables'])
|
||||
expect(pipeline.variables).to be_empty
|
||||
end
|
||||
|
||||
context 'when variables_attributes is not specified' do
|
||||
let(:variables_attributes) { nil }
|
||||
|
||||
it_behaves_like 'does not break the chain'
|
||||
it_behaves_like 'builds pipeline'
|
||||
|
||||
it 'assigns empty variables' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.variables).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is maintainer' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not break the chain'
|
||||
it_behaves_like 'builds pipeline'
|
||||
|
||||
it 'assigns variables_attributes' do
|
||||
step.perform!
|
||||
|
||||
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
|
||||
.to eq variables_attributes.map(&:with_indifferent_access)
|
||||
end
|
||||
end
|
||||
expect(pipeline.sha).not_to be_empty
|
||||
expect(pipeline.sha).to eq project.commit.id
|
||||
expect(pipeline.ref).to eq 'master'
|
||||
expect(pipeline.tag).to be false
|
||||
expect(pipeline.user).to eq user
|
||||
expect(pipeline.project).to eq project
|
||||
end
|
||||
|
||||
it 'returns a valid pipeline' do
|
||||
|
|
|
|||
|
|
@ -87,8 +87,26 @@ RSpec.describe Gitlab::Tracking::StandardContext do
|
|||
end
|
||||
end
|
||||
|
||||
it 'does not contain any ids' do
|
||||
expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
|
||||
it 'does not contain user id' do
|
||||
expect(snowplow_context.to_json[:data].keys).not_to include(:user_id)
|
||||
end
|
||||
|
||||
it 'contains namespace and project ids' do
|
||||
expect(snowplow_context.to_json[:data].keys).to include(:project_id, :namespace_id)
|
||||
end
|
||||
|
||||
it 'accepts just project id as integer' do
|
||||
expect { described_class.new(project: 1).to_context }.not_to raise_error
|
||||
end
|
||||
|
||||
context 'without add_namespace_and_project_to_snowplow_tracking feature' do
|
||||
before do
|
||||
stub_feature_flags(add_namespace_and_project_to_snowplow_tracking: false)
|
||||
end
|
||||
|
||||
it 'does not contain any ids' do
|
||||
expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ RSpec.describe Gitlab::Tracking do
|
|||
it "delegates to #{klass} destination" do
|
||||
other_context = double(:context)
|
||||
|
||||
project = double(:project)
|
||||
project = build_stubbed(:project)
|
||||
user = double(:user)
|
||||
|
||||
expect(Gitlab::Tracking::StandardContext)
|
||||
|
|
|
|||
|
|
@ -1248,16 +1248,47 @@ RSpec.describe Ci::CreatePipelineService do
|
|||
end
|
||||
|
||||
context 'when pipeline variables are specified' do
|
||||
let(:variables_attributes) do
|
||||
[{ key: 'first', secret_value: 'world' },
|
||||
{ key: 'second', secret_value: 'second_world' }]
|
||||
end
|
||||
|
||||
subject(:pipeline) { execute_service(variables_attributes: variables_attributes).payload }
|
||||
|
||||
it 'creates a pipeline with specified variables' do
|
||||
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
|
||||
.to eq variables_attributes.map(&:with_indifferent_access)
|
||||
context 'with valid pipeline variables' do
|
||||
let(:variables_attributes) do
|
||||
[{ key: 'first', secret_value: 'world' },
|
||||
{ key: 'second', secret_value: 'second_world' }]
|
||||
end
|
||||
|
||||
it 'creates a pipeline with specified variables' do
|
||||
expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
|
||||
.to eq variables_attributes.map(&:with_indifferent_access)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate pipeline variables' do
|
||||
let(:variables_attributes) do
|
||||
[{ key: 'hello', secret_value: 'world' },
|
||||
{ key: 'hello', secret_value: 'second_world' }]
|
||||
end
|
||||
|
||||
it 'fails to create the pipeline' do
|
||||
expect(pipeline).to be_failed
|
||||
expect(pipeline.variables).to be_empty
|
||||
expect(pipeline.errors[:base]).to eq(['Duplicate variable name: hello'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with more than one duplicate pipeline variable' do
|
||||
let(:variables_attributes) do
|
||||
[{ key: 'hello', secret_value: 'world' },
|
||||
{ key: 'hello', secret_value: 'second_world' },
|
||||
{ key: 'single', secret_value: 'variable' },
|
||||
{ key: 'other', secret_value: 'value' },
|
||||
{ key: 'other', secret_value: 'other value' }]
|
||||
end
|
||||
|
||||
it 'fails to create the pipeline' do
|
||||
expect(pipeline).to be_failed
|
||||
expect(pipeline.variables).to be_empty
|
||||
expect(pipeline.errors[:base]).to eq(['Duplicate variable names: hello, other'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,17 @@ RSpec.describe Ci::PipelineTriggerService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when params have duplicate variables' do
|
||||
let(:params) { { token: trigger.token, ref: 'master', variables: variables } }
|
||||
let(:variables) { { 'TRIGGER_PAYLOAD' => 'duplicate value' } }
|
||||
|
||||
it 'creates a failed pipeline without variables' do
|
||||
expect { result }.to change { Ci::Pipeline.count }
|
||||
expect(result).to be_error
|
||||
expect(result.message[:base]).to eq(['Duplicate variable name: TRIGGER_PAYLOAD'])
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'detecting an unprocessable pipeline trigger'
|
||||
end
|
||||
|
||||
|
|
@ -201,6 +212,17 @@ RSpec.describe Ci::PipelineTriggerService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when params have duplicate variables' do
|
||||
let(:params) { { token: job.token, ref: 'master', variables: variables } }
|
||||
let(:variables) { { 'TRIGGER_PAYLOAD' => 'duplicate value' } }
|
||||
|
||||
it 'creates a failed pipeline without variables' do
|
||||
expect { result }.to change { Ci::Pipeline.count }
|
||||
expect(result).to be_error
|
||||
expect(result.message[:base]).to eq(['Duplicate variable name: TRIGGER_PAYLOAD'])
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'detecting an unprocessable pipeline trigger'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -82,16 +82,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
|
|||
expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id)
|
||||
end
|
||||
|
||||
it "does not show already added project" do
|
||||
project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim')
|
||||
stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum)
|
||||
|
||||
get :status, format: :json
|
||||
|
||||
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
|
||||
expect(json_response.dig("provider_repos")).to eq([])
|
||||
end
|
||||
|
||||
it "touches the etag cache store" do
|
||||
stub_client(repos: [], orgs: [], each_page: [])
|
||||
|
||||
|
|
|
|||
|
|
@ -19,14 +19,4 @@ RSpec.shared_examples 'import controller status' do
|
|||
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
|
||||
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
|
||||
end
|
||||
|
||||
it "does not show already added project" do
|
||||
project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
|
||||
stub_client(client_repos_field => [repo])
|
||||
|
||||
get :status, format: :json
|
||||
|
||||
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
|
||||
expect(json_response.dig("provider_repos")).to eq([])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue