Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Boxiang Sun
gitlab-ce
Commits
93780da6
Commit
93780da6
authored
Apr 19, 2018
by
Mayra Cabrera
Committed by
Grzegorz Bizon
Apr 19, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "Show `failure_reason` in jobs view content section"
parent
6ef8b497
Changes
20
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
517 additions
and
213 deletions
+517
-213
app/assets/javascripts/jobs/components/header.vue
app/assets/javascripts/jobs/components/header.vue
+81
-69
app/assets/javascripts/jobs/components/sidebar_details_block.vue
...ets/javascripts/jobs/components/sidebar_details_block.vue
+112
-73
app/assets/javascripts/jobs/job_details_bundle.js
app/assets/javascripts/jobs/job_details_bundle.js
+4
-1
app/assets/javascripts/vue_shared/components/callout.vue
app/assets/javascripts/vue_shared/components/callout.vue
+27
-0
app/assets/stylesheets/pages/builds.scss
app/assets/stylesheets/pages/builds.scss
+29
-12
app/controllers/projects/jobs_controller.rb
app/controllers/projects/jobs_controller.rb
+2
-0
app/presenters/ci/build_presenter.rb
app/presenters/ci/build_presenter.rb
+21
-0
app/serializers/job_entity.rb
app/serializers/job_entity.rb
+18
-0
app/views/projects/jobs/_sidebar.html.haml
app/views/projects/jobs/_sidebar.html.haml
+1
-8
doc/ci/pipelines.md
doc/ci/pipelines.md
+4
-1
lib/gitlab/view/presenter/base.rb
lib/gitlab/view/presenter/base.rb
+4
-0
spec/controllers/projects/jobs_controller_spec.rb
spec/controllers/projects/jobs_controller_spec.rb
+4
-1
spec/factories/ci/builds.rb
spec/factories/ci/builds.rb
+5
-0
spec/features/projects/jobs_spec.rb
spec/features/projects/jobs_spec.rb
+5
-3
spec/javascripts/jobs/header_spec.js
spec/javascripts/jobs/header_spec.js
+27
-7
spec/javascripts/jobs/sidebar_details_block_spec.js
spec/javascripts/jobs/sidebar_details_block_spec.js
+33
-28
spec/javascripts/vue_shared/components/callout_spec.js
spec/javascripts/vue_shared/components/callout_spec.js
+45
-0
spec/lib/gitlab/view/presenter/base_spec.rb
spec/lib/gitlab/view/presenter/base_spec.rb
+7
-0
spec/presenters/ci/build_presenter_spec.rb
spec/presenters/ci/build_presenter_spec.rb
+35
-0
spec/serializers/job_entity_spec.rb
spec/serializers/job_entity_spec.rb
+53
-10
No files found.
app/assets/javascripts/jobs/components/header.vue
View file @
93780da6
<
script
>
<
script
>
import
ciHeader
from
'
../../vue_shared/components/header_ci_component.vue
'
;
import
ciHeader
from
'
../../vue_shared/components/header_ci_component.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
callout
from
'
../../vue_shared/components/callout.vue
'
;
export
default
{
export
default
{
name
:
'
JobHeaderSection
'
,
name
:
'
JobHeaderSection
'
,
components
:
{
components
:
{
ciHeader
,
ciHeader
,
loadingIcon
,
loadingIcon
,
callout
,
},
props
:
{
job
:
{
type
:
Object
,
required
:
true
,
},
},
props
:
{
isLoading
:
{
job
:
{
type
:
Boolean
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
isLoading
:
{
type
:
Boolean
,
required
:
true
,
},
},
},
data
()
{
},
return
{
data
()
{
actions
:
this
.
getActions
(),
return
{
};
actions
:
this
.
getActions
(),
};
},
computed
:
{
status
()
{
return
this
.
job
&&
this
.
job
.
status
;
},
},
computed
:
{
shouldRenderContent
()
{
status
()
{
return
!
this
.
isLoading
&&
Object
.
keys
(
this
.
job
).
length
;
return
this
.
job
&&
this
.
job
.
status
;
},
shouldRenderContent
()
{
return
!
this
.
isLoading
&&
Object
.
keys
(
this
.
job
).
length
;
},
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
*/
jobStarted
()
{
return
!
this
.
job
.
started
===
false
;
},
},
},
watch
:
{
shouldRenderReason
()
{
job
()
{
return
!!
(
this
.
job
.
status
&&
this
.
job
.
callout_message
);
this
.
actions
=
this
.
getActions
();
},
},
},
methods
:
{
/**
getActions
()
{
* When job has not started the key will be `false`
const
actions
=
[];
* When job started the key will be a string with a date.
*/
jobStarted
()
{
return
!
this
.
job
.
started
===
false
;
},
},
watch
:
{
job
()
{
this
.
actions
=
this
.
getActions
();
},
},
methods
:
{
getActions
()
{
const
actions
=
[];
if
(
this
.
job
.
new_issue_path
)
{
if
(
this
.
job
.
new_issue_path
)
{
actions
.
push
({
actions
.
push
({
label
:
'
New issue
'
,
label
:
'
New issue
'
,
path
:
this
.
job
.
new_issue_path
,
path
:
this
.
job
.
new_issue_path
,
cssClass
:
'
js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block
'
,
cssClass
:
'
js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block
'
,
type
:
'
link
'
,
type
:
'
link
'
,
});
});
}
}
return
actions
;
return
actions
;
},
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
<div
class=
"js-build-header build-header top-area"
>
<header>
<ci-header
<div
class=
"js-build-header build-header top-area"
>
v-if=
"shouldRenderContent"
<ci-header
:status=
"status"
v-if=
"shouldRenderContent"
item-name=
"Job"
:status=
"status"
:item-id=
"job.id"
item-name=
"Job"
:time=
"job.created_at"
:item-id=
"job.id"
:user=
"job.user"
:time=
"job.created_at"
:actions=
"actions"
:user=
"job.user"
:has-sidebar-button=
"true"
:actions=
"actions"
:should-render-triggered-label=
"jobStarted"
:has-sidebar-button=
"true"
/>
:should-render-triggered-label=
"jobStarted"
<loading-icon
/>
v-if=
"isLoading"
<loading-icon
size=
"2"
v-if=
"isLoading"
class=
"prepend-top-default append-bottom-default"
size=
"2"
class=
"prepend-top-default append-bottom-default"
/>
</div>
<callout
v-if=
"shouldRenderReason"
:message=
"job.callout_message"
/>
/>
</
div
>
</
header
>
</
template
>
</
template
>
app/assets/javascripts/jobs/components/sidebar_details_block.vue
View file @
93780da6
<
script
>
<
script
>
import
detailRow
from
'
./sidebar_detail_row.vue
'
;
import
detailRow
from
'
./sidebar_detail_row.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
timeagoMixin
from
'
../../vue_shared/mixins/timeago
'
;
import
timeagoMixin
from
'
../../vue_shared/mixins/timeago
'
;
import
{
timeIntervalInWords
}
from
'
../../lib/utils/datetime_utility
'
;
import
{
timeIntervalInWords
}
from
'
../../lib/utils/datetime_utility
'
;
export
default
{
export
default
{
name
:
'
SidebarDetailsBlock
'
,
name
:
'
SidebarDetailsBlock
'
,
components
:
{
components
:
{
detailRow
,
detailRow
,
loadingIcon
,
loadingIcon
,
},
mixins
:
[
timeagoMixin
],
props
:
{
job
:
{
type
:
Object
,
required
:
true
,
},
},
mixins
:
[
isLoading
:
{
timeagoMixin
,
type
:
Boolean
,
],
required
:
true
,
props
:
{
job
:
{
type
:
Object
,
required
:
true
,
},
isLoading
:
{
type
:
Boolean
,
required
:
true
,
},
runnerHelpUrl
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
},
computed
:
{
canUserRetry
:
{
shouldRenderContent
()
{
type
:
Boolean
,
return
!
this
.
isLoading
&&
Object
.
keys
(
this
.
job
).
length
>
0
;
required
:
false
,
},
default
:
false
,
coverage
()
{
},
return
`
${
this
.
job
.
coverage
}
%`
;
runnerHelpUrl
:
{
},
type
:
String
,
duration
()
{
required
:
false
,
return
timeIntervalInWords
(
this
.
job
.
duration
);
default
:
''
,
},
},
queued
()
{
},
return
timeIntervalInWords
(
this
.
job
.
queued
);
computed
:
{
},
shouldRenderContent
()
{
runnerId
()
{
return
!
this
.
isLoading
&&
Object
.
keys
(
this
.
job
).
length
>
0
;
return
`#
${
this
.
job
.
runner
.
id
}
`
;
},
},
coverage
()
{
hasTimeout
()
{
return
`
${
this
.
job
.
coverage
}
%`
;
return
this
.
job
.
metadata
!=
null
&&
this
.
job
.
metadata
.
timeout_human_readable
!==
null
;
},
},
duration
()
{
timeout
()
{
return
timeIntervalInWords
(
this
.
job
.
duration
);
if
(
this
.
job
.
metadata
==
null
)
{
},
return
''
;
queued
()
{
}
return
timeIntervalInWords
(
this
.
job
.
queued
);
},
runnerId
()
{
return
`#
${
this
.
job
.
runner
.
id
}
`
;
},
retryButtonClass
()
{
let
className
=
'
js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block
'
;
className
+=
this
.
job
.
status
&&
this
.
job
.
recoverable
?
'
btn-primary
'
:
'
btn-inverted-secondary
'
;
return
className
;
},
hasTimeout
()
{
return
this
.
job
.
metadata
!=
null
&&
this
.
job
.
metadata
.
timeout_human_readable
!==
null
;
},
timeout
()
{
if
(
this
.
job
.
metadata
==
null
)
{
return
''
;
}
let
t
=
this
.
job
.
metadata
.
timeout_human_readable
;
let
t
=
this
.
job
.
metadata
.
timeout_human_readable
;
if
(
this
.
job
.
metadata
.
timeout_source
!==
''
)
{
if
(
this
.
job
.
metadata
.
timeout_source
!==
''
)
{
t
+=
` (from
${
this
.
job
.
metadata
.
timeout_source
}
)`
;
t
+=
` (from
${
this
.
job
.
metadata
.
timeout_source
}
)`
;
}
}
return
t
;
return
t
;
},
renderBlock
()
{
return
this
.
job
.
merge_request
||
this
.
job
.
duration
||
this
.
job
.
finished_data
||
this
.
job
.
erased_at
||
this
.
job
.
queued
||
this
.
job
.
runner
||
this
.
job
.
coverage
||
this
.
job
.
tags
.
length
||
this
.
job
.
cancel_path
;
},
},
},
};
renderBlock
()
{
return
(
this
.
job
.
merge_request
||
this
.
job
.
duration
||
this
.
job
.
finished_data
||
this
.
job
.
erased_at
||
this
.
job
.
queued
||
this
.
job
.
runner
||
this
.
job
.
coverage
||
this
.
job
.
tags
.
length
||
this
.
job
.
cancel_path
);
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
<div>
<div>
<div
class=
"block"
>
<strong
class=
"inline prepend-top-8"
>
{{
job
.
name
}}
</strong>
<a
v-if=
"canUserRetry"
:class=
"retryButtonClass"
:href=
"job.retry_path"
data-method=
"post"
rel=
"nofollow"
>
{{
__
(
'
Retry
'
)
}}
</a>
<button
type=
"button"
:aria-label=
"__('Toggle Sidebar')"
class=
"btn btn-blank gutter-toggle pull-right
visible-xs-block visible-sm-block js-sidebar-build-toggle"
>
<i
aria-hidden=
"true"
data-hidden=
"true"
class=
"fa fa-angle-double-right"
></i>
</button>
</div>
<template
v-if=
"shouldRenderContent"
>
<template
v-if=
"shouldRenderContent"
>
<div
<div
class=
"block retry-link"
class=
"block retry-link"
...
@@ -85,16 +124,16 @@
...
@@ -85,16 +124,16 @@
class=
"js-new-issue btn btn-new btn-inverted"
class=
"js-new-issue btn btn-new btn-inverted"
:href=
"job.new_issue_path"
:href=
"job.new_issue_path"
>
>
New issue
{{
__
(
'
New issue
'
)
}}
</a>
</a>
<a
<a
v-if=
"
job.retry_path
"
v-if=
"
canUserRetry
"
class=
"js-retry-job btn btn-inverted-secondary"
class=
"js-retry-job btn btn-inverted-secondary"
:href=
"job.retry_path"
:href=
"job.retry_path"
data-method=
"post"
data-method=
"post"
rel=
"nofollow"
rel=
"nofollow"
>
>
Retry
{{
__
(
'
Retry
'
)
}}
</a>
</a>
</div>
</div>
<div
:class=
"
{block : renderBlock }">
<div
:class=
"
{block : renderBlock }">
...
@@ -103,7 +142,7 @@
...
@@ -103,7 +142,7 @@
v-if=
"job.merge_request"
v-if=
"job.merge_request"
>
>
<span
class=
"build-light-text"
>
<span
class=
"build-light-text"
>
Merge Request:
{{
__
(
'
Merge Request:
'
)
}}
</span>
</span>
<a
:href=
"job.merge_request.path"
>
<a
:href=
"job.merge_request.path"
>
!
{{
job
.
merge_request
.
iid
}}
!
{{
job
.
merge_request
.
iid
}}
...
@@ -158,7 +197,7 @@
...
@@ -158,7 +197,7 @@
v-if=
"job.tags.length"
v-if=
"job.tags.length"
>
>
<span
class=
"build-light-text"
>
<span
class=
"build-light-text"
>
Tags:
{{
__
(
'
Tags:
'
)
}}
</span>
</span>
<span
<span
v-for=
"(tag, i) in job.tags"
v-for=
"(tag, i) in job.tags"
...
@@ -178,7 +217,7 @@
...
@@ -178,7 +217,7 @@
data-method=
"post"
data-method=
"post"
rel=
"nofollow"
rel=
"nofollow"
>
>
Cancel
{{
__
(
'
Cancel
'
)
}}
</a>
</a>
</div>
</div>
</div>
</div>
...
...
app/assets/javascripts/jobs/job_details_bundle.js
View file @
93780da6
...
@@ -35,9 +35,11 @@ export default () => {
...
@@ -35,9 +35,11 @@ export default () => {
});
});
// Sidebar information block
// Sidebar information block
const
detailsBlockElement
=
document
.
getElementById
(
'
js-details-block-vue
'
);
const
detailsBlockDataset
=
detailsBlockElement
.
dataset
;
// eslint-disable-next-line
// eslint-disable-next-line
new
Vue
({
new
Vue
({
el
:
'
#js-details-block-vue
'
,
el
:
detailsBlockElement
,
components
:
{
components
:
{
detailsBlock
,
detailsBlock
,
},
},
...
@@ -50,6 +52,7 @@ export default () => {
...
@@ -50,6 +52,7 @@ export default () => {
return
createElement
(
'
details-block
'
,
{
return
createElement
(
'
details-block
'
,
{
props
:
{
props
:
{
isLoading
:
this
.
mediator
.
state
.
isLoading
,
isLoading
:
this
.
mediator
.
state
.
isLoading
,
canUserRetry
:
!!
(
'
canUserRetry
'
in
detailsBlockDataset
),
job
:
this
.
mediator
.
store
.
state
.
job
,
job
:
this
.
mediator
.
store
.
state
.
job
,
runnerHelpUrl
:
dataset
.
runnerHelpUrl
,
runnerHelpUrl
:
dataset
.
runnerHelpUrl
,
},
},
...
...
app/assets/javascripts/vue_shared/components/callout.vue
0 → 100644
View file @
93780da6
<
script
>
const
calloutVariants
=
[
'
danger
'
,
'
success
'
,
'
info
'
,
'
warning
'
];
export
default
{
props
:
{
category
:
{
type
:
String
,
required
:
false
,
default
:
calloutVariants
[
0
],
validator
:
value
=>
calloutVariants
.
includes
(
value
),
},
message
:
{
type
:
String
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<div
:class=
"`bs-callout bs-callout-$
{category}`"
role="alert"
aria-live="assertive"
>
{{
message
}}
</div>
</
template
>
app/assets/stylesheets/pages/builds.scss
View file @
93780da6
@keyframes
fade-out-status
{
@keyframes
fade-out-status
{
0
%
,
50
%
{
opacity
:
1
;
}
0
%
,
100
%
{
opacity
:
0
;
}
50
%
{
opacity
:
1
;
}
100
%
{
opacity
:
0
;
}
}
}
@keyframes
blinking-dots
{
@keyframes
blinking-dots
{
0
%
{
0
%
{
background-color
:
rgba
(
$white-light
,
1
);
background-color
:
rgba
(
$white-light
,
1
);
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
0
.2
)
,
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
0
.2
)
,
24px
0
0
0
rgba
(
$white-light
,
0
.2
);
24px
0
0
0
rgba
(
$white-light
,
0
.2
);
}
}
25
%
{
25
%
{
background-color
:
rgba
(
$white-light
,
0
.4
);
background-color
:
rgba
(
$white-light
,
0
.4
);
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
2
)
,
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
2
)
,
24px
0
0
0
rgba
(
$white-light
,
0
.2
);
24px
0
0
0
rgba
(
$white-light
,
0
.2
);
}
}
75
%
{
75
%
{
background-color
:
rgba
(
$white-light
,
0
.4
);
background-color
:
rgba
(
$white-light
,
0
.4
);
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
0
.2
)
,
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
0
.2
)
,
24px
0
0
0
rgba
(
$white-light
,
1
);
24px
0
0
0
rgba
(
$white-light
,
1
);
}
}
100
%
{
100
%
{
background-color
:
rgba
(
$white-light
,
1
);
background-color
:
rgba
(
$white-light
,
1
);
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
0
.2
)
,
box-shadow
:
12px
0
0
0
rgba
(
$white-light
,
0
.2
)
,
24px
0
0
0
rgba
(
$white-light
,
0
.2
);
24px
0
0
0
rgba
(
$white-light
,
0
.2
);
}
}
}
}
@keyframes
blinking-scroll-button
{
@keyframes
blinking-scroll-button
{
0
%
{
opacity
:
0
.2
;
}
0
%
{
25
%
{
opacity
:
0
.5
;
}
opacity
:
0
.2
;
50
%
{
opacity
:
0
.7
;
}
}
100
%
{
opacity
:
1
;
}
25
%
{
opacity
:
0
.5
;
}
50
%
{
opacity
:
0
.7
;
}
100
%
{
opacity
:
1
;
}
}
}
.build-page
{
.build-page
{
...
@@ -125,12 +142,12 @@
...
@@ -125,12 +142,12 @@
.btn-scroll.animate
{
.btn-scroll.animate
{
.first-triangle
{
.first-triangle
{
animation
:
blinking-scroll-button
1s
ease
infinite
;
animation
:
blinking-scroll-button
1s
ease
infinite
;
animation-delay
:
.3s
;
animation-delay
:
0
.3s
;
}
}
.second-triangle
{
.second-triangle
{
animation
:
blinking-scroll-button
1s
ease
infinite
;
animation
:
blinking-scroll-button
1s
ease
infinite
;
animation-delay
:
.2s
;
animation-delay
:
0
.2s
;
}
}
.third-triangle
{
.third-triangle
{
...
...
app/controllers/projects/jobs_controller.rb
View file @
93780da6
...
@@ -78,6 +78,8 @@ class Projects::JobsController < Projects::ApplicationController
...
@@ -78,6 +78,8 @@ class Projects::JobsController < Projects::ApplicationController
result
.
merge!
(
trace
.
to_h
)
result
.
merge!
(
trace
.
to_h
)
end
end
result
[
:html
]
=
result
[
:html
].
presence
||
'No job log'
render
json:
result
render
json:
result
end
end
end
end
...
...
app/presenters/ci/build_presenter.rb
View file @
93780da6
module
Ci
module
Ci
class
BuildPresenter
<
Gitlab
::
View
::
Presenter
::
Delegated
class
BuildPresenter
<
Gitlab
::
View
::
Presenter
::
Delegated
CALLOUT_FAILURE_MESSAGES
=
{
unknown_failure:
'There is an unknown failure, please try again'
,
script_failure:
'There has been a script failure. Check the job log for more information'
,
api_failure:
'There has been an API failure, please try again'
,
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, check the job log for more information'
}.
freeze
presents
:build
presents
:build
def
erased_by_user?
def
erased_by_user?
...
@@ -35,6 +44,14 @@ module Ci
...
@@ -35,6 +44,14 @@ module Ci
"
#{
subject
.
name
}
-
#{
detailed_status
.
status_tooltip
}
"
"
#{
subject
.
name
}
-
#{
detailed_status
.
status_tooltip
}
"
end
end
def
callout_failure_message
CALLOUT_FAILURE_MESSAGES
[
failure_reason
.
to_sym
]
end
def
recoverable?
failed?
&&
!
unrecoverable?
end
private
private
def
tooltip_for_badge
def
tooltip_for_badge
...
@@ -44,5 +61,9 @@ module Ci
...
@@ -44,5 +61,9 @@ module Ci
def
detailed_status
def
detailed_status
@detailed_status
||=
subject
.
detailed_status
(
user
)
@detailed_status
||=
subject
.
detailed_status
(
user
)
end
end
def
unrecoverable?
script_failure?
||
missing_dependency_failure?
end
end
end
end
end
app/serializers/job_entity.rb
View file @
93780da6
...
@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
...
@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
expose
:created_at
expose
:created_at
expose
:updated_at
expose
:updated_at
expose
:detailed_status
,
as: :status
,
with:
StatusEntity
expose
:detailed_status
,
as: :status
,
with:
StatusEntity
expose
:callout_message
,
if:
->
(
*
)
{
failed?
}
expose
:recoverable
,
if:
->
(
*
)
{
failed?
}
private
private
...
@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
...
@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
def
path_to
(
route
,
build
)
def
path_to
(
route
,
build
)
send
(
"
#{
route
}
_path"
,
build
.
project
.
namespace
,
build
.
project
,
build
)
# rubocop:disable GitlabSecurity/PublicSend
send
(
"
#{
route
}
_path"
,
build
.
project
.
namespace
,
build
.
project
,
build
)
# rubocop:disable GitlabSecurity/PublicSend
end
end
def
failed?
build
.
failed?
end
def
callout_message
build_presenter
.
callout_failure_message
end
def
recoverable
build_presenter
.
recoverable?
end
def
build_presenter
@build_presenter
||=
build
.
present
end
end
end
app/views/projects/jobs/_sidebar.html.haml
View file @
93780da6
%aside
.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar
{
data:
{
"offset-top"
=>
"101"
,
"spy"
=>
"affix"
}
}
%aside
.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar
{
data:
{
"offset-top"
=>
"101"
,
"spy"
=>
"affix"
}
}
.sidebar-container
.sidebar-container
.blocks-container
.blocks-container
.block
%strong
.inline.prepend-top-8
=
@build
.
name
-
if
can?
(
current_user
,
:update_build
,
@build
)
&&
@build
.
retryable?
=
link_to
"Retry"
,
retry_namespace_project_job_path
(
@project
.
namespace
,
@project
,
@build
),
class:
'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block'
,
method: :post
%a
.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle
{
href:
"#"
,
'aria-label'
:
'Toggle Sidebar'
,
role:
'button'
}
=
icon
(
'angle-double-right'
)
#js-details-block-vue
#js-details-block-vue
{
data:
{
can_user_retry:
can?
(
current_user
,
:update_build
,
@build
)
&&
@build
.
retryable?
}
}
-
if
can?
(
current_user
,
:read_build
,
@project
)
&&
(
@build
.
artifacts?
||
@build
.
artifacts_expired?
)
-
if
can?
(
current_user
,
:read_build
,
@project
)
&&
(
@build
.
artifacts?
||
@build
.
artifacts_expired?
)
.block
.block
...
...
doc/ci/pipelines.md
View file @
93780da6
...
@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace.
...
@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace.
## Seeing the failure reason for jobs
## Seeing the failure reason for jobs
> [Introduced][ce-
574
2] in GitLab 10.7.
> [Introduced][ce-
1778
2] in GitLab 10.7.
When a pipeline fails or is allowed to fail, there are several places where you
When a pipeline fails or is allowed to fail, there are several places where you
can quickly check the reason it failed:
can quickly check the reason it failed:
...
@@ -88,6 +88,8 @@ In any case, if you hover over the failed job you can see the reason it failed.
...
@@ -88,6 +88,8 @@ In any case, if you hover over the failed job you can see the reason it failed.
![
Pipeline detail
](
img/job_failure_reason.png
)
![
Pipeline detail
](
img/job_failure_reason.png
)
From
[
GitLab 10.8
][
ce-17814
]
you can also see the reason it failed on the Job detail page.
## Pipeline graphs
## Pipeline graphs
> [Introduced][ce-5742] in GitLab 8.11.
> [Introduced][ce-5742] in GitLab 8.11.
...
@@ -279,4 +281,5 @@ runners will not use regular runners, they must be tagged accordingly.
...
@@ -279,4 +281,5 @@ runners will not use regular runners, they must be tagged accordingly.
[
ce-7931
]:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[
ce-7931
]:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[
ce-9760
]:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
[
ce-9760
]:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
[
ce-17782
]:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
[
ce-17782
]:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
[
ce-17814
]:
https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814
[
regexp
]:
https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
[
regexp
]:
https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
lib/gitlab/view/presenter/base.rb
View file @
93780da6
...
@@ -20,6 +20,10 @@ module Gitlab
...
@@ -20,6 +20,10 @@ module Gitlab
subject
subject
end
end
def
present
(
**
attributes
)
self
end
class_methods
do
class_methods
do
def
presenter?
def
presenter?
true
true
...
...
spec/controllers/projects/jobs_controller_spec.rb
View file @
93780da6
...
@@ -190,7 +190,10 @@ describe Projects::JobsController do
...
@@ -190,7 +190,10 @@ describe Projects::JobsController do
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
expect
(
json_response
[
'id'
]).
to
eq
job
.
id
expect
(
json_response
[
'id'
]).
to
eq
job
.
id
expect
(
json_response
[
'status'
]).
to
eq
job
.
status
expect
(
json_response
[
'status'
]).
to
eq
job
.
status
expect
(
json_response
[
'html'
]).
to
be_nil
end
it
'returns no job log message'
do
expect
(
json_response
[
'html'
]).
to
eq
(
'No job log'
)
end
end
end
end
...
...
spec/factories/ci/builds.rb
View file @
93780da6
...
@@ -243,5 +243,10 @@ FactoryBot.define do
...
@@ -243,5 +243,10 @@ FactoryBot.define do
failed
failed
failure_reason
1
failure_reason
1
end
end
trait
:api_failure
do
failed
failure_reason
2
end
end
end
end
end
spec/features/projects/jobs_spec.rb
View file @
93780da6
...
@@ -491,16 +491,18 @@ feature 'Jobs' do
...
@@ -491,16 +491,18 @@ feature 'Jobs' do
end
end
end
end
describe
"POST /:project/jobs/:id/retry"
do
describe
"POST /:project/jobs/:id/retry"
,
:js
do
context
"Job from project"
,
:js
do
context
"Job from project"
,
:js
do
before
do
before
do
job
.
run!
job
.
run!
job
.
cancel!
visit
project_job_path
(
project
,
job
)
visit
project_job_path
(
project
,
job
)
find
(
'.js-cancel-job'
).
click
()
wait_for_requests
find
(
'.js-retry-button'
).
click
find
(
'.js-retry-button'
).
click
end
end
it
'shows the right status and buttons'
,
:js
do
it
'shows the right status and buttons'
do
page
.
within
(
'aside.right-sidebar'
)
do
page
.
within
(
'aside.right-sidebar'
)
do
expect
(
page
).
to
have_content
'Cancel'
expect
(
page
).
to
have_content
'Cancel'
end
end
...
...
spec/javascripts/jobs/header_spec.js
View file @
93780da6
...
@@ -36,14 +36,28 @@ describe('Job details header', () => {
...
@@ -36,14 +36,28 @@ describe('Job details header', () => {
},
},
isLoading
:
false
,
isLoading
:
false
,
};
};
vm
=
mountComponent
(
HeaderComponent
,
props
);
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
vm
.
$destroy
();
vm
.
$destroy
();
});
});
describe
(
'
job reason
'
,
()
=>
{
it
(
'
should not render the reason when reason is absent
'
,
()
=>
{
vm
=
mountComponent
(
HeaderComponent
,
props
);
expect
(
vm
.
shouldRenderReason
).
toBe
(
false
);
});
it
(
'
should render the reason when reason is present
'
,
()
=>
{
props
.
job
.
callout_message
=
'
There is an unknown failure, please try again
'
;
vm
=
mountComponent
(
HeaderComponent
,
props
);
expect
(
vm
.
shouldRenderReason
).
toBe
(
true
);
});
});
describe
(
'
triggered job
'
,
()
=>
{
describe
(
'
triggered job
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
vm
=
mountComponent
(
HeaderComponent
,
props
);
vm
=
mountComponent
(
HeaderComponent
,
props
);
...
@@ -51,14 +65,17 @@ describe('Job details header', () => {
...
@@ -51,14 +65,17 @@ describe('Job details header', () => {
it
(
'
should render provided job information
'
,
()
=>
{
it
(
'
should render provided job information
'
,
()
=>
{
expect
(
expect
(
vm
.
$el
.
querySelector
(
'
.header-main-content
'
).
textContent
.
replace
(
/
\s
+/g
,
'
'
).
trim
(),
vm
.
$el
.
querySelector
(
'
.header-main-content
'
)
.
textContent
.
replace
(
/
\s
+/g
,
'
'
)
.
trim
(),
).
toEqual
(
'
failed Job #123 triggered 3 weeks ago by Foo
'
);
).
toEqual
(
'
failed Job #123 triggered 3 weeks ago by Foo
'
);
});
});
it
(
'
should render new issue link
'
,
()
=>
{
it
(
'
should render new issue link
'
,
()
=>
{
expect
(
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-issue
'
).
getAttribute
(
'
href
'
)).
toEqual
(
vm
.
$el
.
querySelector
(
'
.js-new-issue
'
).
getAttribute
(
'
href
'
)
,
props
.
job
.
new_issue_path
,
)
.
toEqual
(
props
.
job
.
new_issue_path
)
;
);
});
});
});
});
...
@@ -68,7 +85,10 @@ describe('Job details header', () => {
...
@@ -68,7 +85,10 @@ describe('Job details header', () => {
vm
=
mountComponent
(
HeaderComponent
,
props
);
vm
=
mountComponent
(
HeaderComponent
,
props
);
expect
(
expect
(
vm
.
$el
.
querySelector
(
'
.header-main-content
'
).
textContent
.
replace
(
/
\s
+/g
,
'
'
).
trim
(),
vm
.
$el
.
querySelector
(
'
.header-main-content
'
)
.
textContent
.
replace
(
/
\s
+/g
,
'
'
)
.
trim
(),
).
toEqual
(
'
failed Job #123 created 3 weeks ago by Foo
'
);
).
toEqual
(
'
failed Job #123 created 3 weeks ago by Foo
'
);
});
});
});
});
...
...
spec/javascripts/jobs/sidebar_details_block_spec.js
View file @
93780da6
...
@@ -31,10 +31,25 @@ describe('Sidebar details block', () => {
...
@@ -31,10 +31,25 @@ describe('Sidebar details block', () => {
});
});
});
});
describe
(
"
when user can't retry
"
,
()
=>
{
it
(
'
should not render a retry button
'
,
()
=>
{
vm
=
new
SidebarComponent
({
propsData
:
{
job
:
{},
canUserRetry
:
false
,
isLoading
:
true
,
},
}).
$mount
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-retry-job
'
)).
toBeNull
();
});
});
beforeEach
(()
=>
{
beforeEach
(()
=>
{
vm
=
new
SidebarComponent
({
vm
=
new
SidebarComponent
({
propsData
:
{
propsData
:
{
job
,
job
,
canUserRetry
:
true
,
isLoading
:
false
,
isLoading
:
false
,
},
},
}).
$mount
();
}).
$mount
();
...
@@ -42,7 +57,9 @@ describe('Sidebar details block', () => {
...
@@ -42,7 +57,9 @@ describe('Sidebar details block', () => {
describe
(
'
actions
'
,
()
=>
{
describe
(
'
actions
'
,
()
=>
{
it
(
'
should render link to new issue
'
,
()
=>
{
it
(
'
should render link to new issue
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-issue
'
).
getAttribute
(
'
href
'
)).
toEqual
(
job
.
new_issue_path
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-issue
'
).
getAttribute
(
'
href
'
)).
toEqual
(
job
.
new_issue_path
,
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-issue
'
).
textContent
.
trim
()).
toEqual
(
'
New issue
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-issue
'
).
textContent
.
trim
()).
toEqual
(
'
New issue
'
);
});
});
...
@@ -57,43 +74,35 @@ describe('Sidebar details block', () => {
...
@@ -57,43 +74,35 @@ describe('Sidebar details block', () => {
describe
(
'
information
'
,
()
=>
{
describe
(
'
information
'
,
()
=>
{
it
(
'
should render merge request link
'
,
()
=>
{
it
(
'
should render merge request link
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-mr
'
))).
toEqual
(
'
Merge Request: !2
'
);
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-mr
'
)),
).
toEqual
(
'
Merge Request: !2
'
);
expect
(
expect
(
vm
.
$el
.
querySelector
(
'
.js-job-mr a
'
).
getAttribute
(
'
href
'
)).
toEqual
(
vm
.
$el
.
querySelector
(
'
.js-job-mr a
'
).
getAttribute
(
'
href
'
)
,
job
.
merge_request
.
path
,
)
.
toEqual
(
job
.
merge_request
.
path
)
;
);
});
});
it
(
'
should render job duration
'
,
()
=>
{
it
(
'
should render job duration
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-duration
'
))).
toEqual
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-duration
'
))
,
'
Duration: 6 seconds
'
,
)
.
toEqual
(
'
Duration: 6 seconds
'
)
;
);
});
});
it
(
'
should render erased date
'
,
()
=>
{
it
(
'
should render erased date
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-erased
'
))).
toEqual
(
'
Erased: 3 weeks ago
'
);
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-erased
'
)),
).
toEqual
(
'
Erased: 3 weeks ago
'
);
});
});
it
(
'
should render finished date
'
,
()
=>
{
it
(
'
should render finished date
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-finished
'
))).
toEqual
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-finished
'
))
,
'
Finished: 3 weeks ago
'
,
)
.
toEqual
(
'
Finished: 3 weeks ago
'
)
;
);
});
});
it
(
'
should render queued date
'
,
()
=>
{
it
(
'
should render queued date
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-queued
'
))).
toEqual
(
'
Queued: 9 seconds
'
);
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-queued
'
)),
).
toEqual
(
'
Queued: 9 seconds
'
);
});
});
it
(
'
should render runner ID
'
,
()
=>
{
it
(
'
should render runner ID
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-runner
'
))).
toEqual
(
'
Runner: #1
'
);
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-runner
'
)),
).
toEqual
(
'
Runner: #1
'
);
});
});
it
(
'
should render timeout information
'
,
()
=>
{
it
(
'
should render timeout information
'
,
()
=>
{
...
@@ -103,15 +112,11 @@ describe('Sidebar details block', () => {
...
@@ -103,15 +112,11 @@ describe('Sidebar details block', () => {
});
});
it
(
'
should render coverage
'
,
()
=>
{
it
(
'
should render coverage
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-coverage
'
))).
toEqual
(
'
Coverage: 20%
'
);
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-coverage
'
)),
).
toEqual
(
'
Coverage: 20%
'
);
});
});
it
(
'
should render tags
'
,
()
=>
{
it
(
'
should render tags
'
,
()
=>
{
expect
(
expect
(
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-tags
'
))).
toEqual
(
'
Tags: tag
'
);
trimWhitespace
(
vm
.
$el
.
querySelector
(
'
.js-job-tags
'
)),
).
toEqual
(
'
Tags: tag
'
);
});
});
});
});
});
});
spec/javascripts/vue_shared/components/callout_spec.js
0 → 100644
View file @
93780da6
import
Vue
from
'
vue
'
;
import
callout
from
'
~/vue_shared/components/callout.vue
'
;
import
createComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
describe
(
'
Callout Component
'
,
()
=>
{
let
CalloutComponent
;
let
vm
;
const
exampleMessage
=
'
This is a callout message!
'
;
beforeEach
(()
=>
{
CalloutComponent
=
Vue
.
extend
(
callout
);
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
should render the appropriate variant of callout
'
,
()
=>
{
vm
=
createComponent
(
CalloutComponent
,
{
category
:
'
info
'
,
message
:
exampleMessage
,
});
expect
(
vm
.
$el
.
getAttribute
(
'
class
'
)).
toEqual
(
'
bs-callout bs-callout-info
'
);
expect
(
vm
.
$el
.
tagName
).
toEqual
(
'
DIV
'
);
});
it
(
'
should render accessibility attributes
'
,
()
=>
{
vm
=
createComponent
(
CalloutComponent
,
{
message
:
exampleMessage
,
});
expect
(
vm
.
$el
.
getAttribute
(
'
role
'
)).
toEqual
(
'
alert
'
);
expect
(
vm
.
$el
.
getAttribute
(
'
aria-live
'
)).
toEqual
(
'
assertive
'
);
});
it
(
'
should render the provided message
'
,
()
=>
{
vm
=
createComponent
(
CalloutComponent
,
{
message
:
exampleMessage
,
});
expect
(
vm
.
$el
.
innerHTML
.
trim
()).
toEqual
(
exampleMessage
);
});
});
spec/lib/gitlab/view/presenter/base_spec.rb
View file @
93780da6
...
@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do
...
@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do
end
end
end
end
end
end
describe
'#present'
do
it
'returns self'
do
presenter
=
presenter_class
.
new
(
build_stubbed
(
:project
))
expect
(
presenter
.
present
).
to
eq
(
presenter
)
end
end
end
end
spec/presenters/ci/build_presenter_spec.rb
View file @
93780da6
...
@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do
...
@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do
end
end
end
end
end
end
describe
'#callout_failure_message'
do
let
(
:build
)
{
create
(
:ci_build
,
:failed
,
:script_failure
)
}
it
'returns a verbose failure reason'
do
description
=
subject
.
callout_failure_message
expect
(
description
).
to
eq
(
'There has been a script failure. Check the job log for more information'
)
end
end
describe
'#recoverable?'
do
let
(
:build
)
{
create
(
:ci_build
,
:failed
,
:script_failure
)
}
context
'when is a script or missing dependency failure'
do
let
(
:failure_reasons
)
{
%w(script_failure missing_dependency_failure)
}
it
'should return false'
do
failure_reasons
.
each
do
|
failure_reason
|
build
.
update_attribute
(
:failure_reason
,
failure_reason
)
expect
(
presenter
.
recoverable?
).
to
be_falsy
end
end
end
context
'when is any other failure type'
do
let
(
:failure_reasons
)
{
%w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure)
}
it
'should return true'
do
failure_reasons
.
each
do
|
failure_reason
|
build
.
update_attribute
(
:failure_reason
,
failure_reason
)
expect
(
presenter
.
recoverable?
).
to
be_truthy
end
end
end
end
end
end
spec/serializers/job_entity_spec.rb
View file @
93780da6
...
@@ -133,22 +133,65 @@ describe JobEntity do
...
@@ -133,22 +133,65 @@ describe JobEntity do
context
'when job failed'
do
context
'when job failed'
do
let
(
:job
)
{
create
(
:ci_build
,
:script_failure
)
}
let
(
:job
)
{
create
(
:ci_build
,
:script_failure
)
}
describe
'status'
do
it
'contains details'
do
it
'should contain the failure reason inside label'
do
expect
(
subject
[
:status
]).
to
include
:icon
,
:favicon
,
:text
,
:label
,
:tooltip
expect
(
subject
[
:status
]).
to
include
:icon
,
:favicon
,
:text
,
:label
,
:tooltip
end
expect
(
subject
[
:status
][
:label
]).
to
eq
(
'failed'
)
expect
(
subject
[
:status
][
:tooltip
]).
to
eq
(
'failed <br> (script failure)'
)
it
'states that it failed'
do
end
expect
(
subject
[
:status
][
:label
]).
to
eq
(
'failed'
)
end
it
'should indicate the failure reason on tooltip'
do
expect
(
subject
[
:status
][
:tooltip
]).
to
eq
(
'failed <br> (script failure)'
)
end
it
'should include a callout message with a verbose output'
do
expect
(
subject
[
:callout_message
]).
to
eq
(
'There has been a script failure. Check the job log for more information'
)
end
it
'should state that it is not recoverable'
do
expect
(
subject
[
:recoverable
]).
to
be_falsy
end
end
context
'when job is allowed to fail'
do
let
(
:job
)
{
create
(
:ci_build
,
:allowed_to_fail
,
:script_failure
)
}
it
'contains details'
do
expect
(
subject
[
:status
]).
to
include
:icon
,
:favicon
,
:text
,
:label
,
:tooltip
end
it
'states that it failed'
do
expect
(
subject
[
:status
][
:label
]).
to
eq
(
'failed (allowed to fail)'
)
end
it
'should indicate the failure reason on tooltip'
do
expect
(
subject
[
:status
][
:tooltip
]).
to
eq
(
'failed <br> (script failure) (allowed to fail)'
)
end
it
'should include a callout message with a verbose output'
do
expect
(
subject
[
:callout_message
]).
to
eq
(
'There has been a script failure. Check the job log for more information'
)
end
it
'should state that it is not recoverable'
do
expect
(
subject
[
:recoverable
]).
to
be_falsy
end
end
context
'when job failed and is recoverable'
do
let
(
:job
)
{
create
(
:ci_build
,
:api_failure
)
}
it
'should state it is recoverable'
do
expect
(
subject
[
:recoverable
]).
to
be_truthy
end
end
end
end
context
'when job passed'
do
context
'when job passed'
do
let
(
:job
)
{
create
(
:ci_build
,
:success
)
}
let
(
:job
)
{
create
(
:ci_build
,
:success
)
}
describe
'status'
do
it
'should not include callout message or recoverable keys'
do
it
'should not contain the failure reason inside label'
do
expect
(
subject
).
not_to
include
(
'callout_message'
)
expect
(
subject
[
:status
][
:label
]).
to
eq
(
'passed'
)
expect
(
subject
).
not_to
include
(
'recoverable'
)
end
end
end
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment