summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2018-10-03 14:46:14 +0900
committerShinya Maeda <shinya@gitlab.com>2018-10-03 14:46:14 +0900
commit2a6fb57791dee6f95edaf75f633916e380ba692a (patch)
tree905817422a31aafb8b4cc7981abaddf613f466e0
parentd7401cd6ce8af3f82df7e48fe23cfda07673b6df (diff)
downloadgitlab-ce-scheduled-manual-jobs-fe.tar.gz
Revert "Decouple FE"scheduled-manual-jobs-fe
This reverts commit d7401cd6ce8af3f82df7e48fe23cfda07673b6df.
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_scheduled.icobin0 -> 5430 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_scheduled.pngbin0 -> 1072 bytes
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js20
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue31
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue7
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/icons.scss1
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss1
-rw-r--r--app/assets/stylesheets/pages/status.scss1
-rw-r--r--app/controllers/projects/jobs_controller.rb7
-rw-r--r--app/helpers/ci_status_helper.rb6
-rw-r--r--app/helpers/time_helper.rb14
-rw-r--r--app/presenters/ci/build_presenter.rb4
-rw-r--r--app/presenters/commit_status_presenter.rb3
-rw-r--r--app/serializers/build_action_entity.rb5
-rw-r--r--app/serializers/job_entity.rb1
-rw-r--r--app/serializers/pipeline_details_entity.rb1
-rw-r--r--app/serializers/pipeline_serializer.rb1
-rw-r--r--app/views/projects/ci/builds/_build.html.haml22
-rw-r--r--app/views/shared/icons/_icon_status_scheduled.svg1
-rw-r--r--app/views/shared/icons/_icon_status_scheduled_borderless.svg1
-rw-r--r--config/routes/project.rb1
-rw-r--r--lib/gitlab/ci/status/build/factory.rb2
-rw-r--r--lib/gitlab/ci/status/build/failed.rb3
-rw-r--r--lib/gitlab/ci/status/build/scheduled.rb38
-rw-r--r--lib/gitlab/ci/status/build/unschedule.rb41
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb1
-rw-r--r--lib/gitlab/ci/status/pipeline/scheduled.rb21
-rw-r--r--lib/gitlab/ci/status/scheduled.rb23
-rw-r--r--lib/gitlab/ci/yaml_processor.rb3
-rw-r--r--locale/gitlab.pot33
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb40
-rw-r--r--spec/helpers/time_helper_spec.rb38
-rw-r--r--spec/javascripts/datetime_utility_spec.js25
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb65
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/status/build/scheduled_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/scheduled_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb17
-rw-r--r--spec/lib/gitlab/favicon_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--yarn.lock6
44 files changed, 612 insertions, 61 deletions
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico b/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico
new file mode 100644
index 00000000000..5444b8e41dc
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_scheduled.png b/app/assets/images/ci_favicons/favicon_status_scheduled.png
new file mode 100644
index 00000000000..d198c255fdd
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_scheduled.png
Binary files differ
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 1f66fa811ea..abfcf1eaf3f 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -370,3 +370,23 @@ window.gl.utils = {
getTimeago,
localTimeAgo,
};
+
+/**
+ * Formats milliseconds as timestamp (e.g. 01:02:03).
+ *
+ * @param milliseconds
+ * @returns {string}
+ */
+export const formatTime = milliseconds => {
+ const remainingSeconds = Math.floor(milliseconds / 1000) % 60;
+ const remainingMinutes = Math.floor(milliseconds / 1000 / 60) % 60;
+ const remainingHours = Math.floor(milliseconds / 1000 / 60 / 60);
+ let formattedTime = '';
+ if (remainingHours < 10) formattedTime += '0';
+ formattedTime += `${remainingHours}:`;
+ if (remainingMinutes < 10) formattedTime += '0';
+ formattedTime += `${remainingMinutes}:`;
+ if (remainingSeconds < 10) formattedTime += '0';
+ formattedTime += remainingSeconds;
+ return formattedTime;
+};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 017dd560621..1768c7dc2b3 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,4 +1,6 @@
<script>
+import { s__, sprintf } from '~/locale';
+import { formatTime } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub';
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -22,10 +24,17 @@ export default {
};
},
methods: {
- onClickAction(endpoint) {
+ onClickAction(action) {
+ const confirmationMessage = sprintf(s__("DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes."), { jobName: action.name });
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/52099
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(confirmationMessage)) {
+ return;
+ }
+
this.isLoading = true;
- eventHub.$emit('postAction', endpoint);
+ eventHub.$emit('postAction', action.path);
},
isActionDisabled(action) {
@@ -35,6 +44,11 @@ export default {
return !action.playable;
},
+
+ remainingTime(action) {
+ const remainingMilliseconds = new Date(action.scheduled_at).getTime() - Date.now();
+ return formatTime(remainingMilliseconds);
+ },
},
};
</script>
@@ -63,17 +77,24 @@ export default {
<ul class="dropdown-menu dropdown-menu-right">
<li
- v-for="(action, i) in actions"
- :key="i"
+ v-for="action in actions"
+ :key="action.path"
>
<button
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
type="button"
class="js-pipeline-action-link no-btn btn"
- @click="onClickAction(action.path)"
+ @click="onClickAction(action)"
>
{{ action.name }}
+ <span
+ v-if="action.scheduled_at"
+ class="pull-right"
+ >
+ <icon name="clock" />
+ {{ remainingTime(action) }}
+ </span>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index a39cc265601..bae6ff43ee4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -59,6 +59,9 @@ export default {
};
},
computed: {
+ actions() {
+ return [...this.pipeline.details.manual_actions, ...this.pipeline.details.scheduled_actions];
+ },
/**
* If provided, returns the commit tag.
* Needed to render the commit component column.
@@ -321,8 +324,8 @@ export default {
>
<div class="btn-group table-action-buttons">
<pipelines-actions-component
- v-if="pipeline.details.manual_actions.length"
- :actions="pipeline.details.manual_actions"
+ v-if="actions.length > 0"
+ :actions="actions"
/>
<pipelines-artifacts-component
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 686ce0c63a4..c4296c7a88a 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -360,6 +360,10 @@
i {
color: $gl-text-color-secondary;
}
+
+ svg {
+ fill: $gl-text-color-secondary;
+ }
}
.clone-dropdown-btn a {
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index f002edced8a..abd26e38d18 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -64,6 +64,7 @@
}
}
+.ci-status-icon-scheduled,
.ci-status-icon-manual {
svg {
fill: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8bb8b83dc5e..14395cc59b0 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -760,6 +760,7 @@
}
&.ci-status-icon-canceled,
+ &.ci-status-icon-scheduled,
&.ci-status-icon-disabled,
&.ci-status-icon-not-found,
&.ci-status-icon-manual {
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 620297e589d..7d59dd3b5d1 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -27,6 +27,7 @@
&.ci-canceled,
&.ci-disabled,
+ &.ci-scheduled,
&.ci-manual {
color: $gl-text-color;
border-color: $gl-text-color;
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 3f85e442be9..9c9bbe04947 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -110,6 +110,13 @@ class Projects::JobsController < Projects::ApplicationController
redirect_to build_path(@build)
end
+ def unschedule
+ return respond_422 unless @build.scheduled?
+
+ @build.unschedule!
+ redirect_to build_path(@build)
+ end
+
def status
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 136772e1ec3..6f9e2ef78cd 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -20,6 +20,8 @@ module CiStatusHelper
'passed with warnings'
when 'manual'
'waiting for manual action'
+ when 'scheduled'
+ 'waiting for delayed job'
else
status
end
@@ -39,6 +41,8 @@ module CiStatusHelper
s_('CiStatusText|passed')
when 'manual'
s_('CiStatusText|blocked')
+ when 'scheduled'
+ s_('CiStatusText|scheduled')
else
# All states are already being translated inside the detailed statuses:
# :running => Gitlab::Ci::Status::Running
@@ -83,6 +87,8 @@ module CiStatusHelper
'status_skipped'
when 'manual'
'status_manual'
+ when 'scheduled'
+ 'status_scheduled'
else
'status_canceled'
end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 94044d7b85e..3e6a301b77d 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -21,9 +21,17 @@ module TimeHelper
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
- def duration_in_numbers(duration)
- time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
+ def duration_in_numbers(duration_in_seconds, allow_overflow = false)
+ if allow_overflow
+ seconds = duration_in_seconds % 1.minute
+ minutes = (duration_in_seconds / 1.minute) % (1.hour / 1.minute)
+ hours = duration_in_seconds / 1.hour
- Time.at(duration).utc.strftime(time_format)
+ "%02d:%02d:%02d" % [hours, minutes, seconds]
+ else
+ time_format = duration_in_seconds < 1.hour ? "%M:%S" : "%H:%M:%S"
+
+ Time.at(duration_in_seconds).utc.strftime(time_format)
+ end
end
end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 5331cdf632b..33056a809b7 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -35,6 +35,10 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
+ def execute_in
+ scheduled? && scheduled_at && [0, scheduled_at - Time.now].max
+ end
+
private
def tooltip_for_badge
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index 65e77ea3f92..29eaad759bb 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -8,7 +8,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure',
- runner_unsupported: 'Your runner is outdated, please upgrade your runner'
+ runner_unsupported: 'Your runner is outdated, please upgrade your runner',
+ stale_schedule: 'Delayed job could not be executed by some reason, please try again'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
index f9da3f63911..0db7875aa87 100644
--- a/app/serializers/build_action_entity.rb
+++ b/app/serializers/build_action_entity.rb
@@ -12,6 +12,11 @@ class BuildActionEntity < Grape::Entity
end
expose :playable?, as: :playable
+ expose :scheduled_at, if: -> (build) { build.scheduled? }
+
+ expose :unschedule_path, if: -> (build) { build.scheduled? } do |build|
+ unschedule_project_job_path(build.project, build)
+ end
private
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 26b29993fec..dd6c2fa1a6d 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -25,6 +25,7 @@ class JobEntity < Grape::Entity
end
expose :playable?, as: :playable
+ expose :scheduled_at
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: DetailedStatusEntity
diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index 3b56767f774..d78ad4af4dc 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -5,5 +5,6 @@ class PipelineDetailsEntity < PipelineEntity
expose :ordered_stages, as: :stages, using: StageEntity
expose :artifacts, using: BuildArtifactEntity
expose :manual_actions, using: BuildActionEntity
+ expose :scheduled_actions, using: BuildActionEntity
end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 4f31af3c46d..7451433a841 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -13,6 +13,7 @@ class PipelineSerializer < BaseSerializer
:cancelable_statuses,
:trigger_requests,
:manual_actions,
+ :scheduled_actions,
:artifacts,
{
pending_builds: :project,
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 44c1453e239..59c297c46a5 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -47,7 +47,9 @@
%span.badge.badge-info triggered
- if job.try(:allow_failure)
%span.badge.badge-danger allowed to fail
- - if job.action?
+ - if job.schedulable?
+ %span.badge.badge-info= s_('DelayedJobs|scheduled')
+ - elsif job.action?
%span.badge.badge-info manual
- if pipeline_link
@@ -101,6 +103,24 @@
- if job.active?
= link_to cancel_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred')
+ - elsif job.scheduled?
+ .btn-group
+ .btn.btn-default.has-tooltip{ disabled: true,
+ title: job.scheduled_at }
+ = sprite_icon('planning')
+ = duration_in_numbers(job.execute_in, true)
+ - confirmation_message = s_("DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes.") % { job_name: job.name }
+ = link_to play_project_job_path(job.project, job, return_to: request.original_url),
+ method: :post,
+ title: s_('DelayedJobs|Start now'),
+ class: 'btn btn-default btn-build has-tooltip',
+ data: { confirm: confirmation_message } do
+ = sprite_icon('play')
+ = link_to unschedule_project_job_path(job.project, job, return_to: request.original_url),
+ method: :post,
+ title: s_('DelayedJobs|Unschedule'),
+ class: 'btn btn-default btn-build has-tooltip' do
+ = sprite_icon('time-out')
- elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job)
= link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
diff --git a/app/views/shared/icons/_icon_status_scheduled.svg b/app/views/shared/icons/_icon_status_scheduled.svg
new file mode 100644
index 00000000000..ca6e4efce50
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_scheduled.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><circle cx="7" cy="7" r="7"/><circle fill="#FFF" cx="7" cy="7" r="6"/><g transform="translate(2.75 2.75)" fill-rule="nonzero"><path d="M4.165 7.81a3.644 3.644 0 1 1 0-7.29 3.644 3.644 0 0 1 0 7.29zm0-1.042a2.603 2.603 0 1 0 0-5.206 2.603 2.603 0 0 0 0 5.206z"/><rect x="3.644" y="2.083" width="1.041" height="2.603" rx=".488"/><rect x="3.644" y="3.644" width="2.083" height="1.041" rx=".488"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_status_scheduled_borderless.svg b/app/views/shared/icons/_icon_status_scheduled_borderless.svg
new file mode 100644
index 00000000000..dc38c01d898
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_scheduled_borderless.svg
@@ -0,0 +1 @@
+<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M6.16 11.55a5.39 5.39 0 1 1 0-10.78 5.39 5.39 0 0 1 0 10.78zm0-1.54a3.85 3.85 0 1 0 0-7.7 3.85 3.85 0 0 0 0 7.7z"/><rect x="5.39" y="3.08" width="1.54" height="3.85" rx=".767"/><rect x="5.39" y="5.39" width="3.08" height="1.54" rx=".767"/></svg> \ No newline at end of file
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 8a5310b5c23..04a270c5cc9 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -275,6 +275,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
member do
get :status
post :cancel
+ post :unschedule
post :retry
post :play
post :erase
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index 2b26ebb45a1..4a74d6d6ed1 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -5,6 +5,7 @@ module Gitlab
class Factory < Status::Factory
def self.extended_statuses
[[Status::Build::Erased,
+ Status::Build::Scheduled,
Status::Build::Manual,
Status::Build::Canceled,
Status::Build::Created,
@@ -14,6 +15,7 @@ module Gitlab
Status::Build::Retryable],
[Status::Build::Failed],
[Status::Build::FailedAllowed,
+ Status::Build::Unschedule,
Status::Build::Play,
Status::Build::Stop],
[Status::Build::Action],
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 2fa9a0d4541..50b0d044265 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -10,7 +10,8 @@ module Gitlab
stuck_or_timeout_failure: 'stuck or timeout failure',
runner_system_failure: 'runner system failure',
missing_dependency_failure: 'missing dependency failure',
- runner_unsupported: 'unsupported runner'
+ runner_unsupported: 'unsupported runner',
+ stale_schedule: 'stale schedule'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/status/build/scheduled.rb b/lib/gitlab/ci/status/build/scheduled.rb
new file mode 100644
index 00000000000..eebb3f761c5
--- /dev/null
+++ b/lib/gitlab/ci/status/build/scheduled.rb
@@ -0,0 +1,38 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Scheduled < Status::Extended
+ def illustration
+ {
+ image: 'illustrations/illustrations_scheduled-job_countdown.svg',
+ size: 'svg-394',
+ title: _("This is a scheduled to run in ") + " #{execute_in}",
+ content: _("This job will automatically run after it's timer finishes. " \
+ "Often they are used for incremental roll-out deploys " \
+ "to production environments. When unscheduled it converts " \
+ "into a manual action.")
+ }
+ end
+
+ def status_tooltip
+ "scheduled manual action (#{execute_in})"
+ end
+
+ def self.matches?(build, user)
+ build.scheduled? && build.scheduled_at
+ end
+
+ private
+
+ include TimeHelper
+
+ def execute_in
+ remaining_seconds = [0, subject.scheduled_at - Time.now].max
+ duration_in_numbers(remaining_seconds, true)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/unschedule.rb b/lib/gitlab/ci/status/build/unschedule.rb
new file mode 100644
index 00000000000..e1b7b83428c
--- /dev/null
+++ b/lib/gitlab/ci/status/build/unschedule.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class Unschedule < Status::Extended
+ def label
+ 'unschedule action'
+ end
+
+ def has_action?
+ can?(user, :update_build, subject)
+ end
+
+ def action_icon
+ 'time-out'
+ end
+
+ def action_title
+ 'Unschedule'
+ end
+
+ def action_button_title
+ _('Unschedule job')
+ end
+
+ def action_path
+ unschedule_project_job_path(subject.project, subject)
+ end
+
+ def action_method
+ :post
+ end
+
+ def self.matches?(build, user)
+ build.scheduled?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 17f9a75f436..00d8f01cbdc 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -5,6 +5,7 @@ module Gitlab
class Factory < Status::Factory
def self.extended_statuses
[[Status::SuccessWarning,
+ Status::Pipeline::Scheduled,
Status::Pipeline::Blocked]]
end
diff --git a/lib/gitlab/ci/status/pipeline/scheduled.rb b/lib/gitlab/ci/status/pipeline/scheduled.rb
new file mode 100644
index 00000000000..9ec6994bd2f
--- /dev/null
+++ b/lib/gitlab/ci/status/pipeline/scheduled.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Ci
+ module Status
+ module Pipeline
+ class Scheduled < Status::Extended
+ def text
+ s_('CiStatusText|scheduled')
+ end
+
+ def label
+ s_('CiStatusLabel|waiting for delayed job')
+ end
+
+ def self.matches?(pipeline, user)
+ pipeline.scheduled?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
new file mode 100644
index 00000000000..542100e41da
--- /dev/null
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Ci
+ module Status
+ class Scheduled < Status::Core
+ def text
+ s_('CiStatusText|scheduled')
+ end
+
+ def label
+ s_('CiStatusLabel|scheduled')
+ end
+
+ def icon
+ 'status_scheduled'
+ end
+
+ def favicon
+ 'favicon_status_scheduled'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 5d1864ae9e2..a427aa30683 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -49,7 +49,8 @@ module Gitlab
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
- retry: job[:retry]
+ retry: job[:retry],
+ start_in: job[:start_in]
}.compact }
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cc11577b624..29ab9f7d6eb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1221,9 +1221,15 @@ msgstr ""
msgid "CiStatusLabel|pending"
msgstr ""
+msgid "CiStatusLabel|scheduled"
+msgstr ""
+
msgid "CiStatusLabel|skipped"
msgstr ""
+msgid "CiStatusLabel|waiting for delayed job"
+msgstr ""
+
msgid "CiStatusLabel|waiting for manual action"
msgstr ""
@@ -1248,6 +1254,9 @@ msgstr ""
msgid "CiStatusText|pending"
msgstr ""
+msgid "CiStatusText|scheduled"
+msgstr ""
+
msgid "CiStatusText|skipped"
msgstr ""
@@ -2134,6 +2143,21 @@ msgstr ""
msgid "Define a custom pattern with cron syntax"
msgstr ""
+msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes."
+msgstr ""
+
+msgid "DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes."
+msgstr ""
+
+msgid "DelayedJobs|Start now"
+msgstr ""
+
+msgid "DelayedJobs|Unschedule"
+msgstr ""
+
+msgid "DelayedJobs|scheduled"
+msgstr ""
+
msgid "Delete"
msgstr ""
@@ -6051,6 +6075,9 @@ msgstr ""
msgid "This is a confidential issue."
msgstr ""
+msgid "This is a scheduled to run in "
+msgstr ""
+
msgid "This is the author's first Merge Request to this project."
msgstr ""
@@ -6111,6 +6138,9 @@ msgstr ""
msgid "This job requires a manual action"
msgstr ""
+msgid "This job will automatically run after it's timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
@@ -6463,6 +6493,9 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unschedule job"
+msgstr ""
+
msgid "Unstage"
msgstr ""
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index fd11cb31a2a..5b09e4a082c 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -632,6 +632,46 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
+ describe 'POST unschedule' do
+ before do
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: 'master', project: project)
+
+ sign_in(user)
+
+ post_unschedule
+ end
+
+ context 'when job is scheduled' do
+ let(:job) { create(:ci_build, :scheduled, pipeline: pipeline) }
+
+ it 'redirects to the unscheduled job page' do
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(namespace_project_job_path(id: job.id))
+ end
+
+ it 'transits to manual' do
+ expect(job.reload).to be_manual
+ end
+ end
+
+ context 'when job is not scheduled' do
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'renders unprocessable_entity' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ def post_unschedule
+ post :unschedule, namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
+ end
+ end
+
describe 'POST cancel_all' do
before do
project.add_developer(user)
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 0b371d69ecf..cc310766433 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -20,17 +20,35 @@ describe TimeHelper do
end
describe "#duration_in_numbers" do
- it "returns minutes and seconds" do
- durations_and_expectations = {
- 100 => "01:40",
- 121 => "02:01",
- 3721 => "01:02:01",
- 0 => "00:00",
- 42 => "00:42"
- }
+ using RSpec::Parameterized::TableSyntax
+
+ context "without passing allow_overflow" do
+ where(:duration, :formatted_string) do
+ 0 | "00:00"
+ 1.second | "00:01"
+ 42.seconds | "00:42"
+ 2.minutes + 1.second | "02:01"
+ 3.hours + 2.minutes + 1.second | "03:02:01"
+ 30.hours | "06:00:00"
+ end
+
+ with_them do
+ it { expect(duration_in_numbers(duration)).to eq formatted_string }
+ end
+ end
+
+ context "with allow_overflow = true" do
+ where(:duration, :formatted_string) do
+ 0 | "00:00:00"
+ 1.second | "00:00:01"
+ 42.seconds | "00:00:42"
+ 2.minutes + 1.second | "00:02:01"
+ 3.hours + 2.minutes + 1.second | "03:02:01"
+ 30.hours | "30:00:00"
+ end
- durations_and_expectations.each do |duration, expectation|
- expect(duration_in_numbers(duration)).to eq(expectation)
+ with_them do
+ it { expect(duration_in_numbers(duration, true)).to eq formatted_string }
end
end
end
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index 492171684dc..6c3e73f134e 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -6,9 +6,7 @@ describe('Date time utils', () => {
const date = new Date();
date.setFullYear(date.getFullYear() - 1);
- expect(
- datetimeUtility.timeFor(date),
- ).toBe('Past due');
+ expect(datetimeUtility.timeFor(date)).toBe('Past due');
});
it('returns remaining time when in the future', () => {
@@ -19,9 +17,7 @@ describe('Date time utils', () => {
// short of a full year, timeFor will return '11 months remaining'
date.setDate(date.getDate() + 1);
- expect(
- datetimeUtility.timeFor(date),
- ).toBe('1 year remaining');
+ expect(datetimeUtility.timeFor(date)).toBe('1 year remaining');
});
});
@@ -168,3 +164,20 @@ describe('getTimeframeWindowFrom', () => {
});
});
});
+
+describe('formatTime', () => {
+ const expectedTimestamps = [
+ [0, '00:00:00'],
+ [1000, '00:00:01'],
+ [42000, '00:00:42'],
+ [121000, '00:02:01'],
+ [10921000, '03:02:01'],
+ [108000000, '30:00:00'],
+ ];
+
+ expectedTimestamps.forEach(([milliseconds, expectedTimestamp]) => {
+ it(`formats ${milliseconds}ms as ${expectedTimestamp}`, () => {
+ expect(datetimeUtility.formatTime(milliseconds)).toBe(expectedTimestamp);
+ });
+ });
+});
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 2c9758401b7..d745c4ca2ad 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -39,6 +39,16 @@ describe Gitlab::Ci::Config::Entry::Job do
expect(entry.errors).to include "job name can't be blank"
end
end
+
+ context 'when delayed job' do
+ context 'when start_in is specified' do
+ let(:config) { { script: 'echo', when: 'delayed', start_in: '1 day' } }
+
+ it 'returns error about invalid type' do
+ expect(entry).to be_valid
+ end
+ end
+ end
end
context 'when entry value is not correct' do
@@ -129,6 +139,43 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
end
+
+ context 'when delayed job' do
+ context 'when start_in is specified' do
+ let(:config) { { script: 'echo', when: 'delayed', start_in: '1 day' } }
+
+ it 'returns error about invalid type' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when start_in is empty' do
+ let(:config) { { when: 'delayed', start_in: nil } }
+
+ it 'returns error about invalid type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job start in should be a duration'
+ end
+ end
+
+ context 'when start_in is not formateed ad a duration' do
+ let(:config) { { when: 'delayed', start_in: 'test' } }
+
+ it 'returns error about invalid type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job start in should be a duration'
+ end
+ end
+ end
+
+ context 'when start_in specified without delayed specification' do
+ let(:config) { { start_in: '1 day' } }
+
+ it 'returns error about invalid type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job start in must be blank'
+ end
+ end
end
end
@@ -238,6 +285,24 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
+ describe '#delayed?' do
+ context 'when job is a delayed' do
+ let(:config) { { script: 'deploy', when: 'delayed' } }
+
+ it 'is a delayed' do
+ expect(entry).to be_delayed
+ end
+ end
+
+ context 'when job is not a delayed' do
+ let(:config) { { script: 'deploy' } }
+
+ it 'is not a delayed' do
+ expect(entry).not_to be_delayed
+ end
+ end
+ end
+
describe '#ignored?' do
context 'when job is a manual action' do
context 'when it is not specified if job is allowed to fail' do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 8b92088902b..aa53ecd5967 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -319,4 +319,53 @@ describe Gitlab::Ci::Status::Build::Factory do
end
end
end
+
+ context 'when build is a delayed action' do
+ let(:build) { create(:ci_build, :scheduled) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Scheduled
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Scheduled,
+ Gitlab::Ci::Status::Build::Unschedule,
+ Gitlab::Ci::Status::Build::Action]
+ end
+
+ it 'fabricates action detailed status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Action
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'scheduled'
+ expect(status.group).to eq 'scheduled'
+ expect(status.icon).to eq 'status_scheduled'
+ expect(status.favicon).to eq 'favicon_status_scheduled'
+ expect(status.illustration).to include(:image, :size, :title, :content)
+ expect(status.label).to include 'unschedule action'
+ expect(status).to have_details
+ expect(status.action_path).to include 'unschedule'
+ end
+
+ context 'when user has ability to play action' do
+ it 'fabricates status that has action' do
+ expect(status).to have_action
+ end
+ end
+
+ context 'when user does not have ability to play action' do
+ before do
+ allow(build.project).to receive(:empty_repo?).and_return(false)
+
+ create(:protected_branch, :no_one_can_push,
+ name: build.ref, project: build.project)
+ end
+
+ it 'fabricates status that has no action' do
+ expect(status).not_to have_action
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/build/scheduled_spec.rb b/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
new file mode 100644
index 00000000000..3098a17c50d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Scheduled do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :stubbed_repository) }
+ let(:build) { create(:ci_build, :scheduled, project: project) }
+ let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
+
+ subject { described_class.new(status) }
+
+ describe '#illustration' do
+ it { expect(subject.illustration).to include(:image, :size, :title) }
+ end
+
+ describe '#status_tooltip' do
+ context 'when scheduled_at is not expired' do
+ let(:build) { create(:ci_build, scheduled_at: 1.minute.since, project: project) }
+
+ it 'shows execute_in of the scheduled job' do
+ Timecop.freeze do
+ expect(subject.status_tooltip).to include('00:01:00')
+ end
+ end
+ end
+
+ context 'when scheduled_at is expired' do
+ let(:build) { create(:ci_build, :expired_scheduled, project: project) }
+
+ it 'shows 00:00:00' do
+ Timecop.freeze do
+ expect(subject.status_tooltip).to include('00:00:00')
+ end
+ end
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is scheduled and scheduled_at is present' do
+ let(:build) { create(:ci_build, :expired_scheduled, project: project) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when build is scheduled' do
+ let(:build) { create(:ci_build, status: :scheduled, project: project) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when scheduled_at is present' do
+ let(:build) { create(:ci_build, scheduled_at: 1.minute.since, project: project) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index defb3fdc0df..694d4ce160a 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -11,8 +11,7 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
end
context 'when pipeline has a core status' do
- (HasStatus::AVAILABLE_STATUSES - [HasStatus::BLOCKED_STATUS])
- .each do |simple_status|
+ HasStatus::AVAILABLE_STATUSES.each do |simple_status|
context "when core status is #{simple_status}" do
let(:pipeline) { create(:ci_pipeline, status: simple_status) }
@@ -24,12 +23,24 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
expect(factory.core_status).to be_a expected_status
end
- it 'does not match extended statuses' do
- expect(factory.extended_statuses).to be_empty
- end
-
- it "fabricates a core status #{simple_status}" do
- expect(status).to be_a expected_status
+ if simple_status == 'manual'
+ it 'matches a correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Pipeline::Blocked]
+ end
+ elsif simple_status == 'scheduled'
+ it 'matches a correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Pipeline::Scheduled]
+ end
+ else
+ it 'does not match extended statuses' do
+ expect(factory.extended_statuses).to be_empty
+ end
+
+ it "fabricates a core status #{simple_status}" do
+ expect(status).to be_a expected_status
+ end
end
it 'extends core status with common pipeline methods' do
@@ -40,27 +51,6 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
end
end
end
-
- context "when core status is manual" do
- let(:pipeline) { create(:ci_pipeline, status: :manual) }
-
- it "matches manual core status" do
- expect(factory.core_status)
- .to be_a Gitlab::Ci::Status::Manual
- end
-
- it 'matches a correct extended statuses' do
- expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Pipeline::Blocked]
- end
-
- it 'extends core status with common pipeline methods' do
- expect(status).to have_details
- expect(status).not_to have_action
- expect(status.details_path)
- .to include "pipelines/#{pipeline.id}"
- end
- end
end
context 'when pipeline has warnings' do
diff --git a/spec/lib/gitlab/ci/status/scheduled_spec.rb b/spec/lib/gitlab/ci/status/scheduled_spec.rb
new file mode 100644
index 00000000000..c35a6f43d5d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/scheduled_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Scheduled do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'scheduled' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'scheduled' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'status_scheduled' }
+ end
+
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_scheduled' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'scheduled' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index a2d429fa859..d75c473eb66 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -121,6 +121,21 @@ module Gitlab
end
end
end
+
+ describe 'delayed job entry' do
+ context 'when delayed is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rollout 10%',
+ when: 'delayed',
+ start_in: '1 day' })
+ end
+
+ it 'has the attributes' do
+ expect(subject[:when]).to eq 'delayed'
+ expect(subject[:options][:start_in]).to eq '1 day'
+ end
+ end
+ end
end
describe '#stages_attributes' do
@@ -1260,7 +1275,7 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always, manual or delayed")
end
it "returns errors if job artifacts:name is not an a string" do
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 68abcb3520a..49a423191bb 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -58,6 +58,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
favicon_status_not_found
favicon_status_pending
favicon_status_running
+ favicon_status_scheduled
favicon_status_skipped
favicon_status_success
favicon_status_warning
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ec2bdbe22e1..fe167033941 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -117,6 +117,7 @@ pipelines:
- retryable_builds
- cancelable_statuses
- manual_actions
+- scheduled_actions
- artifacts
- pipeline_schedule
- merge_requests
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index e9f1be172b0..1d59cff7ba8 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -300,6 +300,7 @@ CommitStatus:
- retried
- protected
- failure_reason
+- scheduled_at
Ci::Variable:
- id
- project_id
diff --git a/yarn.lock b/yarn.lock
index 20b587d1fc5..f2e948b0dc2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -180,11 +180,15 @@
lodash "^4.17.10"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.0":
+"@gitlab-org/gitlab-svgs@^1.23.0":
version "1.29.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70"
integrity sha512-sCl6nP3ph36+8P3nrw9VanAR648rgOUEBlEoLPHkhKm79xB1dUkXGBtI0uaSJVgbJx40M1/Ts8HSdMv+PF3EIg==
+"@gitlab-org/gitlab-svgs@^1.29.0":
+ version "1.31.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.31.0.tgz#495b074669f93af40e34f9978ce887773dea470a"
+
"@gitlab-org/gitlab-ui@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.7.1.tgz#e9cce86cb7e34311405e705c1de674276b453f17"