Commit 61f0c589 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent d23b2a08
...@@ -117,7 +117,6 @@ schedule:review-build-cng: ...@@ -117,7 +117,6 @@ schedule:review-build-cng:
- source scripts/utils.sh - source scripts/utils.sh
- install_api_client_dependencies_with_apk - install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh - source scripts/review_apps/review-apps.sh
- export REVIEW_APP_CONFIG_CHANGED=$(base_config_changed)
script: script:
- check_kube_domain - check_kube_domain
- ensure_namespace - ensure_namespace
......
// capture anything starting with http:// or https:// // capture anything starting with http:// or https:// which is not already part of a html link
// up until a disallowed character or whitespace // up until a disallowed character or whitespace
export const blobLinkRegex = /https?:\/\/[^"<>\\^`{|}\s]+/g; export const blobLinkRegex = /(?<!<a href=")https?:\/\/[^"<>\\^`{|}\s]+/g;
export default { blobLinkRegex }; export default { blobLinkRegex };
...@@ -37,7 +37,7 @@ const createAction = config => ` ...@@ -37,7 +37,7 @@ const createAction = config => `
`; `;
const createFlashEl = (message, type) => ` const createFlashEl = (message, type) => `
<div class="flash-content flash-${type} rounded"> <div class="flash-${type}">
<div class="flash-text"> <div class="flash-text">
${_.escape(message)} ${_.escape(message)}
<div class="close-icon-wrapper js-close-icon"> <div class="close-icon-wrapper js-close-icon">
......
...@@ -541,7 +541,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => { ...@@ -541,7 +541,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
* The result cannot become negative. * The result cannot become negative.
* *
* @param endDate date string that the time difference is calculated for * @param endDate date string that the time difference is calculated for
* @return {number} number of milliseconds remaining until the given date * @return {Number} number of milliseconds remaining until the given date
*/ */
export const calculateRemainingMilliseconds = endDate => { export const calculateRemainingMilliseconds = endDate => {
const remainingMilliseconds = new Date(endDate).getTime() - Date.now(); const remainingMilliseconds = new Date(endDate).getTime() - Date.now();
...@@ -552,7 +552,7 @@ export const calculateRemainingMilliseconds = endDate => { ...@@ -552,7 +552,7 @@ export const calculateRemainingMilliseconds = endDate => {
* Subtracts a given number of days from a given date and returns the new date. * Subtracts a given number of days from a given date and returns the new date.
* *
* @param {Date} date the date that we will substract days from * @param {Date} date the date that we will substract days from
* @param {number} daysInPast number of days that are subtracted from a given date * @param {Number} daysInPast number of days that are subtracted from a given date
* @returns {Date} Date in past as Date object * @returns {Date} Date in past as Date object
*/ */
export const getDateInPast = (date, daysInPast) => export const getDateInPast = (date, daysInPast) =>
...@@ -594,3 +594,11 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => { ...@@ -594,3 +594,11 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => {
return range.map(formatter); return range.map(formatter);
}; };
/**
* Converts the supplied number of seconds to milliseconds.
*
* @param {Number} seconds
* @return {Number} number of milliseconds
*/
export const secondsToMilliseconds = seconds => seconds * 1000;
import dateformat from 'dateformat'; import dateformat from 'dateformat';
import { secondsIn, dateTimePickerRegex, dateFormats } from './constants'; import { secondsIn, dateTimePickerRegex, dateFormats } from './constants';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
const secondsToMilliseconds = seconds => seconds * 1000;
export const getTimeDiff = timeWindow => { export const getTimeDiff = timeWindow => {
const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds
......
<script> <script>
import { GlButton, GlLink, GlProgressBar } from '@gitlab/ui'; import { GlButton, GlLink, GlProgressBar } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility'; import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
...@@ -31,7 +31,7 @@ export default { ...@@ -31,7 +31,7 @@ export default {
return Math.round((this.report.success_count / this.report.total_count) * 100) || 0; return Math.round((this.report.success_count / this.report.total_count) * 100) || 0;
}, },
formattedDuration() { formattedDuration() {
return formatTime(this.report.total_time * 1000); return formatTime(secondsToMilliseconds(this.report.total_time));
}, },
progressBarVariant() { progressBarVariant() {
if (this.successPercentage < 33) { if (this.successPercentage < 33) {
......
import { TestStatus } from '~/pipelines/constants'; import { TestStatus } from '~/pipelines/constants';
import { formatTime } from '~/lib/utils/datetime_utility'; import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
function iconForTestStatus(status) { function iconForTestStatus(status) {
switch (status) { switch (status) {
...@@ -12,7 +12,7 @@ function iconForTestStatus(status) { ...@@ -12,7 +12,7 @@ function iconForTestStatus(status) {
} }
} }
export const formattedTime = timeInSeconds => formatTime(timeInSeconds * 1000); export const formattedTime = timeInSeconds => formatTime(secondsToMilliseconds(timeInSeconds));
export const addIconStatus = testCase => ({ export const addIconStatus = testCase => ({
...testCase, ...testCase,
......
...@@ -10,6 +10,7 @@ import { slugify } from '~/lib/utils/text_utility'; ...@@ -10,6 +10,7 @@ import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility'; import { getLocationHash } from '~/lib/utils/url_utility';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReleaseBlockFooter from './release_block_footer.vue';
export default { export default {
name: 'ReleaseBlock', name: 'ReleaseBlock',
...@@ -19,6 +20,7 @@ export default { ...@@ -19,6 +20,7 @@ export default {
GlButton, GlButton,
Icon, Icon,
UserAvatarLink, UserAvatarLink,
ReleaseBlockFooter,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -79,6 +81,9 @@ export default { ...@@ -79,6 +81,9 @@ export default {
this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit_url, this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit_url,
); );
}, },
shouldShowFooter() {
return this.glFeatures.releaseIssueSummary;
},
}, },
mounted() { mounted() {
const hash = getLocationHash(); const hash = getLocationHash();
...@@ -164,7 +169,7 @@ export default { ...@@ -164,7 +169,7 @@ export default {
by by
<user-avatar-link <user-avatar-link
class="prepend-left-4" class="prepend-left-4"
:link-href="author.path" :link-href="author.web_url"
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="userImageAltDescription" :img-alt="userImageAltDescription"
:tooltip-text="author.username" :tooltip-text="author.username"
...@@ -216,5 +221,16 @@ export default { ...@@ -216,5 +221,16 @@ export default {
<div v-html="release.description_html"></div> <div v-html="release.description_html"></div>
</div> </div>
</div> </div>
<release-block-footer
v-if="shouldShowFooter"
class="card-footer"
:commit="release.commit"
:commit-path="release.commit_path"
:tag-name="release.tag_name"
:tag-path="release.tag_path"
:author="release.author"
:released-at="release.released_at"
/>
</div> </div>
</template> </template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __, sprintf } from '~/locale';
export default {
name: 'ReleaseBlockFooter',
components: {
Icon,
GlLink,
UserAvatarLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
commit: {
type: Object,
required: false,
default: null,
},
commitPath: {
type: String,
required: false,
default: '',
},
tagName: {
type: String,
required: false,
default: '',
},
tagPath: {
type: String,
required: false,
default: '',
},
author: {
type: Object,
required: false,
default: null,
},
releasedAt: {
type: String,
required: false,
default: '',
},
},
computed: {
releasedAtTimeAgo() {
return this.timeFormated(this.releasedAt);
},
userImageAltDescription() {
return this.author && this.author.username
? sprintf(__("%{username}'s avatar"), { username: this.author.username })
: null;
},
},
};
</script>
<template>
<div>
<div v-if="commit" class="float-left mr-3 d-flex align-items-center js-commit-info">
<icon ref="commitIcon" name="commit" class="mr-1" />
<div v-gl-tooltip.bottom :title="commit.title">
<gl-link v-if="commitPath" :href="commitPath">
{{ commit.short_id }}
</gl-link>
<span v-else>{{ commit.short_id }}</span>
</div>
</div>
<div v-if="tagName" class="float-left mr-3 d-flex align-items-center js-tag-info">
<icon name="tag" class="mr-1" />
<div v-gl-tooltip.bottom :title="__('Tag')">
<gl-link v-if="tagPath" :href="tagPath">
{{ tagName }}
</gl-link>
<span v-else>{{ tagName }}</span>
</div>
</div>
<div
v-if="releasedAt || author"
class="float-left d-flex align-items-center js-author-date-info"
>
<span class="text-secondary">{{ __('Created') }}&nbsp;</span>
<template v-if="releasedAt">
<span
v-gl-tooltip.bottom
:title="tooltipTitle(releasedAt)"
class="text-secondary flex-shrink-0"
>
{{ releasedAtTimeAgo }}&nbsp;
</span>
</template>
<div v-if="author" class="d-flex">
<span class="text-secondary">{{ __('by') }}&nbsp;</span>
<user-avatar-link
:link-href="author.web_url"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
tooltip-placement="bottom"
/>
</div>
</div>
</div>
</template>
...@@ -282,8 +282,7 @@ pre code { ...@@ -282,8 +282,7 @@ pre code {
white-space: pre-wrap; white-space: pre-wrap;
} }
.alert, .alert {
.flash-notice {
border-radius: 0; border-radius: 0;
} }
...@@ -310,12 +309,10 @@ pre code { ...@@ -310,12 +309,10 @@ pre code {
.alert-success, .alert-success,
.alert-info, .alert-info,
.alert-warning, .alert-warning,
.alert-danger, .alert-danger {
.flash-notice {
color: $white-light; color: $white-light;
h4, h4,
a:not(.btn),
.alert-link { .alert-link {
color: $white-light; color: $white-light;
} }
......
$notification-box-shadow-color: rgba(0, 0, 0, 0.25); $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
.flash-container { .flash-container {
margin: 0; margin-top: 10px;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
font-size: 14px; font-size: 14px;
position: relative; position: relative;
...@@ -12,17 +12,22 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); ...@@ -12,17 +12,22 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
position: -webkit-sticky; position: -webkit-sticky;
top: $flash-container-top; top: $flash-container-top;
z-index: 251; z-index: 251;
}
.flash-content { &.flash-container-page {
box-shadow: 0 2px 4px 0 $notification-box-shadow-color; margin-bottom: 0;
} }
&:empty {
margin: 0;
} }
.close-icon-wrapper { .close-icon-wrapper {
padding: ($gl-btn-padding + $gl-padding-4) $gl-padding $gl-btn-padding; padding: ($gl-padding + $gl-padding-4) $gl-padding $gl-padding;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0;
cursor: pointer; cursor: pointer;
.close-icon { .close-icon {
...@@ -31,13 +36,11 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); ...@@ -31,13 +36,11 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
} }
} }
.flash-notice,
.flash-alert, .flash-alert,
.flash-notice,
.flash-success, .flash-success,
.flash-warning { .flash-warning {
border-radius: $border-radius-default; padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
color: $white-light;
padding-right: $gl-padding * 2;
.container-fluid, .container-fluid,
.container-fluid.container-limited { .container-fluid.container-limited {
...@@ -45,75 +48,31 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); ...@@ -45,75 +48,31 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
} }
} }
.flash-notice { .flash-alert {
@extend .alert; background-color: $red-100;
background-color: $blue-500; color: $red-700;
margin: 0; }
&.flash-notice-persistent { .flash-notice {
background-color: $blue-100; background-color: $blue-100;
color: $gl-text-color; color: $blue-700;
a {
color: $blue-600;
&:hover {
color: $blue-800;
text-decoration: none;
}
}
} }
.flash-success {
background-color: $theme-green-100;
color: $green-700;
} }
.flash-warning { .flash-warning {
@extend .alert;
background-color: $orange-100; background-color: $orange-100;
color: $orange-900; color: $orange-800;
cursor: default; cursor: default;
margin: 0;
} }
.flash-text, .flash-text,
.flash-action { .flash-action {
display: inline-block; display: inline-block;
} }
.flash-alert {
@extend .alert;
background-color: $red-500;
margin: 0;
.flash-action {
margin-left: 5px;
text-decoration: none;
font-weight: $gl-font-weight-normal;
border-bottom: 1px solid;
&:hover {
border-color: transparent;
}
}
}
.flash-success {
@extend .alert;
background-color: $green-500;
margin: 0;
}
&.flash-container-page {
margin-bottom: 0;
.flash-notice,
.flash-alert,
.flash-success {
border-radius: 0;
}
}
&:empty {
margin: 0;
}
} }
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
......
...@@ -33,7 +33,8 @@ body { ...@@ -33,7 +33,8 @@ body {
&.limit-container-width { &.limit-container-width {
.flash-container.sticky { .flash-container.sticky {
max-width: $limited-layout-width; max-width: $limited-layout-width;
margin: 0 auto; margin-right: auto;
margin-left: auto;
} }
} }
} }
......
...@@ -92,13 +92,12 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -92,13 +92,12 @@ class Clusters::ClustersController < Clusters::BaseController
end end
def destroy def destroy
if cluster.destroy response = Clusters::DestroyService
flash[:notice] = _('Kubernetes cluster integration was successfully removed.') .new(current_user, destroy_params)
.execute(cluster)
flash[:notice] = response[:message]
redirect_to clusterable.index_path, status: :found redirect_to clusterable.index_path, status: :found
else
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
end
end end
def create_gcp def create_gcp
...@@ -143,6 +142,14 @@ class Clusters::ClustersController < Clusters::BaseController ...@@ -143,6 +142,14 @@ class Clusters::ClustersController < Clusters::BaseController
private private
def destroy_params
# To be uncomented on https://gitlab.com/gitlab-org/gitlab/merge_requests/16954
# This MR got split into other since it was too big.
#
# params.permit(:cleanup)
{}
end
def update_params def update_params
if cluster.provided_by_user? if cluster.provided_by_user?
params.require(:cluster).permit( params.require(:cluster).permit(
......
...@@ -7,6 +7,7 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -7,6 +7,7 @@ class Projects::ReleasesController < Projects::ApplicationController
before_action :authorize_read_release! before_action :authorize_read_release!
before_action do before_action do
push_frontend_feature_flag(:release_edit_page, project) push_frontend_feature_flag(:release_edit_page, project)
push_frontend_feature_flag(:release_issue_summary, project)
end end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
......
...@@ -95,6 +95,14 @@ module UsersHelper ...@@ -95,6 +95,14 @@ module UsersHelper
tabs tabs
end end
def trials_link_url
'https://about.gitlab.com/free-trial/'
end
def trials_allowed?(user)
false
end
def get_current_user_menu_items def get_current_user_menu_items
items = [] items = []
...@@ -105,6 +113,7 @@ module UsersHelper ...@@ -105,6 +113,7 @@ module UsersHelper
items << :help items << :help
items << :profile if can?(current_user, :read_user, current_user) items << :profile if can?(current_user, :read_user, current_user)
items << :settings if can?(current_user, :update_user, current_user) items << :settings if can?(current_user, :update_user, current_user)
items << :start_trial if trials_allowed?(current_user)
items items
end end
......
...@@ -607,8 +607,14 @@ module Ci ...@@ -607,8 +607,14 @@ module Ci
rescue Gitlab::Ci::YamlProcessor::ValidationError => e rescue Gitlab::Ci::YamlProcessor::ValidationError => e
self.yaml_errors = e.message self.yaml_errors = e.message
nil nil
rescue rescue => ex
self.yaml_errors = 'Undefined error' self.yaml_errors = "Undefined error (#{Labkit::Correlation::CorrelationId.current_id})"
Gitlab::Sentry.track_acceptable_exception(ex, extra: {
project_id: project.id,
sha: sha,
ci_yaml_file: ci_yaml_file_path
})
nil nil
end end
end end
......
# frozen_string_literal: true
module Clusters
class DestroyService
attr_reader :current_user, :params
def initialize(user = nil, params = {})
@current_user, @params = user, params.dup
@response = {}
end
def execute(cluster)
cleanup? ? start_cleanup!(cluster) : destroy_cluster!(cluster)
@response
end
private
def cleanup?
Gitlab::Utils.to_boolean(params[:cleanup])
end
def start_cleanup!(cluster)
cluster.start_cleanup!
@response[:message] = _('Kubernetes cluster integration and resources are being removed.')
end
def destroy_cluster!(cluster)
cluster.destroy!
@response[:message] = _('Kubernetes cluster integration was successfully removed.')
end
end
end
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
.gcp-signup-offer--copy .gcp-signup-offer--copy
%h4= s_('ClusterIntegration|Did you know?') %h4= s_('ClusterIntegration|Did you know?')
%p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-default{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' } %a.btn.btn-default{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' }
= s_("ClusterIntegration|Apply for credit") = s_("ClusterIntegration|Apply for credit")
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- flash.each do |key, value| - flash.each do |key, value|
-# Don't show a flash message if the message is nil -# Don't show a flash message if the message is nil
- if value - if value
%div{ class: "flash-content flash-#{key} rounded" } %div{ class: "flash-#{key}" }
%span= value %span= value
%div{ class: "close-icon-wrapper js-close-icon" } %div{ class: "close-icon-wrapper js-close-icon" }
= sprite_icon('close', size: 16, css_class: 'close-icon') = sprite_icon('close', size: 16, css_class: 'close-icon')
...@@ -18,6 +18,11 @@ ...@@ -18,6 +18,11 @@
- if current_user_menu?(:profile) - if current_user_menu?(:profile)
%li %li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
- if current_user_menu?(:start_trial)
%li
%a.profile-link{ href: trials_link_url }
= s_("CurrentUser|Start a trial")
= emoji_icon('rocket')
- if current_user_menu?(:settings) - if current_user_menu?(:settings)
%li %li
= link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' } = link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' }
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= render partial: 'shared/label', collection: @prioritized_labels, as: :label, locals: { force_priority: true, subject: @project } = render partial: 'shared/label', collection: @prioritized_labels, as: :label, locals: { force_priority: true, subject: @project }
- elsif search.present? - elsif search.present?
.nothing-here-block .nothing-here-block
= _('No prioritised labels with such name or description') = _('No prioritized labels with such name or description')
- if @labels.present? - if @labels.present?
.other-labels .other-labels
......
---
title: "[Geo] Fix: undefined Gitlab::BackgroundMigration::PruneOrphanedGeoEvents"
merge_request: 19638
author:
type: fixed
---
title: Update GCP credit URLs
merge_request: 19683
author:
type: fixed
---
title: Refactor maximum user counts in license
merge_request: 19071
author: briankabiro
type: changed
---
title: Fix productivity analytics listing with multiple labels
merge_request: 33182
author:
type: fixed
---
title: Add start a trial option in the top-right user dropdown
merge_request: 19632
author:
type: added
---
title: Update flash messages color sitewide
merge_request: 18369
author:
type: changed
---
title: Move release meta-data into footer on Releases page
merge_request: 19451
author:
type: changed
...@@ -17,6 +17,7 @@ GET /license ...@@ -17,6 +17,7 @@ GET /license
"starts_at": "2018-01-27", "starts_at": "2018-01-27",
"expires_at": "2022-01-27", "expires_at": "2022-01-27",
"historical_max": 300, "historical_max": 300,
"maximum_user_count": 300,
"expired": false, "expired": false,
"overage": 200, "overage": 200,
"user_limit": 100, "user_limit": 100,
...@@ -46,6 +47,7 @@ GET /licenses ...@@ -46,6 +47,7 @@ GET /licenses
"starts_at": "2018-01-27", "starts_at": "2018-01-27",
"expires_at": "2022-01-27", "expires_at": "2022-01-27",
"historical_max": 300, "historical_max": 300,
"maximum_user_count": 300,
"expired": false, "expired": false,
"overage": 200, "overage": 200,
"user_limit": 100, "user_limit": 100,
...@@ -64,6 +66,7 @@ GET /licenses ...@@ -64,6 +66,7 @@ GET /licenses
"starts_at": "2018-01-27", "starts_at": "2018-01-27",
"expires_at": "2022-01-27", "expires_at": "2022-01-27",
"historical_max": 300, "historical_max": 300,
"maximum_user_count": 300,
"expired": false, "expired": false,
"overage": 200, "overage": 200,
"user_limit": 100, "user_limit": 100,
...@@ -112,6 +115,7 @@ Example response: ...@@ -112,6 +115,7 @@ Example response:
"starts_at": "2018-01-27", "starts_at": "2018-01-27",
"expires_at": "2022-01-27", "expires_at": "2022-01-27",
"historical_max": 300, "historical_max": 300,
"maximum_user_count": 300,
"expired": false, "expired": false,
"overage": 200, "overage": 200,
"user_limit": 100, "user_limit": 100,
...@@ -155,6 +159,7 @@ Example response: ...@@ -155,6 +159,7 @@ Example response:
"starts_at": "2018-01-27", "starts_at": "2018-01-27",
"expires_at": "2022-01-27", "expires_at": "2022-01-27",
"historical_max": 300, "historical_max": 300,
"maximum_user_count": 300,
"expired": false, "expired": false,
"overage": 200, "overage": 200,
"user_limit": 100, "user_limit": 100,
......
...@@ -50,7 +50,6 @@ module Gitlab ...@@ -50,7 +50,6 @@ module Gitlab
validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) } validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) }
validates :dependencies, array_of_strings: true validates :dependencies, array_of_strings: true
validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true validates :rules, array_of_hashes: true
end end
...@@ -114,6 +113,11 @@ module Gitlab ...@@ -114,6 +113,11 @@ module Gitlab
description: 'List of evaluable Rules to determine job inclusion.', description: 'List of evaluable Rules to determine job inclusion.',
inherit: false inherit: false
entry :needs, Entry::Needs,
description: 'Needs configuration for this job.',
metadata: { allowed_needs: %i[job] },
inherit: false
entry :variables, Entry::Variables, entry :variables, Entry::Variables,
description: 'Environment variables available for this job.', description: 'Environment variables available for this job.',
inherit: false inherit: false
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
class Need < ::Gitlab::Config::Entry::Simplifiable
strategy :Job, if: -> (config) { config.is_a?(String) }
class Job < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, presence: true
validates :config, type: String
end
def type
:job
end
def value
{ name: @config }
end
end
class UnknownStrategy < ::Gitlab::Config::Entry::Node
def type
end
def value
end
def errors
["#{location} has an unsupported type"]
end
end
end
end
end
end
end
::Gitlab::Ci::Config::Entry::Need.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Need')
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a set of needs dependencies.
#
class Needs < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, presence: true
validate do
unless config.is_a?(Hash) || config.is_a?(Array)
errors.add(:config, 'can only be a Hash or an Array')
end
end
validate on: :composed do
extra_keys = value.keys - opt(:allowed_needs)
if extra_keys.any?
errors.add(:config, "uses invalid types: #{extra_keys.join(', ')}")
end
end
end
def compose!(deps = nil)
super(deps) do
[@config].flatten.each_with_index do |need, index|
@entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need)
.value(need)
.with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
def value
values = @entries.values.select(&:type)
values.group_by(&:type).transform_values do |values|
values.map(&:value)
end
end
end
end
end
end
end
...@@ -18,8 +18,8 @@ module Gitlab ...@@ -18,8 +18,8 @@ module Gitlab
config[:dependencies] = expand_names(config[:dependencies]) config[:dependencies] = expand_names(config[:dependencies])
end end
if config[:needs] if job_needs = config.dig(:needs, :job)
config[:needs] = expand_names(config[:needs]) config[:needs][:job] = expand_needs(job_needs)
end end
config config
...@@ -36,6 +36,22 @@ module Gitlab ...@@ -36,6 +36,22 @@ module Gitlab
end end
end end
def expand_needs(job_needs)
return unless job_needs
job_needs.flat_map do |job_need|
job_need_name = job_need[:name].to_sym
if all_job_names = parallelized_jobs[job_need_name]
all_job_names.map do |job_name|
{ name: job_name }
end
else
job_need
end
end
end
def parallelized_jobs def parallelized_jobs
strong_memoize(:parallelized_jobs) do strong_memoize(:parallelized_jobs) do
@jobs_config.each_with_object({}) do |(job_name, config), hash| @jobs_config.each_with_object({}) do |(job_name, config), hash|
......
...@@ -113,9 +113,10 @@ promoteBeta: ...@@ -113,9 +113,10 @@ promoteBeta:
promoteProduction: promoteProduction:
extends: .promote_job extends: .promote_job
stage: production stage: production
# We only allow production promotion on `master` because # We only allow production promotion on the default branch because
# it has its own production scoped secret variables # it has its own production scoped secret variables
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
script: script:
- bundle exec fastlane promote_beta_to_production - bundle exec fastlane promote_beta_to_production
...@@ -10,7 +10,8 @@ docker-build-master: ...@@ -10,7 +10,8 @@ docker-build-master:
- docker build --pull -t "$CI_REGISTRY_IMAGE" . - docker build --pull -t "$CI_REGISTRY_IMAGE" .
- docker push "$CI_REGISTRY_IMAGE" - docker push "$CI_REGISTRY_IMAGE"
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
docker-build: docker-build:
# Official docker image. # Official docker image.
...@@ -24,4 +25,5 @@ docker-build: ...@@ -24,4 +25,5 @@ docker-build:
- docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" . - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -24,9 +24,8 @@ review: ...@@ -24,9 +24,8 @@ review:
- tags - tags
kubernetes: active kubernetes: active
except: except:
refs:
- master
variables: variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- $REVIEW_DISABLED - $REVIEW_DISABLED
stop_review: stop_review:
...@@ -48,9 +47,8 @@ stop_review: ...@@ -48,9 +47,8 @@ stop_review:
- tags - tags
kubernetes: active kubernetes: active
except: except:
refs:
- master
variables: variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- $REVIEW_DISABLED - $REVIEW_DISABLED
# Staging deploys are disabled by default since # Staging deploys are disabled by default since
...@@ -73,10 +71,9 @@ staging: ...@@ -73,10 +71,9 @@ staging:
name: staging name: staging
url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
only: only:
refs:
- master
kubernetes: active kubernetes: active
variables: variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- $STAGING_ENABLED - $STAGING_ENABLED
# Canaries are disabled by default, but if you want them, # Canaries are disabled by default, but if you want them,
...@@ -98,10 +95,9 @@ canary: ...@@ -98,10 +95,9 @@ canary:
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
when: manual when: manual
only: only:
refs:
- master
kubernetes: active kubernetes: active
variables: variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- $CANARY_ENABLED - $CANARY_ENABLED
.production: &production_template .production: &production_template
...@@ -126,9 +122,9 @@ canary: ...@@ -126,9 +122,9 @@ canary:
production: production:
<<: *production_template <<: *production_template
only: only:
refs:
- master
kubernetes: active kubernetes: active
variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
except: except:
variables: variables:
- $STAGING_ENABLED - $STAGING_ENABLED
...@@ -141,10 +137,9 @@ production_manual: ...@@ -141,10 +137,9 @@ production_manual:
when: manual when: manual
allow_failure: false allow_failure: false
only: only:
refs:
- master
kubernetes: active kubernetes: active
variables: variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- $STAGING_ENABLED - $STAGING_ENABLED
- $CANARY_ENABLED - $CANARY_ENABLED
except: except:
...@@ -152,7 +147,7 @@ production_manual: ...@@ -152,7 +147,7 @@ production_manual:
- $INCREMENTAL_ROLLOUT_ENABLED - $INCREMENTAL_ROLLOUT_ENABLED
- $INCREMENTAL_ROLLOUT_MODE - $INCREMENTAL_ROLLOUT_MODE
# This job implements incremental rollout on for every push to `master`. # This job implements incremental rollout for every push to the default branch.
.rollout: &rollout_template .rollout: &rollout_template
extends: .auto-deploy extends: .auto-deploy
...@@ -178,10 +173,9 @@ production_manual: ...@@ -178,10 +173,9 @@ production_manual:
when: manual when: manual
# This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4) # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4)
only: only:
refs:
- master
kubernetes: active kubernetes: active
variables: variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- $INCREMENTAL_ROLLOUT_MODE == "manual" - $INCREMENTAL_ROLLOUT_MODE == "manual"
- $INCREMENTAL_ROLLOUT_ENABLED - $INCREMENTAL_ROLLOUT_ENABLED
except: except:
...@@ -193,10 +187,9 @@ production_manual: ...@@ -193,10 +187,9 @@ production_manual:
when: delayed when: delayed
start_in: 5 minutes start_in: 5 minutes
only: only:
refs:
- master
kubernetes: active kubernetes: active
variables: variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
- $INCREMENTAL_ROLLOUT_MODE == "timed" - $INCREMENTAL_ROLLOUT_MODE == "timed"
timed rollout 10%: timed rollout 10%:
......
...@@ -64,7 +64,8 @@ pages: ...@@ -64,7 +64,8 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
# WARNING: This template is using the `julia` images from [Docker # WARNING: This template is using the `julia` images from [Docker
# Hub][3]. One can use custom Julia images and/or the official ones found # Hub][3]. One can use custom Julia images and/or the official ones found
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
# This template will build and test your projects # This template will build and test your projects
# * Caches downloaded dependencies and plugins between invocation. # * Caches downloaded dependencies and plugins between invocation.
# * Verify but don't deploy merge requests. # * Verify but don't deploy merge requests.
# * Deploy built artifacts from master branch only. # * Deploy built artifacts from the default branch only.
variables: variables:
# This will suppress any download for dependencies and plugins or upload messages which would clutter the console log. # This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
...@@ -33,7 +33,8 @@ cache: ...@@ -33,7 +33,8 @@ cache:
script: script:
- 'mvn $MAVEN_CLI_OPTS verify' - 'mvn $MAVEN_CLI_OPTS verify'
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
# Verify merge requests using JDK8 # Verify merge requests using JDK8
verify:jdk8: verify:jdk8:
...@@ -42,7 +43,7 @@ verify:jdk8: ...@@ -42,7 +43,7 @@ verify:jdk8:
# To deploy packages from CI, create a ci_settings.xml file # To deploy packages from CI, create a ci_settings.xml file
# For deploying packages to GitLab's Maven Repository: See https://docs.gitlab.com/ee/user/project/packages/maven_repository.html#creating-maven-packages-with-gitlab-cicd for more details. # For deploying packages to GitLab's Maven Repository: See https://docs.gitlab.com/ee/user/project/packages/maven_repository.html#creating-maven-packages-with-gitlab-cicd for more details.
# Please note: The GitLab Maven Repository is currently only available in GitLab Premium / Ultimate. # Please note: The GitLab Maven Repository is currently only available in GitLab Premium / Ultimate.
# For `master` branch run `mvn deploy` automatically. # For the default branch run `mvn deploy` automatically.
deploy:jdk8: deploy:jdk8:
stage: deploy stage: deploy
script: script:
...@@ -51,4 +52,5 @@ deploy:jdk8: ...@@ -51,4 +52,5 @@ deploy:jdk8:
fi fi
- 'mvn $MAVEN_CLI_OPTS deploy -s ci_settings.xml' - 'mvn $MAVEN_CLI_OPTS deploy -s ci_settings.xml'
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -25,7 +25,8 @@ before_script: ...@@ -25,7 +25,8 @@ before_script:
release: release:
stage: deploy stage: deploy
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
artifacts: artifacts:
paths: paths:
- build/release/MyProject.exe - build/release/MyProject.exe
......
...@@ -49,7 +49,8 @@ review: ...@@ -49,7 +49,8 @@ review:
only: only:
- branches - branches
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
stop-review: stop-review:
<<: *deploy <<: *deploy
...@@ -66,7 +67,8 @@ stop-review: ...@@ -66,7 +67,8 @@ stop-review:
only: only:
- branches - branches
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
staging: staging:
<<: *deploy <<: *deploy
...@@ -78,7 +80,8 @@ staging: ...@@ -78,7 +80,8 @@ staging:
name: staging name: staging
url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
production: production:
<<: *deploy <<: *deploy
...@@ -91,4 +94,5 @@ production: ...@@ -91,4 +94,5 @@ production:
name: production name: production
url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -25,4 +25,5 @@ build: ...@@ -25,4 +25,5 @@ build:
- find . -maxdepth 1 -name '*.json' -print0 | xargs -t0n1 packer build - find . -maxdepth 1 -name '*.json' -print0 | xargs -t0n1 packer build
when: manual when: manual
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -12,4 +12,5 @@ pages: ...@@ -12,4 +12,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -10,4 +10,5 @@ pages: ...@@ -10,4 +10,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -14,4 +14,5 @@ pages: ...@@ -14,4 +14,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -9,4 +9,5 @@ pages: ...@@ -9,4 +9,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -12,4 +12,5 @@ pages: ...@@ -12,4 +12,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -14,4 +14,5 @@ pages: ...@@ -14,4 +14,5 @@ pages:
- node_modules - node_modules
key: project key: project
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -8,10 +8,12 @@ pages: ...@@ -8,10 +8,12 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
test: test:
script: script:
- hugo - hugo
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -11,7 +11,8 @@ test: ...@@ -11,7 +11,8 @@ test:
- pip install hyde - pip install hyde
- hyde gen - hyde gen
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
pages: pages:
stage: deploy stage: deploy
...@@ -22,4 +23,5 @@ pages: ...@@ -22,4 +23,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -17,7 +17,8 @@ test: ...@@ -17,7 +17,8 @@ test:
paths: paths:
- test - test
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
pages: pages:
stage: deploy stage: deploy
...@@ -27,4 +28,5 @@ pages: ...@@ -27,4 +28,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -34,4 +34,5 @@ pages: ...@@ -34,4 +34,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -9,4 +9,5 @@ pages: ...@@ -9,4 +9,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -13,4 +13,5 @@ pages: ...@@ -13,4 +13,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -12,7 +12,8 @@ test: ...@@ -12,7 +12,8 @@ test:
- bundle install --path vendor - bundle install --path vendor
- bundle exec middleman build - bundle exec middleman build
except: except:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
pages: pages:
script: script:
...@@ -24,4 +25,5 @@ pages: ...@@ -24,4 +25,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -9,4 +9,5 @@ pages: ...@@ -9,4 +9,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -12,4 +12,5 @@ pages: ...@@ -12,4 +12,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -26,4 +26,5 @@ pages: ...@@ -26,4 +26,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -48,4 +48,5 @@ pages: ...@@ -48,4 +48,5 @@ pages:
paths: paths:
- public - public
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -22,7 +22,8 @@ archive_project: ...@@ -22,7 +22,8 @@ archive_project:
- xcodebuild clean archive -archivePath build/ProjectName -scheme SchemeName - xcodebuild clean archive -archivePath build/ProjectName -scheme SchemeName
- xcodebuild -exportArchive -exportFormat ipa -archivePath "build/ProjectName.xcarchive" -exportPath "build/ProjectName.ipa" -exportProvisioningProfile "ProvisioningProfileName" - xcodebuild -exportArchive -exportFormat ipa -archivePath "build/ProjectName.xcarchive" -exportPath "build/ProjectName.ipa" -exportProvisioningProfile "ProvisioningProfileName"
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
artifacts: artifacts:
paths: paths:
- build/ProjectName.ipa - build/ProjectName.ipa
......
...@@ -53,4 +53,5 @@ apply: ...@@ -53,4 +53,5 @@ apply:
- plan - plan
when: manual when: manual
only: only:
- master variables:
- $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
environment: job[:environment_name], environment: job[:environment_name],
coverage_regex: job[:coverage], coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name), yaml_variables: yaml_variables(name),
needs_attributes: job[:needs]&.map { |need| { name: need } }, needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible], interruptible: job[:interruptible],
rules: job[:rules], rules: job[:rules],
options: { options: {
...@@ -59,7 +59,7 @@ module Gitlab ...@@ -59,7 +59,7 @@ module Gitlab
instance: job[:instance], instance: job[:instance],
start_in: job[:start_in], start_in: job[:start_in],
trigger: job[:trigger], trigger: job[:trigger],
bridge_needs: job[:needs] bridge_needs: job.dig(:needs, :bridge)&.first
}.compact }.compact }.compact }.compact
end end
...@@ -159,17 +159,19 @@ module Gitlab ...@@ -159,17 +159,19 @@ module Gitlab
end end
def validate_job_needs!(name, job) def validate_job_needs!(name, job)
return unless job[:needs] return unless job.dig(:needs, :job)
stage_index = @stages.index(job[:stage]) stage_index = @stages.index(job[:stage])
job[:needs].each do |need| job.dig(:needs, :job).each do |need|
raise ValidationError, "#{name} job: undefined need: #{need}" unless @jobs[need.to_sym] need_job_name = need[:name]
needs_stage_index = @stages.index(@jobs[need.to_sym][:stage]) raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym]
needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage])
unless needs_stage_index.present? && needs_stage_index < stage_index unless needs_stage_index.present? && needs_stage_index < stage_index
raise ValidationError, "#{name} job: need #{need} is not defined in prior stages" raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages"
end end
end end
end end
......
...@@ -29,6 +29,7 @@ module Gitlab ...@@ -29,6 +29,7 @@ module Gitlab
def compose!(deps = nil) def compose!(deps = nil)
return unless valid? return unless valid?
super do
self.class.nodes.each do |key, factory| self.class.nodes.each do |key, factory|
# If we override the config type validation # If we override the config type validation
# we can end with different config types like String # we can end with different config types like String
...@@ -47,6 +48,7 @@ module Gitlab ...@@ -47,6 +48,7 @@ module Gitlab
entry.compose!(deps) entry.compose!(deps)
end end
end end
end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def skip_config_hash_validation? def skip_config_hash_validation?
...@@ -67,12 +69,13 @@ module Gitlab ...@@ -67,12 +69,13 @@ module Gitlab
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil) def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil, metadata: {})
factory = ::Gitlab::Config::Entry::Factory.new(entry) factory = ::Gitlab::Config::Entry::Factory.new(entry)
.with(description: description) .with(description: description)
.with(default: default) .with(default: default)
.with(inherit: inherit) .with(inherit: inherit)
.with(reserved: reserved) .with(reserved: reserved)
.metadata(metadata)
(@nodes ||= {}).merge!(key.to_sym => factory) (@nodes ||= {}).merge!(key.to_sym => factory)
end end
......
...@@ -112,6 +112,10 @@ module Gitlab ...@@ -112,6 +112,10 @@ module Gitlab
@aspects ||= [] @aspects ||= []
end end
def self.with_aspect(blk)
self.aspects.append(blk)
end
private private
attr_reader :entries attr_reader :entries
......
...@@ -4,11 +4,11 @@ module Gitlab ...@@ -4,11 +4,11 @@ module Gitlab
module Config module Config
module Entry module Entry
class Simplifiable < SimpleDelegator class Simplifiable < SimpleDelegator
EntryStrategy = Struct.new(:name, :condition) EntryStrategy = Struct.new(:name, :klass, :condition)
attr_reader :subject attr_reader :subject
def initialize(config, **metadata) def initialize(config, **metadata, &blk)
unless self.class.const_defined?(:UnknownStrategy) unless self.class.const_defined?(:UnknownStrategy)
raise ArgumentError, 'UndefinedStrategy not available!' raise ArgumentError, 'UndefinedStrategy not available!'
end end
...@@ -19,14 +19,13 @@ module Gitlab ...@@ -19,14 +19,13 @@ module Gitlab
entry = self.class.entry_class(strategy) entry = self.class.entry_class(strategy)
@subject = entry.new(config, metadata) @subject = entry.new(config, metadata, &blk)
yield(@subject) if block_given?
super(@subject) super(@subject)
end end
def self.strategy(name, **opts) def self.strategy(name, **opts)
EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy| EntryStrategy.new(name, opts.dig(:class), opts.fetch(:if)).tap do |strategy|
strategies.append(strategy) strategies.append(strategy)
end end
end end
...@@ -37,7 +36,7 @@ module Gitlab ...@@ -37,7 +36,7 @@ module Gitlab
def self.entry_class(strategy) def self.entry_class(strategy)
if strategy.present? if strategy.present?
self.const_get(strategy.name, false) strategy.klass || self.const_get(strategy.name, false)
else else
self::UnknownStrategy self::UnknownStrategy
end end
......
...@@ -7,14 +7,27 @@ module Gitlab ...@@ -7,14 +7,27 @@ module Gitlab
extend ActiveSupport::Concern extend ActiveSupport::Concern
def self.included(node) def self.included(node)
node.aspects.append -> do node.with_aspect -> do
@validator = self.class.validator.new(self) validate(:new)
@validator.validate(:new)
end end
end end
def validator
@validator ||= self.class.validator.new(self)
end
def validate(context = nil)
validator.validate(context)
end
def compose!(deps = nil, &blk)
super(deps, &blk)
validate(:composed)
end
def errors def errors
@validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables validator.messages + descendants.flat_map(&:errors)
end end
class_methods do class_methods do
......
...@@ -4958,6 +4958,9 @@ msgstr "" ...@@ -4958,6 +4958,9 @@ msgstr ""
msgid "CurrentUser|Settings" msgid "CurrentUser|Settings"
msgstr "" msgstr ""
msgid "CurrentUser|Start a trial"
msgstr ""
msgid "Custom CI configuration path" msgid "Custom CI configuration path"
msgstr "" msgstr ""
...@@ -9676,7 +9679,7 @@ msgstr "" ...@@ -9676,7 +9679,7 @@ msgstr ""
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}" msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
msgstr "" msgstr ""
msgid "Kubernetes cluster integration was not removed." msgid "Kubernetes cluster integration and resources are being removed."
msgstr "" msgstr ""
msgid "Kubernetes cluster integration was successfully removed." msgid "Kubernetes cluster integration was successfully removed."
...@@ -11285,7 +11288,7 @@ msgstr "" ...@@ -11285,7 +11288,7 @@ msgstr ""
msgid "No preview for this file type" msgid "No preview for this file type"
msgstr "" msgstr ""
msgid "No prioritised labels with such name or description" msgid "No prioritized labels with such name or description"
msgstr "" msgstr ""
msgid "No public groups" msgid "No public groups"
......
...@@ -207,19 +207,16 @@ function download_chart() { ...@@ -207,19 +207,16 @@ function download_chart() {
} }
function base_config_changed() { function base_config_changed() {
git fetch origin master --depth=50 if [ -z "${CI_MERGE_REQUEST_IID}" ]; then return; fi
[ -n "$(git diff origin/master... --name-only -- scripts/review_apps/base-config.yaml)" ] curl "${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/changes" | jq '.changes | any(.old_path == "scripts/review_apps/base-config.yaml")'
} }
function deploy() { function deploy() {
local name="$CI_ENVIRONMENT_SLUG" local name="$CI_ENVIRONMENT_SLUG"
local edition="${GITLAB_EDITION-ce}" local edition="${GITLAB_EDITION-ce}"
local base_config_file_ref="master" local base_config_file_ref="master"
echo "REVIEW_APP_CONFIG_CHANGED: ${REVIEW_APP_CONFIG_CHANGED}" if [[ "$(base_config_changed)" == "true" ]]; then base_config_file_ref="$CI_COMMIT_SHA"; fi
if [ -n "${REVIEW_APP_CONFIG_CHANGED}" ]; then
base_config_file_ref="$CI_COMMIT_SHA"
fi
local base_config_file="https://gitlab.com/gitlab-org/gitlab/raw/${base_config_file_ref}/scripts/review_apps/base-config.yaml" local base_config_file="https://gitlab.com/gitlab-org/gitlab/raw/${base_config_file_ref}/scripts/review_apps/base-config.yaml"
echoinfo "Deploying ${name}..." true echoinfo "Deploying ${name}..." true
......
...@@ -68,7 +68,7 @@ describe 'Search for labels', :js do ...@@ -68,7 +68,7 @@ describe 'Search for labels', :js do
find('#label-search').native.send_keys(:enter) find('#label-search').native.send_keys(:enter)
page.within('.prioritized-labels') do page.within('.prioritized-labels') do
expect(page).to have_content('No prioritised labels with such name or description') expect(page).to have_content('No prioritized labels with such name or description')
end end
page.within('.other-labels') do page.within('.other-labels') do
......
...@@ -474,3 +474,11 @@ describe('getDatesInRange', () => { ...@@ -474,3 +474,11 @@ describe('getDatesInRange', () => {
}); });
}); });
}); });
describe('secondsToMilliseconds', () => {
it('converts seconds to milliseconds correctly', () => {
expect(datetimeUtility.secondsToMilliseconds(0)).toBe(0);
expect(datetimeUtility.secondsToMilliseconds(60)).toBe(60000);
expect(datetimeUtility.secondsToMilliseconds(123)).toBe(123000);
});
});
import { mount } from '@vue/test-utils';
import ReleaseBlockFooter from '~/releases/list/components/release_block_footer.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLink } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { release } from '../../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
jest.mock('~/vue_shared/mixins/timeago', () => ({
methods: {
timeFormated() {
return '7 fortnightes ago';
},
tooltipTitle() {
return 'February 30, 2401';
},
},
}));
describe('Release block footer', () => {
let wrapper;
let releaseClone;
const factory = (props = {}) => {
wrapper = mount(ReleaseBlockFooter, {
propsData: {
...convertObjectPropsToCamelCase(releaseClone),
...props,
},
sync: false,
});
return wrapper.vm.$nextTick();
};
beforeEach(() => {
releaseClone = JSON.parse(JSON.stringify(release));
});
afterEach(() => {
wrapper.destroy();
});
const commitInfoSection = () => wrapper.find('.js-commit-info');
const commitInfoSectionLink = () => commitInfoSection().find(GlLink);
const tagInfoSection = () => wrapper.find('.js-tag-info');
const tagInfoSectionLink = () => tagInfoSection().find(GlLink);
const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
describe('with all props provided', () => {
beforeEach(() => factory());
it('renders the commit icon', () => {
const commitIcon = commitInfoSection().find(Icon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('commit');
});
it('renders the commit SHA with a link', () => {
const commitLink = commitInfoSectionLink();
expect(commitLink.exists()).toBe(true);
expect(commitLink.text()).toBe(releaseClone.commit.short_id);
expect(commitLink.attributes('href')).toBe(releaseClone.commit_path);
});
it('renders the tag icon', () => {
const commitIcon = tagInfoSection().find(Icon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('tag');
});
it('renders the tag name with a link', () => {
const commitLink = tagInfoSection().find(GlLink);
expect(commitLink.exists()).toBe(true);
expect(commitLink.text()).toBe(releaseClone.tag_name);
expect(commitLink.attributes('href')).toBe(releaseClone.tag_path);
});
it('renders the author and creation time info', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
`Created 7 fortnightes ago by ${releaseClone.author.username}`,
);
});
it("renders the author's avatar image", () => {
const avatarImg = authorDateInfoSection().find('img');
expect(avatarImg.exists()).toBe(true);
expect(avatarImg.attributes('src')).toBe(releaseClone.author.avatar_url);
});
it("renders a link to the author's profile", () => {
const authorLink = authorDateInfoSection().find(GlLink);
expect(authorLink.exists()).toBe(true);
expect(authorLink.attributes('href')).toBe(releaseClone.author.web_url);
});
});
describe('without any commit info', () => {
beforeEach(() => factory({ commit: undefined }));
it('does not render any commit info', () => {
expect(commitInfoSection().exists()).toBe(false);
});
});
describe('without a commit URL', () => {
beforeEach(() => factory({ commitPath: undefined }));
it('renders the commit SHA as plain text (instead of a link)', () => {
expect(commitInfoSectionLink().exists()).toBe(false);
expect(commitInfoSection().text()).toBe(releaseClone.commit.short_id);
});
});
describe('without a tag name', () => {
beforeEach(() => factory({ tagName: undefined }));
it('does not render any tag info', () => {
expect(tagInfoSection().exists()).toBe(false);
});
});
describe('without a tag URL', () => {
beforeEach(() => factory({ tagPath: undefined }));
it('renders the tag name as plain text (instead of a link)', () => {
expect(tagInfoSectionLink().exists()).toBe(false);
expect(tagInfoSection().text()).toBe(releaseClone.tag_name);
});
});
describe('without any author info', () => {
beforeEach(() => factory({ author: undefined }));
it('renders the release date without the author name', () => {
expect(trimText(authorDateInfoSection().text())).toBe('Created 7 fortnightes ago');
});
});
describe('without a released at date', () => {
beforeEach(() => factory({ releasedAt: undefined }));
it('renders the author name without the release date', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
`Created by ${releaseClone.author.username}`,
);
});
});
describe('without a release date or author info', () => {
beforeEach(() => factory({ author: undefined, releasedAt: undefined }));
it('does not render any author or release date info', () => {
expect(authorDateInfoSection().exists()).toBe(false);
});
});
});
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import ReleaseBlock from '~/releases/list/components/release_block.vue'; import ReleaseBlock from '~/releases/list/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/list/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore'; import { first } from 'underscore';
import { release } from '../../mock_data'; import { release } from '../../mock_data';
...@@ -21,14 +22,16 @@ describe('Release block', () => { ...@@ -21,14 +22,16 @@ describe('Release block', () => {
let wrapper; let wrapper;
let releaseClone; let releaseClone;
const factory = (releaseProp, releaseEditPageFeatureFlag = true) => { const factory = (releaseProp, featureFlags = {}) => {
wrapper = mount(ReleaseBlock, { wrapper = mount(ReleaseBlock, {
propsData: { propsData: {
release: releaseProp, release: releaseProp,
}, },
provide: { provide: {
glFeatures: { glFeatures: {
releaseEditPage: releaseEditPageFeatureFlag, releaseEditPage: true,
releaseIssueSummary: true,
...featureFlags,
}, },
}, },
sync: false, sync: false,
...@@ -142,6 +145,10 @@ describe('Release block', () => { ...@@ -142,6 +145,10 @@ describe('Release block', () => {
expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description); expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
}); });
it('renders the footer', () => {
expect(wrapper.find(ReleaseBlockFooter).exists()).toBe(true);
});
}); });
it('renders commit sha', () => { it('renders commit sha', () => {
...@@ -173,7 +180,7 @@ describe('Release block', () => { ...@@ -173,7 +180,7 @@ describe('Release block', () => {
}); });
it('does not render an edit button if the releaseEditPage feature flag is disabled', () => it('does not render an edit button if the releaseEditPage feature flag is disabled', () =>
factory(releaseClone, false).then(() => { factory(releaseClone, { releaseEditPage: false }).then(() => {
expect(editButton().exists()).toBe(false); expect(editButton().exists()).toBe(false);
})); }));
......
...@@ -30,6 +30,7 @@ export const milestones = [ ...@@ -30,6 +30,7 @@ export const milestones = [
export const release = { export const release = {
name: 'New release', name: 'New release',
tag_name: 'v0.3', tag_name: 'v0.3',
tag_path: '/root/release-test/-/tags/v0.3',
description: 'A super nice release!', description: 'A super nice release!',
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>', description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z', created_at: '2019-08-26T17:54:04.952Z',
...@@ -56,6 +57,7 @@ export const release = { ...@@ -56,6 +57,7 @@ export const release = {
committer_email: 'admin@example.com', committer_email: 'admin@example.com',
committed_date: '2019-08-26T17:47:07.000Z', committed_date: '2019-08-26T17:47:07.000Z',
}, },
commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false, upcoming_release: false,
milestones, milestones,
assets: { assets: {
......
...@@ -76,6 +76,10 @@ describe UsersHelper do ...@@ -76,6 +76,10 @@ describe UsersHelper do
allow(helper).to receive(:can?).and_return(false) allow(helper).to receive(:can?).and_return(false)
end end
after do
expect(items).not_to include(:start_trial)
end
it 'includes all default items' do it 'includes all default items' do
expect(items).to include(:help, :sign_out) expect(items).to include(:help, :sign_out)
end end
......
...@@ -176,15 +176,13 @@ describe('Blob viewer', () => { ...@@ -176,15 +176,13 @@ describe('Blob viewer', () => {
}); });
}); });
describe('a URL inside the blob content', () => { describe('linkifyURLs', () => {
beforeEach(() => { it('renders a plain url as a link in simple view', done => {
mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, { mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, {
html: html:
'<div class="js-blob-content"><pre class="code"><code><span class="line" lang="yaml"><span class="c1">To install gitlab-shell you also need a Go compiler version 1.8 or newer. https://golang.org/dl/</span></span></code></pre></div>', '<div class="js-blob-content"><pre class="code"><code><span class="line" lang="yaml"><span class="c1">To install gitlab-shell you also need a Go compiler version 1.8 or newer. https://golang.org/dl/</span></span></code></pre></div>',
}); });
});
it('is rendered as a link in simple view', done => {
asyncClick() asyncClick()
.then(() => { .then(() => {
expect(document.querySelector('.blob-viewer[data-type="simple"]').innerHTML).toContain( expect(document.querySelector('.blob-viewer[data-type="simple"]').innerHTML).toContain(
...@@ -197,5 +195,24 @@ describe('Blob viewer', () => { ...@@ -197,5 +195,24 @@ describe('Blob viewer', () => {
done(); done();
}); });
}); });
it('leaves an unescaped url untouched', done => {
mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, {
html:
'<div class="js-blob-content"><pre class="code"><code><span class="line" lang="yaml"><a href="https://golang.org/dl/">golang</a></span></span></code></pre></div>',
});
asyncClick()
.then(() => {
expect(document.querySelector('.blob-viewer[data-type="simple"]').innerHTML).toContain(
'<a href="https://golang.org/dl/">golang</a>',
);
done();
})
.catch(() => {
fail();
done();
});
});
}); });
}); });
...@@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do let(:result) do
%i[before_script script stage type after_script cache %i[before_script script stage type after_script cache
image services only except rules variables artifacts image services only except rules needs variables artifacts
environment coverage retry] environment coverage retry]
end end
...@@ -384,21 +384,6 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -384,21 +384,6 @@ describe Gitlab::Ci::Config::Entry::Job do
end end
context 'when has needs' do context 'when has needs' do
context 'that are not a array of strings' do
let(:config) do
{
stage: 'test',
script: 'echo',
needs: 'build-job'
}
end
it 'returns error about invalid type' do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job needs should be an array of strings'
end
end
context 'when have dependencies that are not subset of needs' do context 'when have dependencies that are not subset of needs' do
let(:config) do let(:config) do
{ {
......
# frozen_string_literal: true
require 'spec_helper'
describe ::Gitlab::Ci::Config::Entry::Need do
subject(:need) { described_class.new(config) }
context 'when job is specified' do
let(:config) { 'job_name' }
describe '#valid?' do
it { is_expected.to be_valid }
end
describe '#value' do
it 'returns job needs configuration' do
expect(need.value).to eq(name: 'job_name')
end
end
end
context 'when need is empty' do
let(:config) { '' }
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'is returns an error about an empty config' do
expect(need.errors)
.to contain_exactly("job config can't be blank")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ::Gitlab::Ci::Config::Entry::Needs do
subject(:needs) { described_class.new(config) }
before do
needs.metadata[:allowed_needs] = %i[job]
end
describe 'validations' do
before do
needs.compose!
end
context 'when entry config value is correct' do
let(:config) { ['job_name'] }
describe '#valid?' do
it { is_expected.to be_valid }
end
end
context 'when config value has wrong type' do
let(:config) { 123 }
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns error about incorrect type' do
expect(needs.errors)
.to include('needs config can only be a hash or an array')
end
end
end
context 'when wrong needs type is used' do
let(:config) { [123] }
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns error about incorrect type' do
expect(needs.errors).to contain_exactly(
'need has an unsupported type')
end
end
end
end
describe '.compose!' do
context 'when valid job entries composed' do
let(:config) { %w[first_job_name second_job_name] }
before do
needs.compose!
end
describe '#value' do
it 'returns key value' do
expect(needs.value).to eq(
job: [
{ name: 'first_job_name' },
{ name: 'second_job_name' }
]
)
end
end
describe '#descendants' do
it 'creates valid descendant nodes' do
expect(needs.descendants.count).to eq 2
expect(needs.descendants)
.to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need))
end
end
end
end
end
...@@ -7,6 +7,16 @@ describe Gitlab::Ci::Config::Normalizer do ...@@ -7,6 +7,16 @@ describe Gitlab::Ci::Config::Normalizer do
let(:job_config) { { script: 'rspec', parallel: 5, name: 'rspec' } } let(:job_config) { { script: 'rspec', parallel: 5, name: 'rspec' } }
let(:config) { { job_name => job_config } } let(:config) { { job_name => job_config } }
let(:expanded_job_names) do
[
"rspec 1/5",
"rspec 2/5",
"rspec 3/5",
"rspec 4/5",
"rspec 5/5"
]
end
describe '.normalize_jobs' do describe '.normalize_jobs' do
subject { described_class.new(config).normalize_jobs } subject { described_class.new(config).normalize_jobs }
...@@ -15,9 +25,7 @@ describe Gitlab::Ci::Config::Normalizer do ...@@ -15,9 +25,7 @@ describe Gitlab::Ci::Config::Normalizer do
end end
it 'has parallelized jobs' do it 'has parallelized jobs' do
job_names = [:"rspec 1/5", :"rspec 2/5", :"rspec 3/5", :"rspec 4/5", :"rspec 5/5"] is_expected.to include(*expanded_job_names.map(&:to_sym))
is_expected.to include(*job_names)
end end
it 'sets job instance in options' do it 'sets job instance in options' do
...@@ -43,49 +51,109 @@ describe Gitlab::Ci::Config::Normalizer do ...@@ -43,49 +51,109 @@ describe Gitlab::Ci::Config::Normalizer do
let(:job_name) { :"rspec 35/2" } let(:job_name) { :"rspec 35/2" }
it 'properly parallelizes job names' do it 'properly parallelizes job names' do
job_names = [:"rspec 35/2 1/5", :"rspec 35/2 2/5", :"rspec 35/2 3/5", :"rspec 35/2 4/5", :"rspec 35/2 5/5"] job_names = [
:"rspec 35/2 1/5",
:"rspec 35/2 2/5",
:"rspec 35/2 3/5",
:"rspec 35/2 4/5",
:"rspec 35/2 5/5"
]
is_expected.to include(*job_names) is_expected.to include(*job_names)
end end
end end
%i[dependencies needs].each do |context| context 'for dependencies' do
context "when job has #{context} on parallelized jobs" do context "when job has dependencies on parallelized jobs" do
let(:config) do let(:config) do
{ {
job_name => job_config, job_name => job_config,
other_job: { script: 'echo 1', context => [job_name.to_s] } other_job: { script: 'echo 1', dependencies: [job_name.to_s] }
} }
end end
it "parallelizes #{context}" do it "parallelizes dependencies" do
job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"] expect(subject[:other_job][:dependencies]).to eq(expanded_job_names)
expect(subject[:other_job][context]).to include(*job_names)
end end
it "does not include original job name in #{context}" do it "does not include original job name in #{context}" do
expect(subject[:other_job][context]).not_to include(job_name) expect(subject[:other_job][:dependencies]).not_to include(job_name)
end end
end end
context "when there are #{context} which are both parallelized and not" do context "when there are dependencies which are both parallelized and not" do
let(:config) do let(:config) do
{ {
job_name => job_config, job_name => job_config,
other_job: { script: 'echo 1' }, other_job: { script: 'echo 1' },
final_job: { script: 'echo 1', context => [job_name.to_s, "other_job"] } final_job: { script: 'echo 1', dependencies: [job_name.to_s, "other_job"] }
} }
end end
it "parallelizes #{context}" do it "parallelizes dependencies" do
job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"] job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"]
expect(subject[:final_job][context]).to include(*job_names) expect(subject[:final_job][:dependencies]).to include(*job_names)
end
it "includes the regular job in dependencies" do
expect(subject[:final_job][:dependencies]).to include('other_job')
end
end
end
context 'for needs' do
let(:expanded_job_attributes) do
expanded_job_names.map do |job_name|
{ name: job_name }
end
end
context "when job has needs on parallelized jobs" do
let(:config) do
{
job_name => job_config,
other_job: {
script: 'echo 1',
needs: {
job: [
{ name: job_name.to_s }
]
}
}
}
end
it "parallelizes needs" do
expect(subject.dig(:other_job, :needs, :job)).to eq(expanded_job_attributes)
end
end
context "when there are dependencies which are both parallelized and not" do
let(:config) do
{
job_name => job_config,
other_job: {
script: 'echo 1'
},
final_job: {
script: 'echo 1',
needs: {
job: [
{ name: job_name.to_s },
{ name: "other_job" }
]
}
}
}
end
it "parallelizes dependencies" do
expect(subject.dig(:final_job, :needs, :job)).to include(*expanded_job_attributes)
end end
it "includes the regular job in #{context}" do it "includes the regular job in dependencies" do
expect(subject[:final_job][context]).to include('other_job') expect(subject.dig(:final_job, :needs, :job)).to include(name: 'other_job')
end end
end end
end end
......
...@@ -1293,7 +1293,7 @@ module Gitlab ...@@ -1293,7 +1293,7 @@ module Gitlab
end end
end end
describe "Needs" do describe "Job Needs" do
let(:needs) { } let(:needs) { }
let(:dependencies) { } let(:dependencies) { }
...@@ -1301,6 +1301,7 @@ module Gitlab ...@@ -1301,6 +1301,7 @@ module Gitlab
{ {
build1: { stage: 'build', script: 'test' }, build1: { stage: 'build', script: 'test' },
build2: { stage: 'build', script: 'test' }, build2: { stage: 'build', script: 'test' },
parallel: { stage: 'build', script: 'test', parallel: 2 },
test1: { stage: 'test', script: 'test', needs: needs, dependencies: dependencies }, test1: { stage: 'test', script: 'test', needs: needs, dependencies: dependencies },
test2: { stage: 'test', script: 'test' }, test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' } deploy: { stage: 'test', script: 'test' }
...@@ -1317,7 +1318,7 @@ module Gitlab ...@@ -1317,7 +1318,7 @@ module Gitlab
let(:needs) { %w(build1 build2) } let(:needs) { %w(build1 build2) }
it "does create jobs with valid specification" do it "does create jobs with valid specification" do
expect(subject.builds.size).to eq(5) expect(subject.builds.size).to eq(7)
expect(subject.builds[0]).to eq( expect(subject.builds[0]).to eq(
stage: "build", stage: "build",
stage_idx: 1, stage_idx: 1,
...@@ -1329,16 +1330,11 @@ module Gitlab ...@@ -1329,16 +1330,11 @@ module Gitlab
allow_failure: false, allow_failure: false,
yaml_variables: [] yaml_variables: []
) )
expect(subject.builds[2]).to eq( expect(subject.builds[4]).to eq(
stage: "test", stage: "test",
stage_idx: 2, stage_idx: 2,
name: "test1", name: "test1",
options: { options: { script: ["test"] },
script: ["test"],
# This does not make sense, there is a follow-up:
# https://gitlab.com/gitlab-org/gitlab-foss/issues/65569
bridge_needs: %w[build1 build2]
},
needs_attributes: [ needs_attributes: [
{ name: "build1" }, { name: "build1" },
{ name: "build2" } { name: "build2" }
...@@ -1350,10 +1346,25 @@ module Gitlab ...@@ -1350,10 +1346,25 @@ module Gitlab
end end
end end
context 'needs two builds defined as symbols' do context 'needs parallel job' do
let(:needs) { [:build1, :build2] } let(:needs) { %w(parallel) }
it { expect { subject }.not_to raise_error } it "does create jobs with valid specification" do
expect(subject.builds.size).to eq(7)
expect(subject.builds[4]).to eq(
stage: "test",
stage_idx: 2,
name: "test1",
options: { script: ["test"] },
needs_attributes: [
{ name: "parallel 1/2" },
{ name: "parallel 2/2" }
],
when: "on_success",
allow_failure: false,
yaml_variables: []
)
end
end end
context 'undefined need' do context 'undefined need' do
......
...@@ -2893,6 +2893,25 @@ describe Ci::Pipeline, :mailer do ...@@ -2893,6 +2893,25 @@ describe Ci::Pipeline, :mailer do
it 'contains yaml errors' do it 'contains yaml errors' do
expect(pipeline).to have_yaml_errors expect(pipeline).to have_yaml_errors
expect(pipeline.yaml_errors).to include('contains unknown keys')
end
end
context 'when pipeline has undefined error' do
let(:pipeline) do
create(:ci_pipeline, config: {})
end
it 'contains yaml errors' do
expect(::Gitlab::Ci::YamlProcessor).to receive(:new)
.and_raise(RuntimeError, 'undefined failure')
expect(Gitlab::Sentry).to receive(:track_acceptable_exception)
.with(be_a(RuntimeError), anything)
.and_call_original
expect(pipeline).to have_yaml_errors
expect(pipeline.yaml_errors).to include('Undefined error')
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::DestroyService do
describe '#execute' do
subject { described_class.new(cluster.user, params).execute(cluster) }
let!(:cluster) { create(:cluster, :project, :provided_by_user) }
context 'when correct params' do
shared_examples 'only removes cluster' do
it 'does not start cleanup' do
expect(cluster).not_to receive(:start_cleanup)
subject
end
it 'destroys the cluster' do
subject
expect { cluster.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
context 'when params are empty' do
let(:params) { {} }
it_behaves_like 'only removes cluster'
end
context 'when cleanup param is false' do
let(:params) { { cleanup: 'false' } }
it_behaves_like 'only removes cluster'
end
context 'when cleanup param is true' do
let(:params) { { cleanup: 'true' } }
before do
allow(Clusters::Cleanup::AppWorker).to receive(:perform_async)
end
it 'does not destroy cluster' do
subject
expect(Clusters::Cluster.where(id: cluster.id).exists?).not_to be_falsey
end
it 'transition cluster#cleanup_status from cleanup_not_started to uninstalling_applications' do
expect { subject }.to change { cluster.cleanup_status_name }
.from(:cleanup_not_started)
.to(:cleanup_uninstalling_applications)
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment