summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml4
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss265
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/framework/variables.scss39
-rw-r--r--app/assets/stylesheets/new_nav.scss119
-rw-r--r--app/assets/stylesheets/new_sidebar.scss7
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss64
-rw-r--r--app/controllers/admin/users_controller.rb1
-rw-r--r--app/controllers/ci/lints_controller.rb4
-rw-r--r--app/controllers/profiles/preferences_controller.rb3
-rw-r--r--app/controllers/projects/pipelines_controller.rb8
-rw-r--r--app/helpers/auto_devops_helper.rb3
-rw-r--r--app/helpers/builds_helper.rb2
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/models/blob_viewer/gitlab_ci_yml.rb2
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/pipeline_schedule.rb2
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/ci/runner_project.rb2
-rw-r--r--app/models/ci/stage.rb2
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/event.rb32
-rw-r--r--app/models/project_auto_devops.rb3
-rw-r--r--app/models/user.rb1
-rw-r--r--app/serializers/build_details_entity.rb4
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml20
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml8
-rw-r--r--app/views/projects/runners/_form.html.haml2
-rw-r--r--changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml5
-rw-r--r--changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml5
-rw-r--r--changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml5
-rw-r--r--changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml5
-rw-r--r--changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml5
-rw-r--r--changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml5
-rw-r--r--changelogs/unreleased/zj-feature-flipper-disable-banner.yml5
-rw-r--r--changelogs/unreleased/zj-usage-data-auto-devops.yml5
-rw-r--r--config/gitlab.yml.example7
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--db/migrate/20170816234252_add_theme_id_to_users.rb10
-rw-r--r--db/migrate/20170828135939_migrate_user_external_mail_data.rb57
-rw-r--r--db/schema.rb1
-rw-r--r--doc/api/keys.md1
-rw-r--r--doc/api/session.md1
-rw-r--r--doc/api/users.md5
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/emails.md23
-rw-r--r--doc/topics/autodevops/index.md17
-rw-r--r--doc/topics/autodevops/quick_start_guide.md80
-rw-r--r--doc/update/mysql_to_postgresql.md2
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/ci/ansi2html.rb331
-rw-r--r--lib/ci/assets/.gitkeep0
-rw-r--r--lib/ci/charts.rb116
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb251
-rw-r--r--lib/ci/mask_secret.rb10
-rw-r--r--lib/ci/model.rb11
-rw-r--r--lib/gitlab/ci/ansi2html.rb333
-rw-r--r--lib/gitlab/ci/charts.rb118
-rw-r--r--lib/gitlab/ci/mask_secret.rb12
-rw-r--r--lib/gitlab/ci/model.rb13
-rw-r--r--lib/gitlab/ci/trace/stream.rb4
-rw-r--r--lib/gitlab/ci/yaml_processor.rb253
-rw-r--r--lib/gitlab/conflict/parser.rb28
-rw-r--r--lib/gitlab/git/commit.rb2
-rw-r--r--lib/gitlab/git/commit_stats.rb19
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb8
-rw-r--r--lib/gitlab/mail_room.rb27
-rw-r--r--lib/gitlab/themes.rb84
-rw-r--r--lib/gitlab/usage_data.rb4
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb6
-rw-r--r--spec/features/projects/jobs_spec.rb6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json1
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb10
-rw-r--r--spec/helpers/preferences_helper_spec.rb30
-rw-r--r--spec/initializers/doorkeeper_spec.rb2
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb1697
-rw-r--r--spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb (renamed from spec/lib/ci/ansi2html_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/charts_spec.rb (renamed from spec/lib/ci/charts_spec.rb)4
-rw-r--r--spec/lib/gitlab/ci/mask_secret_spec.rb (renamed from spec/lib/ci/mask_secret_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb1699
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/themes_spec.rb48
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb4
-rw-r--r--spec/migrations/convert_custom_notification_settings_to_columns_spec.rb6
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/support/gitlab_stubs/session.json2
-rw-r--r--spec/support/gitlab_stubs/user.json4
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb4
101 files changed, 3401 insertions, 2684 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 4640681379a..dbeb1880d39 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -643,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
- Max: 14
+ Max: 13
# Limit lines to 80 characters.
Metrics/LineLength:
@@ -665,7 +665,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
- Max: 17
+ Max: 15
# Lint ########################################################################
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index c0524bf6aa3..35e7a10379f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -19,6 +19,7 @@
@import "framework/flash";
@import "framework/forms";
@import "framework/gfm";
+@import "framework/gitlab-theme";
@import "framework/header";
@import "framework/highlight";
@import "framework/issue_box";
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
new file mode 100644
index 00000000000..71f764923ff
--- /dev/null
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -0,0 +1,265 @@
+/**
+ * Styles the GitLab application with a specific color theme
+ */
+
+@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
+ // Header
+
+ header.navbar-gitlab-new {
+ background: linear-gradient(to right, $color-900, $color-800);
+
+ .navbar-collapse {
+ color: $color-200;
+ }
+
+ .container-fluid {
+ .navbar-toggle {
+ border-left: 1px solid lighten($color-700, 10%);
+ }
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ background-color: rgba($color-200, .2);
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $color-900;
+ background-color: $color-alternate;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ &.line-separator {
+ border-left: 1px solid rgba($color-200, .2);
+ }
+ }
+ }
+
+ .navbar-sub-nav {
+ color: $color-200;
+ }
+
+ .nav {
+ > li {
+ color: $color-200;
+
+ > a {
+ svg {
+ fill: $color-200;
+ }
+
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $color-200;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ @media (min-width: $screen-sm-min) {
+ background-color: rgba($color-200, .2);
+ }
+
+ svg {
+ fill: currentColor;
+ }
+ }
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $color-900;
+ background-color: $color-alternate;
+
+ &:hover {
+ svg {
+ fill: $color-900;
+ }
+ }
+ }
+
+ .impersonated-user,
+ .impersonated-user:hover {
+ svg {
+ fill: $color-900;
+ }
+ }
+ }
+ }
+ }
+
+ .title {
+ > a {
+ &:hover,
+ &:focus {
+ background-color: rgba($color-200, .2);
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: rgba($color-200, .2);
+
+ &:hover {
+ background-color: rgba($color-200, .3);
+ }
+ }
+
+ .location-badge {
+ color: $color-100;
+ background-color: rgba($color-200, .1);
+ border-right: 1px solid $color-800;
+ }
+
+ .search-input::placeholder {
+ color: rgba($color-200, .8);
+ }
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ color: rgba($color-200, .8);
+ }
+ }
+
+ &.search-active {
+ form {
+ background-color: $white-light;
+ }
+
+ .location-badge {
+ color: $gl-text-color;
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ color: rgba($color-200, .8);
+ }
+ }
+ }
+ }
+
+ .btn-sign-in {
+ background-color: $color-100;
+ color: $color-900;
+ }
+
+
+ // Sidebar
+ .nav-sidebar li.active {
+ box-shadow: inset 4px 0 0 $color-700;
+
+ > a {
+ color: $color-900;
+ }
+
+ svg {
+ fill: $color-900;
+ }
+ }
+}
+
+
+body {
+ &.ui_indigo {
+ @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
+ }
+
+ &.ui_dark {
+ @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
+ }
+
+ &.ui_blue {
+ @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
+ }
+
+ &.ui_green {
+ @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
+ }
+
+ &.ui_light {
+ @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
+
+ header.navbar-gitlab-new {
+ background: $theme-gray-100;
+ box-shadow: 0 2px 0 0 $border-color;
+
+ .logo-text svg {
+ fill: $theme-gray-900;
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ color: $theme-gray-900;
+ }
+
+ &.active > a {
+ color: $white-light;
+
+ &:hover {
+ color: $white-light;
+ }
+ }
+ }
+ }
+
+ .container-fluid {
+ .navbar-toggle,
+ .navbar-toggle:hover {
+ color: $theme-gray-700;
+ border-left: 1px solid $theme-gray-200;
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: $white-light;
+ box-shadow: inset 0 0 0 1px $border-color;
+
+ &:hover {
+ background-color: $white-light;
+ box-shadow: inset 0 0 0 1px $blue-100;
+
+ .location-badge {
+ box-shadow: inset 0 0 0 1px $blue-100;
+ }
+ }
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ color: $theme-gray-200;
+ }
+ }
+
+ .location-badge {
+ color: $theme-gray-700;
+ box-shadow: inset 0 0 0 1px $border-color;
+ background-color: $nav-badge-bg;
+ border-right: 0;
+ }
+ }
+
+ .nav-sidebar li.active {
+ > a {
+ color: $theme-gray-900;
+ }
+
+ svg {
+ fill: $theme-gray-900;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index b00a2d053e2..ab3c34df1fb 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -111,7 +111,6 @@ header {
svg {
height: 16px;
width: 23px;
- fill: currentColor;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e300b006026..3857226cddb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
+// GitLab themes
+
$indigo-50: #f7f7ff;
$indigo-100: #ebebfa;
$indigo-200: #d1d1f0;
@@ -86,6 +88,43 @@ $indigo-800: #393982;
$indigo-900: #292961;
$indigo-950: #1a1a40;
+$theme-gray-50: #fafafa;
+$theme-gray-100: #f2f2f2;
+$theme-gray-200: #dfdfdf;
+$theme-gray-300: #cccccc;
+$theme-gray-400: #bababa;
+$theme-gray-500: #a7a7a7;
+$theme-gray-600: #949494;
+$theme-gray-700: #707070;
+$theme-gray-800: #4f4f4f;
+$theme-gray-900: #2e2e2e;
+$theme-gray-950: #1f1f1f;
+
+$theme-blue-50: #f4f8fc;
+$theme-blue-100: #e6edf5;
+$theme-blue-200: #c8d7e6;
+$theme-blue-300: #97b3cf;
+$theme-blue-400: #648cb4;
+$theme-blue-500: #4a79a8;
+$theme-blue-600: #3e6fa0;
+$theme-blue-700: #305c88;
+$theme-blue-800: #25496e;
+$theme-blue-900: #1a3652;
+$theme-blue-950: #0f2235;
+
+$theme-green-50: #f2faf6;
+$theme-green-100: #e4f3ea;
+$theme-green-200: #c0dfcd;
+$theme-green-300: #8ac2a1;
+$theme-green-400: #52a274;
+$theme-green-500: #35935c;
+$theme-green-600: #288a50;
+$theme-green-700: #1c7441;
+$theme-green-800: #145d33;
+$theme-green-900: #0d4524;
+$theme-green-950: #072d16;
+
+
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index cd8fdd3f2ea..58e205537ef 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -9,10 +9,20 @@
header.navbar-gitlab-new {
color: $white-light;
- background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
min-height: $new-navbar-height;
+ .logo-text {
+ line-height: initial;
+
+ svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: $white-light;
+ }
+ }
+
.header-content {
display: -webkit-flex;
display: flex;
@@ -38,10 +48,10 @@ header.navbar-gitlab-new {
img {
height: 28px;
- margin-right: 10px;
+ margin-right: 8px;
}
- > a {
+ a {
display: -webkit-flex;
display: flex;
align-items: center;
@@ -54,22 +64,6 @@ header.navbar-gitlab-new {
margin-right: 8px;
}
}
-
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: $white-light;
- }
- }
-
- &:hover,
- &:focus {
- background-color: rgba($indigo-200, .2);
- }
}
}
@@ -106,7 +100,6 @@ header.navbar-gitlab-new {
.navbar-collapse {
padding-left: 0;
- color: $indigo-200;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
@@ -132,7 +125,6 @@ header.navbar-gitlab-new {
font-size: 14px;
text-align: center;
color: currentColor;
- border-left: 1px solid lighten($indigo-700, 10%);
&:hover,
&:focus,
@@ -167,63 +159,49 @@ header.navbar-gitlab-new {
will-change: color;
margin: 4px 2px;
padding: 6px 8px;
- color: $indigo-200;
height: 32px;
@media (max-width: $screen-xs-max) {
padding: 0;
}
- svg {
- fill: $indigo-200;
- }
-
&.header-user-dropdown-toggle {
margin-left: 2px;
.header-user-avatar {
- border-color: $indigo-200;
margin-right: 0;
}
}
- }
-
- .header-new-dropdown-toggle {
- margin-right: 0;
- }
- > a:hover,
- > a:focus {
- text-decoration: none;
- outline: 0;
- opacity: 1;
- color: $white-light;
-
- @media (min-width: $screen-sm-min) {
- background-color: rgba($indigo-200, .2);
- }
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ outline: 0;
+ opacity: 1;
+ color: $white-light;
- svg {
- fill: currentColor;
- }
+ svg {
+ fill: currentColor;
+ }
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $white-light;
+ }
}
}
}
+ .header-new-dropdown-toggle {
+ margin-right: 0;
+ }
+
.impersonated-user,
.impersonated-user:hover {
margin-right: 1px;
background-color: $white-light;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
-
- svg {
- fill: $indigo-900;
- }
}
.impersonation-btn,
@@ -241,8 +219,6 @@ header.navbar-gitlab-new {
&.active > a,
&.dropdown.open > a {
- color: $indigo-900;
- background-color: $white-light;
svg {
fill: currentColor;
@@ -256,7 +232,6 @@ header.navbar-gitlab-new {
display: -webkit-flex;
display: flex;
margin: 0 0 0 6px;
- color: $indigo-200;
.dropdown-chevron {
position: relative;
@@ -274,17 +249,6 @@ header.navbar-gitlab-new {
text-decoration: none;
outline: 0;
color: $white-light;
- background-color: rgba($indigo-200, .2);
-
- svg {
- fill: currentColor;
- }
- }
-
- &.active > a,
- &.dropdown.open > a {
- color: $indigo-900;
- background-color: $white-light;
svg {
fill: currentColor;
@@ -309,7 +273,6 @@ header.navbar-gitlab-new {
}
&.line-separator {
- border-left: 1px solid rgba($indigo-200, .2);
margin: 8px;
}
}
@@ -339,17 +302,14 @@ header.navbar-gitlab-new {
height: 32px;
border: 0;
border-radius: $border-radius-default;
- background-color: rgba($indigo-200, .2);
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
- background-color: rgba($indigo-200, .3);
box-shadow: none;
}
}
&.search-active form {
- background-color: $white-light;
box-shadow: none;
.search-input {
@@ -377,43 +337,26 @@ header.navbar-gitlab-new {
}
.search-input::placeholder {
- color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
- color: $indigo-100;
- background-color: rgba($indigo-200, .1);
- will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
- border-right: 1px solid $indigo-800;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
- .search-input-wrap {
- .search-icon,
- .clear-icon {
- color: rgba($indigo-200, .8);
- }
- }
-
&.search-active {
.location-badge {
- color: $gl-text-color;
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
- .search-icon {
- color: rgba($indigo-200, .8);
- }
-
.clear-icon {
color: $white-light;
}
@@ -518,8 +461,6 @@ header.navbar-gitlab-new {
.btn-sign-in {
margin-top: 3px;
- background-color: $indigo-100;
- color: $indigo-900;
font-weight: $gl-font-weight-bold;
&:hover {
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index 378ef8926d5..e4c12e46056 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -162,16 +162,9 @@ $new-sidebar-collapsed-width: 50px;
}
li.active {
- box-shadow: inset 4px 0 0 $active-border;
-
> a {
- color: $active-color;
font-weight: $gl-font-weight-bold;
}
-
- svg {
- fill: $active-color;
- }
}
@media (max-width: $screen-xs-max) {
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 305feaacaa1..c197494b152 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,3 +1,67 @@
+@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
+ .one {
+ background-color: $color-1;
+ border-top-left-radius: $border-radius-default;
+ }
+
+ .two {
+ background-color: $color-2;
+ border-top-right-radius: $border-radius-default;
+ }
+
+ .three {
+ background-color: $color-3;
+ border-bottom-left-radius: $border-radius-default;
+ }
+
+ .four {
+ background-color: $color-4;
+ border-bottom-right-radius: $border-radius-default;
+ }
+}
+
+.application-theme {
+ label {
+ margin-right: 20px;
+ text-align: center;
+ }
+
+ .preview {
+ font-size: 0;
+ margin-bottom: 10px;
+
+ &.indigo {
+ @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
+ }
+
+ &.dark {
+ @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
+ }
+
+ &.light {
+ @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
+ }
+
+ &.blue {
+ @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
+ }
+
+ &.green {
+ @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
+ }
+ }
+
+ .preview-row {
+ display: block;
+ }
+
+ .quadrant {
+ display: inline-block;
+ height: 50px;
+ width: 80px;
+ }
+}
+
.syntax-theme {
label {
margin-right: 20px;
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 9ec7719fabb..cbcef70e957 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
+ :theme_id,
:twitter,
:username,
:website_url
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index 3eb485de9db..be667687c18 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -7,11 +7,11 @@ module Ci
def create
@content = params[:content]
- @error = Ci::GitlabCiYamlProcessor.validation_message(@content)
+ @error = Gitlab::Ci::YamlProcessor.validation_message(@content)
@status = @error.blank?
if @error.blank?
- @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
+ @config_processor = Gitlab::Ci::YamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 1e557c47638..cce2a847b53 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id,
:layout,
:dashboard,
- :project_view
+ :project_view,
+ :theme_id
)
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index a3bfbf0694e..7ad7b3003af 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def charts
@charts = {}
- @charts[:week] = Ci::Charts::WeekChart.new(project)
- @charts[:month] = Ci::Charts::MonthChart.new(project)
- @charts[:year] = Ci::Charts::YearChart.new(project)
- @charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project)
+ @charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project)
+ @charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project)
+ @charts[:year] = Gitlab::Ci::Charts::YearChart.new(project)
+ @charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project)
@counts = {}
@counts[:total] = @project.pipelines.count(:all)
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 4ff38f86b5f..c132daed323 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -1,6 +1,7 @@
module AutoDevopsHelper
def show_auto_devops_callout?(project)
- show_callout?('auto_devops_settings_dismissed') &&
+ Feature.get(:auto_devops_banner_disabled).off? &&
+ show_callout?('auto_devops_settings_dismissed') &&
can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled?
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index 85bc784d53c..aa3a9a055a0 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -30,7 +30,7 @@ module BuildsHelper
def build_failed_issue_options
{
- title: "Build Failed ##{@build.id}",
+ title: "Job Failed ##{@build.id}",
description: project_job_url(@project, @build)
}
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index d36bb4ab074..0d7347ed30d 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -40,6 +40,10 @@ module PreferencesHelper
]
end
+ def user_application_theme
+ @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
+ end
+
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb
index 7267c3965d3..53bc247dec1 100644
--- a/app/models/blob_viewer/gitlab_ci_yml.rb
+++ b/app/models/blob_viewer/gitlab_ci_yml.rb
@@ -13,7 +13,7 @@ module BlobViewer
prepare!
- @validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data)
+ @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data)
end
def valid?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5ebe6f180e6..ee544d8ac56 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -446,8 +446,8 @@ module Ci
return unless trace
trace = trace.dup
- Ci::MaskSecret.mask!(trace, project.runners_token) if project
- Ci::MaskSecret.mask!(trace, token)
+ Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
+ Gitlab::Ci::MaskSecret.mask!(trace, token)
trace
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index f64bc245a67..afeae69ba39 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -1,6 +1,6 @@
module Ci
class GroupVariable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
include Presentable
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 871c76fbad3..476db384bbd 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,6 +1,6 @@
module Ci
class Pipeline < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasStatus
include Importable
include AfterCommitQueue
@@ -336,8 +336,8 @@ module Ci
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
- Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path)
- rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
+ Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path)
+ rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message
nil
rescue
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index e7e02587759..10ead6b6d3b 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -1,6 +1,6 @@
module Ci
class PipelineSchedule < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include Importable
acts_as_paranoid
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index ee5b8733fac..af989fb14b4 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -1,6 +1,6 @@
module Ci
class PipelineScheduleVariable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline_schedule
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 00b419c3efa..de5aae17a15 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -1,6 +1,6 @@
module Ci
class PipelineVariable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index b1798084787..a0d07902ba2 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,6 +1,6 @@
module Ci
class Runner < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 5f01a0daae9..505d178ba8e 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,6 +1,6 @@
module Ci
class RunnerProject < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
belongs_to :runner
belongs_to :project
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 754c37518b3..75b8ea2a371 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -1,6 +1,6 @@
module Ci
class Stage < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include Importable
include HasStatus
include Gitlab::OptimisticLocking
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 6df41a3f301..b5290bcaf53 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -1,6 +1,6 @@
module Ci
class Trigger < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
acts_as_paranoid
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 2c860598281..215b1cf6753 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -1,6 +1,6 @@
module Ci
class TriggerRequest < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index cf0fe04ddaf..67d3ec81b6f 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,6 +1,6 @@
module Ci
class Variable < ActiveRecord::Base
- extend Ci::Model
+ extend Gitlab::Ci::Model
include HasVariable
include Presentable
diff --git a/app/models/event.rb b/app/models/event.rb
index 8e9490b66f4..0b1f053a7e6 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -241,13 +241,7 @@ class Event < ActiveRecord::Base
def action_name
if push?
- if new_ref?
- "pushed new"
- elsif rm_ref?
- "deleted"
- else
- "pushed to"
- end
+ push_action_name
elsif closed?
"closed"
elsif merged?
@@ -263,11 +257,7 @@ class Event < ActiveRecord::Base
elsif commented?
"commented on"
elsif created_project?
- if project.external_import?
- "imported"
- else
- "created"
- end
+ created_project_action_name
else
"opened"
end
@@ -360,6 +350,24 @@ class Event < ActiveRecord::Base
private
+ def push_action_name
+ if new_ref?
+ "pushed new"
+ elsif rm_ref?
+ "deleted"
+ else
+ "pushed to"
+ end
+ end
+
+ def created_project_action_name
+ if project.external_import?
+ "imported"
+ else
+ "created"
+ end
+ end
+
def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 53731579e87..7af3b6870e2 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -1,6 +1,9 @@
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
+ scope :enabled, -> { where(enabled: true) }
+ scope :disabled, -> { where(enabled: false) }
+
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def variables
diff --git a/app/models/user.rb b/app/models/user.rb
index 358b04ac71f..09c9b3250eb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -35,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale
+ default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 743a08acefe..8c89eea607f 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity
private
def build_failed_issue_options
- { title: "Build Failed ##{build.id}",
- description: project_job_path(project, build) }
+ { title: "Job Failed ##{build.id}",
+ description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
end
def current_user
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 65ac8aaa59b..0ca34b276a7 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
+ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
= render "layouts/header/default"
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 352c2d66bab..69885008ecd 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -3,6 +3,26 @@
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
+ .col-lg-4.application-theme
+ %h4.prepend-top-0
+ GitLab navigation theme
+ %p Customize the appearance of the application header and navigation sidebar.
+ .col-lg-8.application-theme
+ - Gitlab::Themes.each do |theme|
+ = label_tag do
+ .preview{ class: theme.name.downcase }
+ .preview-row
+ .quadrant.one
+ .quadrant.two
+ .preview-row
+ .quadrant.three
+ .quadrant.four
+ = f.radio_button :theme_id, theme.id
+ = theme.name
+
+ .col-sm-12
+ %hr
+
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Syntax highlighting theme
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 431ab9d052b..8966dd3fd86 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -1,3 +1,7 @@
+// Remove body class for any previous theme, re-add current one
+$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
+$('body').addClass('<%= user_application_theme %>')
+
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
index aa004a739d7..01879556894 100644
--- a/app/views/projects/diffs/viewers/_image.html.haml
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path)
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
.swipe-wrap
.frame.added
- = image_tag(blob_raw_path, alt: diff_file.new_path)
+ = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
%span.swipe-bar
%span.top-handle
%span.bottom-handle
@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- = image_tag(old_blob_raw_path, alt: diff_file.old_path)
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
.frame.added
- = image_tag(blob_raw_path, alt: diff_file.new_path)
+ = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
.controls
.transparent
.drag-track
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index ac8e15a48b2..de85615c672 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -39,6 +39,6 @@
Tags
.col-sm-10
= f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
- .help-block You can setup jobs to only use Runners with specific tags
+ .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas.
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml
new file mode 100644
index 00000000000..6036e1a43a0
--- /dev/null
+++ b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml
@@ -0,0 +1,5 @@
+---
+title: Decrease Perceived Complexity threshold to 15
+merge_request: 14160
+author: Maxim Rydkin
+type: other
diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml
new file mode 100644
index 00000000000..a404456198a
--- /dev/null
+++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml
@@ -0,0 +1,5 @@
+---
+title: Decrease Cyclomatic Complexity threshold to 13
+merge_request: 14152
+author: Maxim Rydkin
+type: other
diff --git a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml
new file mode 100644
index 00000000000..74aa337a18c
--- /dev/null
+++ b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml
@@ -0,0 +1,5 @@
+---
+title: Add option in preferences to change navigation theme color
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml b/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml
new file mode 100644
index 00000000000..bec7da26b1a
--- /dev/null
+++ b/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add help text to runner edit: tags should be separated by commas.'
+merge_request:
+author: Brendan O'Leary
+type: added
diff --git a/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml b/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml
new file mode 100644
index 00000000000..7357c61b5f2
--- /dev/null
+++ b/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Image onion skin + swipe does not work anymore
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml b/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml
new file mode 100644
index 00000000000..44e16512bae
--- /dev/null
+++ b/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Move `lib/ci` to `lib/gitlab/ci`
+merge_request: 14078
+author: Maxim Rydkin
+type: other
diff --git a/changelogs/unreleased/zj-feature-flipper-disable-banner.yml b/changelogs/unreleased/zj-feature-flipper-disable-banner.yml
new file mode 100644
index 00000000000..fd5dd1bbe37
--- /dev/null
+++ b/changelogs/unreleased/zj-feature-flipper-disable-banner.yml
@@ -0,0 +1,5 @@
+---
+title: Allow all AutoDevOps banners to be turned off
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-usage-data-auto-devops.yml b/changelogs/unreleased/zj-usage-data-auto-devops.yml
new file mode 100644
index 00000000000..9b5ec894042
--- /dev/null
+++ b/changelogs/unreleased/zj-usage-data-auto-devops.yml
@@ -0,0 +1,5 @@
+---
+title: Add usage data for Auto DevOps
+merge_request:
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index e9661090844..cd44f888d3f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -76,6 +76,13 @@ production: &base
# default_can_create_group: false # default: true
# username_changing_enabled: false # default: true - User can change her username/namespace
+ ## Default theme ID
+ ## 1 - Indigo
+ ## 2 - Dark
+ ## 3 - Light
+ ## 4 - Blue
+ ## 5 - Green
+ # default_theme: 1 # default: 1
## Automatic issue closing
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 8a560d84f1f..94429ee91a9 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({})
Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
+Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
diff --git a/db/migrate/20170816234252_add_theme_id_to_users.rb b/db/migrate/20170816234252_add_theme_id_to_users.rb
new file mode 100644
index 00000000000..5043f9ec591
--- /dev/null
+++ b/db/migrate/20170816234252_add_theme_id_to_users.rb
@@ -0,0 +1,10 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddThemeIdToUsers < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :users, :theme_id, :integer, limit: 2
+ end
+end
diff --git a/db/migrate/20170828135939_migrate_user_external_mail_data.rb b/db/migrate/20170828135939_migrate_user_external_mail_data.rb
deleted file mode 100644
index 592e141b7e6..00000000000
--- a/db/migrate/20170828135939_migrate_user_external_mail_data.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class MigrateUserExternalMailData < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- disable_ddl_transaction!
-
- class User < ActiveRecord::Base
- self.table_name = 'users'
-
- include EachBatch
- end
-
- class UserSyncedAttributesMetadata < ActiveRecord::Base
- self.table_name = 'user_synced_attributes_metadata'
-
- include EachBatch
- end
-
- def up
- User.each_batch do |batch|
- start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
-
- execute <<-EOF
- INSERT INTO user_synced_attributes_metadata (user_id, provider, email_synced)
- SELECT id, email_provider, external_email
- FROM users
- WHERE external_email = TRUE
- AND NOT EXISTS (
- SELECT true
- FROM user_synced_attributes_metadata
- WHERE user_id = users.id
- AND provider = users.email_provider
- )
- AND id BETWEEN #{start_id} AND #{end_id}
- EOF
- end
- end
-
- def down
- UserSyncedAttributesMetadata.each_batch do |batch|
- start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
-
- execute <<-EOF
- UPDATE users
- SET users.email_provider = metadata.provider, users.external_email = metadata.email_synced
- FROM user_synced_attributes_metadata as metadata, users
- WHERE metadata.email_synced = TRUE
- AND metadata.user_id = users.id
- AND id BETWEEN #{start_id} AND #{end_id}
- EOF
- end
- end
-end
diff --git a/db/schema.rb b/db/schema.rb
index df941afa7d7..2149f5ad23d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1608,6 +1608,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.boolean "notified_of_own_activity"
t.string "preferred_language"
t.string "rss_token"
+ t.integer "theme_id", limit: 2
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 376ac27df3a..ddcf7830621 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -32,6 +32,7 @@ Parameters:
"twitter": "",
"website_url": "",
"email": "john@example.com",
+ "theme_id": 2,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": null,
diff --git a/doc/api/session.md b/doc/api/session.md
index f79eac11689..b97e26f34a2 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -39,6 +39,7 @@ Example response:
"twitter": "",
"website_url": "",
"email": "john@example.com",
+ "theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": "2015-07-07T07:10:58.392Z",
diff --git a/doc/api/users.md b/doc/api/users.md
index 9f3e4caf2f4..6d5db16b36a 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -72,6 +72,7 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -105,6 +106,7 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 3,
"projects_limit": 100,
@@ -215,6 +217,7 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -341,6 +344,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -387,6 +391,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
diff --git a/doc/development/README.md b/doc/development/README.md
index eeff14819da..3096d9f25f0 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -43,6 +43,7 @@
- [Object state models](object_state_models.md)
- [Building a package for testing purposes](build_test_package.md)
- [Manage feature flags](feature_flags.md)
+- [View sent emails or preview mailers](emails.md)
## Databases
diff --git a/doc/development/emails.md b/doc/development/emails.md
new file mode 100644
index 00000000000..18f47f44cb5
--- /dev/null
+++ b/doc/development/emails.md
@@ -0,0 +1,23 @@
+# Dealing with email in development
+
+## Sent emails
+
+To view rendered emails "sent" in your development instance, visit
+[`/rails/letter_opener`](http://localhost:3000/rails/letter_opener).
+
+## Mailer previews
+
+Rails provides a way to preview our mailer templates in HTML and plaintext using
+dummy data.
+
+The previews live in [`spec/mailers/previews`][previews] and can be viewed at
+[`/rails/mailers`](http://localhost:3000/rails/mailers).
+
+See the [Rails guides] for more info.
+
+[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews
+[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index babf44d2665..b31b8eaaca0 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -323,6 +323,23 @@ container registry. **Restarting a pod, scaling a service, or other actions whic
require on-going access to the registry will fail**. On-going secure access is
planned for a subsequent release.
+## Disable the banner instance wide
+
+If an administrater would like to disable the banners on an instance level, this
+feature can be disabled either through the console:
+
+```basb
+$ gitlab-rails console
+[1] pry(main)> Feature.get(:auto_devops_banner_disabled).disable
+=> true
+```
+
+Or through the HTTP API with the admin access token:
+
+```
+curl --data "value=true" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled
+```
+
## Troubleshooting
- Auto Build and Auto Test may fail in detecting your language/framework. There
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index f23c9d794b4..564dd3222ac 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -18,45 +18,66 @@ example for this guide. It contains two files:
## Fork sample project on GitLab.com
Let’s start by forking our sample application. Go to [the project
-page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork`
-button. Soon you should have a project under your namespace with the necessary
-files.
+page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
+**Fork** button. Soon you should have a project under your namespace with the
+necessary files.
## Setup your own cluster on Google Container Engine
-If you do not already have a Google Cloud account, create one at https://console.cloud.google.com.
+If you do not already have a Google Cloud account, create one at
+https://console.cloud.google.com.
-Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface.
+Visit the [**Container Engine**](https://console.cloud.google.com/kubernetes/list)
+tab and create a new cluster. You can change the name and leave the rest of the
+default settings. Once you have your cluster running, you need to connect to the
+cluster by following the Google interface.
## Connect to Kubernetes cluster
You need to have the Google Cloud SDK installed. e.g.
-On OSX, install [homebrew](https://brew.sh):
+On macOS, install [homebrew](https://brew.sh):
1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask`
2. Install Google Cloud SDK: `brew cask install google-cloud-sdk`
-3. Add `kubectl`: `gcloud components install kubectl`
+3. Add `kubectl` with: `gcloud components install kubectl`
4. Log in: `gcloud auth login`
-Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`.
+Now go back to the Google interface, find your cluster, follow the instructions
+under "Connect to the cluster" and open the Kubernetes Dashboard. It will look
+something like:
+
+```sh
+gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX
+```
+
+Finally, run `kubectl proxy`.
![connect to cluster](img/guide_connect_cluster.png)
## Copy credentials to GitLab.com project
-Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token.
+Once you have the Kubernetes Dashboard interface running, you should visit
+**Secrets** under the "Config" section. There, you should find the settings we
+need for GitLab integration: `ca.crt` and token.
![connect to cluster](img/guide_secret.png)
-You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project **Settings > Integrations > Project services > Kubernetes**. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you.
+You need to copy-paste the `ca.crt` and token into your project on GitLab.com in
+the Kubernetes integration page under project
+**Settings > Integrations > Project services > Kubernetes**. Don't actually copy
+the namespace though. Each project should have a unique namespace, and by leaving
+it blank, GitLab will create one for you.
![connect to cluster](img/guide_integration.png)
-For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform.
+For the API URL, you should use the "Endpoint" IP from your cluster page on
+Google Cloud Platform.
## Expose application to the world
-In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address.
+In order to be able to visit your application, you need to install an NGINX
+ingress controller and point your domain name to its external IP address. Let's
+see how that's done.
### Set up Ingress controller
@@ -68,28 +89,49 @@ helm init
helm install --name ruby-app stable/nginx-ingress
```
-This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that.
+This should create several services including `ruby-app-nginx-ingress-controller`.
+You can list your services by running `kubectl get svc` to confirm that.
### Point DNS at Cluster IP
-Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running:
+Find out the external IP address of the `ruby-app-nginx-ingress-controller` by
+running:
```sh
kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
-Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.<your-domain>`.
+Use this IP address to configure your DNS. This part heavily depends on your
+preferences and domain provider. But in case you are not sure, just create an
+A record with a wildcard host like `*.<your-domain>`.
-Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP.
+Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is
+assigned to the cluster IP.
## Set up Auto DevOps
-In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save.
+In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps
+section. Select "Enable Auto DevOps", add in your base domain, and save.
![auto devops settings](img/auto_devops_settings.png)
-Then trigger your first pipeline run. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, and `production`. The `build` job will create a docker image with your new change and push it to the GitLab Container Registry. The `test` job will test your change. The `codequality` job will run static analysis on your change. The `production` job will deploy your change to a production application. Once the deploy job succeeds you should be able to see your application by visiting the Kubernetes dashboard. Select the namespace of your project, which will look like `minimal-ruby-app-23`, but with a unique ID for your project, and your app will be listed as "production" under the Deployment tab.
+Next, a pipeline needs to be triggered. Since the test project doesn't have a
+`.gitlab-ci.yml`, you need to either push a change to the repository or
+manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/run`,
+where `<username>` is your username.
+
+This will create a new pipeline with several jobs: `build`, `test`, `codequality`,
+and `production`. The `build` job will create a Docker image with your new
+change and push it to the Container Registry. The `test` job will test your
+changes, whereas the `codequality` job will run static analysis on your changes.
+Finally, the `production` job will deploy your changes to a production application.
+
+Once the deploy job succeeds you should be able to see your application by
+visiting the Kubernetes dashboard. Select the namespace of your project, which
+will look like `minimal-ruby-app-23`, but with a unique ID for your project,
+and your app will be listed as "production" under the Deployment tab.
-Once its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!”
+Once its ready, just visit `http://minimal-ruby-app.example.com` to see the
+famous "Hello, world!"!
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index a7de5648c0e..5dc8e6f65f8 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -1,3 +1,5 @@
+*** NOTE: These instructions should be considered deprecated. In GitLab 10.0 we will be releasing new migration instructions using [pgloader](http://pgloader.io/).
+
# Migrating GitLab from MySQL to Postgres
*Make sure you view this [guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/mysql_to_postgresql.md#migrating-gitlab-from-mysql-to-postgres) for the most up to date instructions.*
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 216408064d1..52c49e5caa9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -45,7 +45,7 @@ module API
expose :confirmed_at
expose :last_activity_on
expose :email
- expose :color_scheme_id, :projects_limit, :current_sign_in_at
+ expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index ae43a4a3237..d202eaa4c49 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -6,7 +6,7 @@ module API
requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
end
post '/lint' do
- error = Ci::GitlabCiYamlProcessor.validation_message(params[:content])
+ error = Gitlab::Ci::YamlProcessor.validation_message(params[:content])
status 200
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
deleted file mode 100644
index b9e9f9f7f4a..00000000000
--- a/lib/ci/ansi2html.rb
+++ /dev/null
@@ -1,331 +0,0 @@
-# ANSI color library
-#
-# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
-module Ci
- module Ansi2html
- # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
- COLOR = {
- 0 => 'black', # not that this is gray in the intense color table
- 1 => 'red',
- 2 => 'green',
- 3 => 'yellow',
- 4 => 'blue',
- 5 => 'magenta',
- 6 => 'cyan',
- 7 => 'white', # not that this is gray in the dark (aka default) color table
- }.freeze
-
- STYLE_SWITCHES = {
- bold: 0x01,
- italic: 0x02,
- underline: 0x04,
- conceal: 0x08,
- cross: 0x10
- }.freeze
-
- def self.convert(ansi, state = nil)
- Converter.new.convert(ansi, state)
- end
-
- class Converter
- def on_0(s) reset() end
-
- def on_1(s) enable(STYLE_SWITCHES[:bold]) end
-
- def on_3(s) enable(STYLE_SWITCHES[:italic]) end
-
- def on_4(s) enable(STYLE_SWITCHES[:underline]) end
-
- def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
-
- def on_9(s) enable(STYLE_SWITCHES[:cross]) end
-
- def on_21(s) disable(STYLE_SWITCHES[:bold]) end
-
- def on_22(s) disable(STYLE_SWITCHES[:bold]) end
-
- def on_23(s) disable(STYLE_SWITCHES[:italic]) end
-
- def on_24(s) disable(STYLE_SWITCHES[:underline]) end
-
- def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
-
- def on_29(s) disable(STYLE_SWITCHES[:cross]) end
-
- def on_30(s) set_fg_color(0) end
-
- def on_31(s) set_fg_color(1) end
-
- def on_32(s) set_fg_color(2) end
-
- def on_33(s) set_fg_color(3) end
-
- def on_34(s) set_fg_color(4) end
-
- def on_35(s) set_fg_color(5) end
-
- def on_36(s) set_fg_color(6) end
-
- def on_37(s) set_fg_color(7) end
-
- def on_38(s) set_fg_color_256(s) end
-
- def on_39(s) set_fg_color(9) end
-
- def on_40(s) set_bg_color(0) end
-
- def on_41(s) set_bg_color(1) end
-
- def on_42(s) set_bg_color(2) end
-
- def on_43(s) set_bg_color(3) end
-
- def on_44(s) set_bg_color(4) end
-
- def on_45(s) set_bg_color(5) end
-
- def on_46(s) set_bg_color(6) end
-
- def on_47(s) set_bg_color(7) end
-
- def on_48(s) set_bg_color_256(s) end
-
- def on_49(s) set_bg_color(9) end
-
- def on_90(s) set_fg_color(0, 'l') end
-
- def on_91(s) set_fg_color(1, 'l') end
-
- def on_92(s) set_fg_color(2, 'l') end
-
- def on_93(s) set_fg_color(3, 'l') end
-
- def on_94(s) set_fg_color(4, 'l') end
-
- def on_95(s) set_fg_color(5, 'l') end
-
- def on_96(s) set_fg_color(6, 'l') end
-
- def on_97(s) set_fg_color(7, 'l') end
-
- def on_99(s) set_fg_color(9, 'l') end
-
- def on_100(s) set_bg_color(0, 'l') end
-
- def on_101(s) set_bg_color(1, 'l') end
-
- def on_102(s) set_bg_color(2, 'l') end
-
- def on_103(s) set_bg_color(3, 'l') end
-
- def on_104(s) set_bg_color(4, 'l') end
-
- def on_105(s) set_bg_color(5, 'l') end
-
- def on_106(s) set_bg_color(6, 'l') end
-
- def on_107(s) set_bg_color(7, 'l') end
-
- def on_109(s) set_bg_color(9, 'l') end
-
- attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
-
- STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
-
- def convert(stream, new_state)
- reset_state
- restore_state(new_state, stream) if new_state.present?
-
- append = false
- truncated = false
-
- cur_offset = stream.tell
- if cur_offset > @offset
- @offset = cur_offset
- truncated = true
- else
- stream.seek(@offset)
- append = @offset > 0
- end
- start_offset = @offset
-
- open_new_tag
-
- stream.each_line do |line|
- s = StringScanner.new(line)
- until s.eos?
- if s.scan(/\e([@-_])(.*?)([@-~])/)
- handle_sequence(s)
- elsif s.scan(/\e(([@-_])(.*?)?)?$/)
- break
- elsif s.scan(/</)
- @out << '&lt;'
- elsif s.scan(/\r?\n/)
- @out << '<br>'
- else
- @out << s.scan(/./m)
- end
- @offset += s.matched_size
- end
- end
-
- close_open_tags()
-
- OpenStruct.new(
- html: @out.force_encoding(Encoding.default_external),
- state: state,
- append: append,
- truncated: truncated,
- offset: start_offset,
- size: stream.tell - start_offset,
- total: stream.size
- )
- end
-
- def handle_sequence(s)
- indicator = s[1]
- commands = s[2].split ';'
- terminator = s[3]
-
- # We are only interested in color and text style changes - triggered by
- # sequences starting with '\e[' and ending with 'm'. Any other control
- # sequence gets stripped (including stuff like "delete last line")
- return unless indicator == '[' && terminator == 'm'
-
- close_open_tags()
-
- if commands.empty?()
- reset()
- return
- end
-
- evaluate_command_stack(commands)
-
- open_new_tag
- end
-
- def evaluate_command_stack(stack)
- return unless command = stack.shift()
-
- if self.respond_to?("on_#{command}", true)
- self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- evaluate_command_stack(stack)
- end
-
- def open_new_tag
- css_classes = []
-
- unless @fg_color.nil?
- fg_color = @fg_color
- # Most terminals show bold colored text in the light color variant
- # Let's mimic that here
- if @style_mask & STYLE_SWITCHES[:bold] != 0
- fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
- end
- css_classes << fg_color
- end
- css_classes << @bg_color unless @bg_color.nil?
-
- STYLE_SWITCHES.each do |css_class, flag|
- css_classes << "term-#{css_class}" if @style_mask & flag != 0
- end
-
- return if css_classes.empty?
-
- @out << %{<span class="#{css_classes.join(' ')}">}
- @n_open_tags += 1
- end
-
- def close_open_tags
- while @n_open_tags > 0
- @out << %{</span>}
- @n_open_tags -= 1
- end
- end
-
- def reset_state
- @offset = 0
- @n_open_tags = 0
- @out = ''
- reset
- end
-
- def state
- state = STATE_PARAMS.inject({}) do |h, param|
- h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
- h
- end
- Base64.urlsafe_encode64(state.to_json)
- end
-
- def restore_state(new_state, stream)
- state = Base64.urlsafe_decode64(new_state)
- state = JSON.parse(state, symbolize_names: true)
- return if state[:offset].to_i > stream.size
-
- STATE_PARAMS.each do |param|
- send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
- def reset
- @fg_color = nil
- @bg_color = nil
- @style_mask = 0
- end
-
- def enable(flag)
- @style_mask |= flag
- end
-
- def disable(flag)
- @style_mask &= ~flag
- end
-
- def set_fg_color(color_index, prefix = nil)
- @fg_color = get_term_color_class(color_index, ["fg", prefix])
- end
-
- def set_bg_color(color_index, prefix = nil)
- @bg_color = get_term_color_class(color_index, ["bg", prefix])
- end
-
- def get_term_color_class(color_index, prefix)
- color_name = COLOR[color_index]
- return nil if color_name.nil?
-
- get_color_class(["term", prefix, color_name])
- end
-
- def set_fg_color_256(command_stack)
- css_class = get_xterm_color_class(command_stack, "fg")
- @fg_color = css_class unless css_class.nil?
- end
-
- def set_bg_color_256(command_stack)
- css_class = get_xterm_color_class(command_stack, "bg")
- @bg_color = css_class unless css_class.nil?
- end
-
- def get_xterm_color_class(command_stack, prefix)
- # the 38 and 48 commands have to be followed by "5" and the color index
- return unless command_stack.length >= 2
- return unless command_stack[0] == "5"
-
- command_stack.shift() # ignore the "5" command
- color_index = command_stack.shift().to_i
-
- return unless color_index >= 0
- return unless color_index <= 255
-
- get_color_class(["xterm", prefix, color_index])
- end
-
- def get_color_class(segments)
- [segments].flatten.compact.join('-')
- end
- end
- end
-end
diff --git a/lib/ci/assets/.gitkeep b/lib/ci/assets/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/lib/ci/assets/.gitkeep
+++ /dev/null
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
deleted file mode 100644
index 76a69bf8a83..00000000000
--- a/lib/ci/charts.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-module Ci
- module Charts
- module DailyInterval
- def grouped_count(query)
- query
- .group("DATE(#{Ci::Pipeline.table_name}.created_at)")
- .count(:created_at)
- .transform_keys { |date| date.strftime(@format) }
- end
-
- def interval_step
- @interval_step ||= 1.day
- end
- end
-
- module MonthlyInterval
- def grouped_count(query)
- if Gitlab::Database.postgresql?
- query
- .group("to_char(#{Ci::Pipeline.table_name}.created_at, '01 Month YYYY')")
- .count(:created_at)
- .transform_keys(&:squish)
- else
- query
- .group("DATE_FORMAT(#{Ci::Pipeline.table_name}.created_at, '01 %M %Y')")
- .count(:created_at)
- end
- end
-
- def interval_step
- @interval_step ||= 1.month
- end
- end
-
- class Chart
- attr_reader :labels, :total, :success, :project, :pipeline_times
-
- def initialize(project)
- @labels = []
- @total = []
- @success = []
- @pipeline_times = []
- @project = project
-
- collect
- end
-
- def collect
- query = project.pipelines
- .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection
-
- totals_count = grouped_count(query)
- success_count = grouped_count(query.success)
-
- current = @from
- while current < @to
- label = current.strftime(@format)
-
- @labels << label
- @total << (totals_count[label] || 0)
- @success << (success_count[label] || 0)
-
- current += interval_step
- end
- end
- end
-
- class YearChart < Chart
- include MonthlyInterval
-
- def initialize(*)
- @to = Date.today.end_of_month
- @from = @to.years_ago(1).beginning_of_month
- @format = '%d %B %Y'
-
- super
- end
- end
-
- class MonthChart < Chart
- include DailyInterval
-
- def initialize(*)
- @to = Date.today
- @from = @to - 30.days
- @format = '%d %B'
-
- super
- end
- end
-
- class WeekChart < Chart
- include DailyInterval
-
- def initialize(*)
- @to = Date.today
- @from = @to - 7.days
- @format = '%d %B'
-
- super
- end
- end
-
- class PipelineTime < Chart
- def collect
- commits = project.pipelines.last(30)
-
- commits.each do |commit|
- @labels << commit.short_sha
- duration = commit.duration || 0
- @pipeline_times << (duration / 60)
- end
- end
- end
- end
-end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
deleted file mode 100644
index 62b44389b15..00000000000
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ /dev/null
@@ -1,251 +0,0 @@
-module Ci
- class GitlabCiYamlProcessor
- ValidationError = Class.new(StandardError)
-
- include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
-
- attr_reader :path, :cache, :stages, :jobs
-
- def initialize(config, path = nil)
- @ci_config = Gitlab::Ci::Config.new(config)
- @config = @ci_config.to_hash
- @path = path
-
- unless @ci_config.valid?
- raise ValidationError, @ci_config.errors.first
- end
-
- initial_parsing
- rescue Gitlab::Ci::Config::Loader::FormatError => e
- raise ValidationError, e.message
- end
-
- def builds_for_stage_and_ref(stage, ref, tag = false, source = nil)
- jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _|
- build_attributes(name)
- end
- end
-
- def builds
- @jobs.map do |name, _|
- build_attributes(name)
- end
- end
-
- def stage_seeds(pipeline)
- seeds = @stages.uniq.map do |stage|
- builds = pipeline_stage_builds(stage, pipeline)
-
- Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
- end
-
- seeds.compact
- end
-
- def build_attributes(name)
- job = @jobs[name.to_sym] || {}
-
- { stage_idx: @stages.index(job[:stage]),
- stage: job[:stage],
- commands: job[:commands],
- tag_list: job[:tags] || [],
- name: job[:name].to_s,
- allow_failure: job[:ignore],
- when: job[:when] || 'on_success',
- environment: job[:environment_name],
- coverage_regex: job[:coverage],
- yaml_variables: yaml_variables(name),
- options: {
- image: job[:image],
- services: job[:services],
- artifacts: job[:artifacts],
- cache: job[:cache],
- dependencies: job[:dependencies],
- before_script: job[:before_script],
- script: job[:script],
- after_script: job[:after_script],
- environment: job[:environment],
- retry: job[:retry]
- }.compact }
- end
-
- def self.validation_message(content)
- return 'Please provide content of .gitlab-ci.yml' if content.blank?
-
- begin
- Ci::GitlabCiYamlProcessor.new(content)
- nil
- rescue ValidationError, Psych::SyntaxError => e
- e.message
- end
- end
-
- private
-
- def pipeline_stage_builds(stage, pipeline)
- builds = builds_for_stage_and_ref(
- stage, pipeline.ref, pipeline.tag?, pipeline.source)
-
- builds.select do |build|
- job = @jobs[build.fetch(:name).to_sym]
- has_kubernetes = pipeline.has_kubernetes_active?
- only_kubernetes = job.dig(:only, :kubernetes)
- except_kubernetes = job.dig(:except, :kubernetes)
-
- [!only_kubernetes && !except_kubernetes,
- only_kubernetes && has_kubernetes,
- except_kubernetes && !has_kubernetes].any?
- end
- end
-
- def jobs_for_ref(ref, tag = false, source = nil)
- @jobs.select do |_, job|
- process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source)
- end
- end
-
- def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil)
- jobs_for_ref(ref, tag, source).select do |_, job|
- job[:stage] == stage
- end
- end
-
- def initial_parsing
- ##
- # Global config
- #
- @before_script = @ci_config.before_script
- @image = @ci_config.image
- @after_script = @ci_config.after_script
- @services = @ci_config.services
- @variables = @ci_config.variables
- @stages = @ci_config.stages
- @cache = @ci_config.cache
-
- ##
- # Jobs
- #
- @jobs = @ci_config.jobs
-
- @jobs.each do |name, job|
- # logical validation for job
-
- validate_job_stage!(name, job)
- validate_job_dependencies!(name, job)
- validate_job_environment!(name, job)
- end
- end
-
- def yaml_variables(name)
- variables = (@variables || {})
- .merge(job_variables(name))
-
- variables.map do |key, value|
- { key: key.to_s, value: value, public: true }
- end
- end
-
- def job_variables(name)
- job = @jobs[name.to_sym]
- return {} unless job
-
- job[:variables] || {}
- end
-
- def validate_job_stage!(name, job)
- return unless job[:stage]
-
- unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
- raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
- end
- end
-
- def validate_job_dependencies!(name, job)
- return unless job[:dependencies]
-
- stage_index = @stages.index(job[:stage])
-
- job[:dependencies].each do |dependency|
- raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
-
- unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
- raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
- end
- end
- end
-
- def validate_job_environment!(name, job)
- return unless job[:environment]
- return unless job[:environment].is_a?(Hash)
-
- environment = job[:environment]
- validate_on_stop_job!(name, environment, environment[:on_stop])
- end
-
- def validate_on_stop_job!(name, environment, on_stop)
- return unless on_stop
-
- on_stop_job = @jobs[on_stop.to_sym]
- unless on_stop_job
- raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
- end
-
- unless on_stop_job[:environment]
- raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
- end
-
- unless on_stop_job[:environment][:name] == environment[:name]
- raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
- end
-
- unless on_stop_job[:environment][:action] == 'stop'
- raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
- end
- end
-
- def process?(only_params, except_params, ref, tag, source)
- if only_params.present?
- return false unless matching?(only_params, ref, tag, source)
- end
-
- if except_params.present?
- return false if matching?(except_params, ref, tag, source)
- end
-
- true
- end
-
- def matching?(patterns, ref, tag, source)
- patterns.any? do |pattern|
- pattern, path = pattern.split('@', 2)
- matches_path?(path) && matches_pattern?(pattern, ref, tag, source)
- end
- end
-
- def matches_path?(path)
- return true unless path
-
- path == self.path
- end
-
- def matches_pattern?(pattern, ref, tag, source)
- return true if tag && pattern == 'tags'
- return true if !tag && pattern == 'branches'
- return true if source_to_pattern(source) == pattern
-
- if pattern.first == "/" && pattern.last == "/"
- Regexp.new(pattern[1...-1]) =~ ref
- else
- pattern == ref
- end
- end
-
- def source_to_pattern(source)
- if %w[api external web].include?(source)
- source
- else
- source&.pluralize
- end
- end
- end
-end
diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb
deleted file mode 100644
index 997377abc55..00000000000
--- a/lib/ci/mask_secret.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module Ci::MaskSecret
- class << self
- def mask!(value, token)
- return value unless value.present? && token.present?
-
- value.gsub!(token, 'x' * token.length)
- value
- end
- end
-end
diff --git a/lib/ci/model.rb b/lib/ci/model.rb
deleted file mode 100644
index c42a0ad36db..00000000000
--- a/lib/ci/model.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module Ci
- module Model
- def table_name_prefix
- "ci_"
- end
-
- def model_name
- @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
- end
- end
-end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
new file mode 100644
index 00000000000..ad78ae244b2
--- /dev/null
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -0,0 +1,333 @@
+# ANSI color library
+#
+# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
+module Gitlab
+ module Ci
+ module Ansi2html
+ # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
+ COLOR = {
+ 0 => 'black', # not that this is gray in the intense color table
+ 1 => 'red',
+ 2 => 'green',
+ 3 => 'yellow',
+ 4 => 'blue',
+ 5 => 'magenta',
+ 6 => 'cyan',
+ 7 => 'white', # not that this is gray in the dark (aka default) color table
+ }.freeze
+
+ STYLE_SWITCHES = {
+ bold: 0x01,
+ italic: 0x02,
+ underline: 0x04,
+ conceal: 0x08,
+ cross: 0x10
+ }.freeze
+
+ def self.convert(ansi, state = nil)
+ Converter.new.convert(ansi, state)
+ end
+
+ class Converter
+ def on_0(s) reset() end
+
+ def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+
+ def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+
+ def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+
+ def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+
+ def on_9(s) enable(STYLE_SWITCHES[:cross]) end
+
+ def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+
+ def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+
+ def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+
+ def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+
+ def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+
+ def on_29(s) disable(STYLE_SWITCHES[:cross]) end
+
+ def on_30(s) set_fg_color(0) end
+
+ def on_31(s) set_fg_color(1) end
+
+ def on_32(s) set_fg_color(2) end
+
+ def on_33(s) set_fg_color(3) end
+
+ def on_34(s) set_fg_color(4) end
+
+ def on_35(s) set_fg_color(5) end
+
+ def on_36(s) set_fg_color(6) end
+
+ def on_37(s) set_fg_color(7) end
+
+ def on_38(s) set_fg_color_256(s) end
+
+ def on_39(s) set_fg_color(9) end
+
+ def on_40(s) set_bg_color(0) end
+
+ def on_41(s) set_bg_color(1) end
+
+ def on_42(s) set_bg_color(2) end
+
+ def on_43(s) set_bg_color(3) end
+
+ def on_44(s) set_bg_color(4) end
+
+ def on_45(s) set_bg_color(5) end
+
+ def on_46(s) set_bg_color(6) end
+
+ def on_47(s) set_bg_color(7) end
+
+ def on_48(s) set_bg_color_256(s) end
+
+ def on_49(s) set_bg_color(9) end
+
+ def on_90(s) set_fg_color(0, 'l') end
+
+ def on_91(s) set_fg_color(1, 'l') end
+
+ def on_92(s) set_fg_color(2, 'l') end
+
+ def on_93(s) set_fg_color(3, 'l') end
+
+ def on_94(s) set_fg_color(4, 'l') end
+
+ def on_95(s) set_fg_color(5, 'l') end
+
+ def on_96(s) set_fg_color(6, 'l') end
+
+ def on_97(s) set_fg_color(7, 'l') end
+
+ def on_99(s) set_fg_color(9, 'l') end
+
+ def on_100(s) set_bg_color(0, 'l') end
+
+ def on_101(s) set_bg_color(1, 'l') end
+
+ def on_102(s) set_bg_color(2, 'l') end
+
+ def on_103(s) set_bg_color(3, 'l') end
+
+ def on_104(s) set_bg_color(4, 'l') end
+
+ def on_105(s) set_bg_color(5, 'l') end
+
+ def on_106(s) set_bg_color(6, 'l') end
+
+ def on_107(s) set_bg_color(7, 'l') end
+
+ def on_109(s) set_bg_color(9, 'l') end
+
+ attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
+
+ STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
+
+ def convert(stream, new_state)
+ reset_state
+ restore_state(new_state, stream) if new_state.present?
+
+ append = false
+ truncated = false
+
+ cur_offset = stream.tell
+ if cur_offset > @offset
+ @offset = cur_offset
+ truncated = true
+ else
+ stream.seek(@offset)
+ append = @offset > 0
+ end
+ start_offset = @offset
+
+ open_new_tag
+
+ stream.each_line do |line|
+ s = StringScanner.new(line)
+ until s.eos?
+ if s.scan(/\e([@-_])(.*?)([@-~])/)
+ handle_sequence(s)
+ elsif s.scan(/\e(([@-_])(.*?)?)?$/)
+ break
+ elsif s.scan(/</)
+ @out << '&lt;'
+ elsif s.scan(/\r?\n/)
+ @out << '<br>'
+ else
+ @out << s.scan(/./m)
+ end
+ @offset += s.matched_size
+ end
+ end
+
+ close_open_tags()
+
+ OpenStruct.new(
+ html: @out.force_encoding(Encoding.default_external),
+ state: state,
+ append: append,
+ truncated: truncated,
+ offset: start_offset,
+ size: stream.tell - start_offset,
+ total: stream.size
+ )
+ end
+
+ def handle_sequence(s)
+ indicator = s[1]
+ commands = s[2].split ';'
+ terminator = s[3]
+
+ # We are only interested in color and text style changes - triggered by
+ # sequences starting with '\e[' and ending with 'm'. Any other control
+ # sequence gets stripped (including stuff like "delete last line")
+ return unless indicator == '[' && terminator == 'm'
+
+ close_open_tags()
+
+ if commands.empty?()
+ reset()
+ return
+ end
+
+ evaluate_command_stack(commands)
+
+ open_new_tag
+ end
+
+ def evaluate_command_stack(stack)
+ return unless command = stack.shift()
+
+ if self.respond_to?("on_#{command}", true)
+ self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ evaluate_command_stack(stack)
+ end
+
+ def open_new_tag
+ css_classes = []
+
+ unless @fg_color.nil?
+ fg_color = @fg_color
+ # Most terminals show bold colored text in the light color variant
+ # Let's mimic that here
+ if @style_mask & STYLE_SWITCHES[:bold] != 0
+ fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
+ end
+ css_classes << fg_color
+ end
+ css_classes << @bg_color unless @bg_color.nil?
+
+ STYLE_SWITCHES.each do |css_class, flag|
+ css_classes << "term-#{css_class}" if @style_mask & flag != 0
+ end
+
+ return if css_classes.empty?
+
+ @out << %{<span class="#{css_classes.join(' ')}">}
+ @n_open_tags += 1
+ end
+
+ def close_open_tags
+ while @n_open_tags > 0
+ @out << %{</span>}
+ @n_open_tags -= 1
+ end
+ end
+
+ def reset_state
+ @offset = 0
+ @n_open_tags = 0
+ @out = ''
+ reset
+ end
+
+ def state
+ state = STATE_PARAMS.inject({}) do |h, param|
+ h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
+ h
+ end
+ Base64.urlsafe_encode64(state.to_json)
+ end
+
+ def restore_state(new_state, stream)
+ state = Base64.urlsafe_decode64(new_state)
+ state = JSON.parse(state, symbolize_names: true)
+ return if state[:offset].to_i > stream.size
+
+ STATE_PARAMS.each do |param|
+ send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def reset
+ @fg_color = nil
+ @bg_color = nil
+ @style_mask = 0
+ end
+
+ def enable(flag)
+ @style_mask |= flag
+ end
+
+ def disable(flag)
+ @style_mask &= ~flag
+ end
+
+ def set_fg_color(color_index, prefix = nil)
+ @fg_color = get_term_color_class(color_index, ["fg", prefix])
+ end
+
+ def set_bg_color(color_index, prefix = nil)
+ @bg_color = get_term_color_class(color_index, ["bg", prefix])
+ end
+
+ def get_term_color_class(color_index, prefix)
+ color_name = COLOR[color_index]
+ return nil if color_name.nil?
+
+ get_color_class(["term", prefix, color_name])
+ end
+
+ def set_fg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "fg")
+ @fg_color = css_class unless css_class.nil?
+ end
+
+ def set_bg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "bg")
+ @bg_color = css_class unless css_class.nil?
+ end
+
+ def get_xterm_color_class(command_stack, prefix)
+ # the 38 and 48 commands have to be followed by "5" and the color index
+ return unless command_stack.length >= 2
+ return unless command_stack[0] == "5"
+
+ command_stack.shift() # ignore the "5" command
+ color_index = command_stack.shift().to_i
+
+ return unless color_index >= 0
+ return unless color_index <= 255
+
+ get_color_class(["xterm", prefix, color_index])
+ end
+
+ def get_color_class(segments)
+ [segments].flatten.compact.join('-')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb
new file mode 100644
index 00000000000..7df7b542d91
--- /dev/null
+++ b/lib/gitlab/ci/charts.rb
@@ -0,0 +1,118 @@
+module Gitlab
+ module Ci
+ module Charts
+ module DailyInterval
+ def grouped_count(query)
+ query
+ .group("DATE(#{::Ci::Pipeline.table_name}.created_at)")
+ .count(:created_at)
+ .transform_keys { |date| date.strftime(@format) }
+ end
+
+ def interval_step
+ @interval_step ||= 1.day
+ end
+ end
+
+ module MonthlyInterval
+ def grouped_count(query)
+ if Gitlab::Database.postgresql?
+ query
+ .group("to_char(#{::Ci::Pipeline.table_name}.created_at, '01 Month YYYY')")
+ .count(:created_at)
+ .transform_keys(&:squish)
+ else
+ query
+ .group("DATE_FORMAT(#{::Ci::Pipeline.table_name}.created_at, '01 %M %Y')")
+ .count(:created_at)
+ end
+ end
+
+ def interval_step
+ @interval_step ||= 1.month
+ end
+ end
+
+ class Chart
+ attr_reader :labels, :total, :success, :project, :pipeline_times
+
+ def initialize(project)
+ @labels = []
+ @total = []
+ @success = []
+ @pipeline_times = []
+ @project = project
+
+ collect
+ end
+
+ def collect
+ query = project.pipelines
+ .where("? > #{::Ci::Pipeline.table_name}.created_at AND #{::Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection
+
+ totals_count = grouped_count(query)
+ success_count = grouped_count(query.success)
+
+ current = @from
+ while current < @to
+ label = current.strftime(@format)
+
+ @labels << label
+ @total << (totals_count[label] || 0)
+ @success << (success_count[label] || 0)
+
+ current += interval_step
+ end
+ end
+ end
+
+ class YearChart < Chart
+ include MonthlyInterval
+
+ def initialize(*)
+ @to = Date.today.end_of_month
+ @from = @to.years_ago(1).beginning_of_month
+ @format = '%d %B %Y'
+
+ super
+ end
+ end
+
+ class MonthChart < Chart
+ include DailyInterval
+
+ def initialize(*)
+ @to = Date.today
+ @from = @to - 30.days
+ @format = '%d %B'
+
+ super
+ end
+ end
+
+ class WeekChart < Chart
+ include DailyInterval
+
+ def initialize(*)
+ @to = Date.today
+ @from = @to - 7.days
+ @format = '%d %B'
+
+ super
+ end
+ end
+
+ class PipelineTime < Chart
+ def collect
+ commits = project.pipelines.last(30)
+
+ commits.each do |commit|
+ @labels << commit.short_sha
+ duration = commit.duration || 0
+ @pipeline_times << (duration / 60)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb
new file mode 100644
index 00000000000..0daddaa638c
--- /dev/null
+++ b/lib/gitlab/ci/mask_secret.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ module Ci::MaskSecret
+ class << self
+ def mask!(value, token)
+ return value unless value.present? && token.present?
+
+ value.gsub!(token, 'x' * token.length)
+ value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb
new file mode 100644
index 00000000000..3994a50772b
--- /dev/null
+++ b/lib/gitlab/ci/model.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Ci
+ module Model
+ def table_name_prefix
+ "ci_"
+ end
+
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 8503ecf8700..ab3408f48d6 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -56,13 +56,13 @@ module Gitlab
end
def html_with_state(state = nil)
- ::Ci::Ansi2html.convert(stream, state)
+ ::Gitlab::Ci::Ansi2html.convert(stream, state)
end
def html(last_lines: nil)
text = raw(last_lines: last_lines)
buffer = StringIO.new(text)
- ::Ci::Ansi2html.convert(buffer).html
+ ::Gitlab::Ci::Ansi2html.convert(buffer).html
end
def extract_coverage(regex)
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
new file mode 100644
index 00000000000..7582964b24e
--- /dev/null
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -0,0 +1,253 @@
+module Gitlab
+ module Ci
+ class YamlProcessor
+ ValidationError = Class.new(StandardError)
+
+ include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
+
+ attr_reader :path, :cache, :stages, :jobs
+
+ def initialize(config, path = nil)
+ @ci_config = Gitlab::Ci::Config.new(config)
+ @config = @ci_config.to_hash
+ @path = path
+
+ unless @ci_config.valid?
+ raise ValidationError, @ci_config.errors.first
+ end
+
+ initial_parsing
+ rescue Gitlab::Ci::Config::Loader::FormatError => e
+ raise ValidationError, e.message
+ end
+
+ def builds_for_stage_and_ref(stage, ref, tag = false, source = nil)
+ jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _|
+ build_attributes(name)
+ end
+ end
+
+ def builds
+ @jobs.map do |name, _|
+ build_attributes(name)
+ end
+ end
+
+ def stage_seeds(pipeline)
+ seeds = @stages.uniq.map do |stage|
+ builds = pipeline_stage_builds(stage, pipeline)
+
+ Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
+ end
+
+ seeds.compact
+ end
+
+ def build_attributes(name)
+ job = @jobs[name.to_sym] || {}
+
+ { stage_idx: @stages.index(job[:stage]),
+ stage: job[:stage],
+ commands: job[:commands],
+ tag_list: job[:tags] || [],
+ name: job[:name].to_s,
+ allow_failure: job[:ignore],
+ when: job[:when] || 'on_success',
+ environment: job[:environment_name],
+ coverage_regex: job[:coverage],
+ yaml_variables: yaml_variables(name),
+ options: {
+ image: job[:image],
+ services: job[:services],
+ artifacts: job[:artifacts],
+ cache: job[:cache],
+ dependencies: job[:dependencies],
+ before_script: job[:before_script],
+ script: job[:script],
+ after_script: job[:after_script],
+ environment: job[:environment],
+ retry: job[:retry]
+ }.compact }
+ end
+
+ def self.validation_message(content)
+ return 'Please provide content of .gitlab-ci.yml' if content.blank?
+
+ begin
+ Gitlab::Ci::YamlProcessor.new(content)
+ nil
+ rescue ValidationError, Psych::SyntaxError => e
+ e.message
+ end
+ end
+
+ private
+
+ def pipeline_stage_builds(stage, pipeline)
+ builds = builds_for_stage_and_ref(
+ stage, pipeline.ref, pipeline.tag?, pipeline.source)
+
+ builds.select do |build|
+ job = @jobs[build.fetch(:name).to_sym]
+ has_kubernetes = pipeline.has_kubernetes_active?
+ only_kubernetes = job.dig(:only, :kubernetes)
+ except_kubernetes = job.dig(:except, :kubernetes)
+
+ [!only_kubernetes && !except_kubernetes,
+ only_kubernetes && has_kubernetes,
+ except_kubernetes && !has_kubernetes].any?
+ end
+ end
+
+ def jobs_for_ref(ref, tag = false, source = nil)
+ @jobs.select do |_, job|
+ process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source)
+ end
+ end
+
+ def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil)
+ jobs_for_ref(ref, tag, source).select do |_, job|
+ job[:stage] == stage
+ end
+ end
+
+ def initial_parsing
+ ##
+ # Global config
+ #
+ @before_script = @ci_config.before_script
+ @image = @ci_config.image
+ @after_script = @ci_config.after_script
+ @services = @ci_config.services
+ @variables = @ci_config.variables
+ @stages = @ci_config.stages
+ @cache = @ci_config.cache
+
+ ##
+ # Jobs
+ #
+ @jobs = @ci_config.jobs
+
+ @jobs.each do |name, job|
+ # logical validation for job
+
+ validate_job_stage!(name, job)
+ validate_job_dependencies!(name, job)
+ validate_job_environment!(name, job)
+ end
+ end
+
+ def yaml_variables(name)
+ variables = (@variables || {})
+ .merge(job_variables(name))
+
+ variables.map do |key, value|
+ { key: key.to_s, value: value, public: true }
+ end
+ end
+
+ def job_variables(name)
+ job = @jobs[name.to_sym]
+ return {} unless job
+
+ job[:variables] || {}
+ end
+
+ def validate_job_stage!(name, job)
+ return unless job[:stage]
+
+ unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
+ raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
+ end
+ end
+
+ def validate_job_dependencies!(name, job)
+ return unless job[:dependencies]
+
+ stage_index = @stages.index(job[:stage])
+
+ job[:dependencies].each do |dependency|
+ raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
+
+ unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
+ raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
+ end
+ end
+ end
+
+ def validate_job_environment!(name, job)
+ return unless job[:environment]
+ return unless job[:environment].is_a?(Hash)
+
+ environment = job[:environment]
+ validate_on_stop_job!(name, environment, environment[:on_stop])
+ end
+
+ def validate_on_stop_job!(name, environment, on_stop)
+ return unless on_stop
+
+ on_stop_job = @jobs[on_stop.to_sym]
+ unless on_stop_job
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
+ end
+
+ unless on_stop_job[:environment]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
+ end
+
+ unless on_stop_job[:environment][:name] == environment[:name]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
+ end
+
+ unless on_stop_job[:environment][:action] == 'stop'
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
+ end
+ end
+
+ def process?(only_params, except_params, ref, tag, source)
+ if only_params.present?
+ return false unless matching?(only_params, ref, tag, source)
+ end
+
+ if except_params.present?
+ return false if matching?(except_params, ref, tag, source)
+ end
+
+ true
+ end
+
+ def matching?(patterns, ref, tag, source)
+ patterns.any? do |pattern|
+ pattern, path = pattern.split('@', 2)
+ matches_path?(path) && matches_pattern?(pattern, ref, tag, source)
+ end
+ end
+
+ def matches_path?(path)
+ return true unless path
+
+ path == self.path
+ end
+
+ def matches_pattern?(pattern, ref, tag, source)
+ return true if tag && pattern == 'tags'
+ return true if !tag && pattern == 'branches'
+ return true if source_to_pattern(source) == pattern
+
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ ref
+ else
+ pattern == ref
+ end
+ end
+
+ def source_to_pattern(source)
+ if %w[api external web].include?(source)
+ source
+ else
+ source&.pluralize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index 84f9ecd3d23..e3678c914db 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -12,12 +12,7 @@ module Gitlab
MissingEndDelimiter = Class.new(ParserError)
def parse(text, our_path:, their_path:, parent_file: nil)
- raise UnmergeableFile if text.blank? # Typically a binary file
- raise UnmergeableFile if text.length > 200.kilobytes
-
- text.force_encoding('UTF-8')
-
- raise UnsupportedEncoding unless text.valid_encoding?
+ validate_text!(text)
line_obj_index = 0
line_old = 1
@@ -32,15 +27,15 @@ module Gitlab
full_line = line.delete("\n")
if full_line == conflict_start
- raise UnexpectedDelimiter unless type.nil?
+ validate_delimiter!(type.nil?)
type = 'new'
elsif full_line == conflict_middle
- raise UnexpectedDelimiter unless type == 'new'
+ validate_delimiter!(type == 'new')
type = 'old'
elsif full_line == conflict_end
- raise UnexpectedDelimiter unless type == 'old'
+ validate_delimiter!(type == 'old')
type = nil
elsif line[0] == '\\'
@@ -59,6 +54,21 @@ module Gitlab
lines
end
+
+ private
+
+ def validate_text!(text)
+ raise UnmergeableFile if text.blank? # Typically a binary file
+ raise UnmergeableFile if text.length > 200.kilobytes
+
+ text.force_encoding('UTF-8')
+
+ raise UnsupportedEncoding unless text.valid_encoding?
+ end
+
+ def validate_delimiter!(condition)
+ raise UnexpectedDelimiter unless condition
+ end
end
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 5ee6669050c..1f370686186 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -352,7 +352,7 @@ module Gitlab
end
def stats
- Gitlab::Git::CommitStats.new(self)
+ Gitlab::Git::CommitStats.new(@repository, self)
end
def to_patch(options = {})
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index 00acb4763e9..6bf49a0af18 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -10,12 +10,29 @@ module Gitlab
# Instantiate a CommitStats object
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/323
- def initialize(commit)
+ def initialize(repo, commit)
@id = commit.id
@additions = 0
@deletions = 0
@total = 0
+ repo.gitaly_migrate(:commit_stats) do |is_enabled|
+ if is_enabled
+ gitaly_stats(repo, commit)
+ else
+ rugged_stats(commit)
+ end
+ end
+ end
+
+ def gitaly_stats(repo, commit)
+ stats = repo.gitaly_commit_client.commit_stats(@id)
+ @additions = stats.additions
+ @deletions = stats.deletions
+ @total = @additions + @deletions
+ end
+
+ def rugged_stats(commit)
diff = commit.rugged_diff_from_parent
diff.each_patch do |p|
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 0825a3a7694..1ba1a7830a4 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -204,6 +204,14 @@ module Gitlab
response.sum(&:data)
end
+ def commit_stats(revision)
+ request = Gitaly::CommitStatsRequest.new(
+ repository: @gitaly_repo,
+ revision: GitalyClient.encode(revision)
+ )
+ GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request)
+ end
+
private
def commit_diff_request_params(commit, options = {})
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index 9f432673a6e..344784c866f 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -4,6 +4,15 @@ require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues)
module Gitlab
module MailRoom
+ DEFAULT_CONFIG = {
+ enabled: false,
+ port: 143,
+ ssl: false,
+ start_tls: false,
+ mailbox: 'inbox',
+ idle_timeout: 60
+ }.freeze
+
class << self
def enabled?
config[:enabled] && config[:address]
@@ -22,16 +31,10 @@ module Gitlab
def fetch_config
return {} unless File.exist?(config_file)
- rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
- all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys
-
- config = all_config[:incoming_email] || {}
- config[:enabled] = false if config[:enabled].nil?
- config[:port] = 143 if config[:port].nil?
- config[:ssl] = false if config[:ssl].nil?
- config[:start_tls] = false if config[:start_tls].nil?
- config[:mailbox] = 'inbox' if config[:mailbox].nil?
- config[:idle_timeout] = 60 if config[:idle_timeout].nil?
+ config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] || {}
+ config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval|
+ newval.nil? ? oldval : newval
+ end
if config[:enabled] && config[:address]
gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env)
@@ -45,6 +48,10 @@ module Gitlab
config
end
+ def rails_env
+ @rails_env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
+ end
+
def config_file
ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__)
end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
new file mode 100644
index 00000000000..d43eff5ba4a
--- /dev/null
+++ b/lib/gitlab/themes.rb
@@ -0,0 +1,84 @@
+module Gitlab
+ # Module containing GitLab's application theme definitions and helper methods
+ # for accessing them.
+ module Themes
+ extend self
+
+ # Theme ID used when no `default_theme` configuration setting is provided.
+ APPLICATION_DEFAULT = 1
+
+ # Struct class representing a single Theme
+ Theme = Struct.new(:id, :name, :css_class)
+
+ # All available Themes
+ THEMES = [
+ Theme.new(1, 'Indigo', 'ui_indigo'),
+ Theme.new(2, 'Dark', 'ui_dark'),
+ Theme.new(3, 'Light', 'ui_light'),
+ Theme.new(4, 'Blue', 'ui_blue'),
+ Theme.new(5, 'Green', 'ui_green')
+ ].freeze
+
+ # Convenience method to get a space-separated String of all the theme
+ # classes that might be applied to the `body` element
+ #
+ # Returns a String
+ def body_classes
+ THEMES.collect(&:css_class).uniq.join(' ')
+ end
+
+ # Get a Theme by its ID
+ #
+ # If the ID is invalid, returns the default Theme.
+ #
+ # id - Integer ID
+ #
+ # Returns a Theme
+ def by_id(id)
+ THEMES.detect { |t| t.id == id } || default
+ end
+
+ # Returns the number of defined Themes
+ def count
+ THEMES.size
+ end
+
+ # Get the default Theme
+ #
+ # Returns a Theme
+ def default
+ by_id(default_id)
+ end
+
+ # Iterate through each Theme
+ #
+ # Yields the Theme object
+ def each(&block)
+ THEMES.each(&block)
+ end
+
+ # Get the Theme for the specified user, or the default
+ #
+ # user - User record
+ #
+ # Returns a Theme
+ def for_user(user)
+ if user
+ by_id(user.theme_id)
+ else
+ default
+ end
+ end
+
+ private
+
+ def default_id
+ @default_id ||= begin
+ id = Gitlab.config.gitlab.default_theme.to_i
+ theme_ids = THEMES.map(&:id)
+
+ theme_ids.include?(id) ? id : APPLICATION_DEFAULT
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 3cf26625108..36708078136 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -22,9 +22,13 @@ module Gitlab
ci_builds: ::Ci::Build.count,
ci_internal_pipelines: ::Ci::Pipeline.internal.count,
ci_external_pipelines: ::Ci::Pipeline.external.count,
+ ci_pipeline_config_auto_devops: ::Ci::Pipeline.auto_devops_source.count,
+ ci_pipeline_config_repository: ::Ci::Pipeline.repository_source.count,
ci_runners: ::Ci::Runner.count,
ci_triggers: ::Ci::Trigger.count,
ci_pipeline_schedules: ::Ci::PipelineSchedule.count,
+ auto_devops_enabled: ::ProjectAutoDevops.enabled.count,
+ auto_devops_disabled: ::ProjectAutoDevops.disabled.count,
deploy_keys: DeployKey.count,
deployments: Deployment.count,
environments: ::Environment.count,
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index a5f544b4f92..a66b4ab0902 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -25,7 +25,8 @@ describe Profiles::PreferencesController do
def go(params: {}, format: :js)
params.reverse_merge!(
color_scheme_id: '1',
- dashboard: 'stars'
+ dashboard: 'stars',
+ theme_id: '1'
)
patch :update, user: params, format: format
@@ -40,7 +41,8 @@ describe Profiles::PreferencesController do
it "changes the user's preferences" do
prefs = {
color_scheme_id: '1',
- dashboard: 'stars'
+ dashboard: 'stars',
+ theme_id: '2'
}.with_indifferent_access
expect(user).to receive(:assign_attributes).with(prefs)
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 3b5c6966287..a4ed589f3de 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -164,9 +164,9 @@ feature 'Jobs' do
end
it 'links to issues/new with the title and description filled in' do
- button_title = "Build Failed ##{job.id}"
- job_path = project_job_path(project, job)
- options = { issue: { title: button_title, description: job_path } }
+ button_title = "Job Failed ##{job.id}"
+ job_url = project_job_path(project, job)
+ options = { issue: { title: button_title, description: "Job [##{job.id}](#{job_url}) failed for #{job.sha}:\n" } }
href = new_project_issue_path(project, options)
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
index 6181b3ccc86..e6c1d9c9d84 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/login.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
@@ -19,6 +19,7 @@
"organization",
"last_sign_in_at",
"confirmed_at",
+ "theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index b6d892548ef..80d58ff6bf7 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -10,6 +10,8 @@ describe AutoDevopsHelper do
before do
allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed }
allow(helper).to receive(:current_user) { user }
+
+ Feature.get(:auto_devops_banner_disabled).disable
end
subject { helper.show_auto_devops_callout?(project) }
@@ -18,6 +20,14 @@ describe AutoDevopsHelper do
it { is_expected.to eq(true) }
end
+ context 'when the banner is disabled by feature flag' do
+ it 'allows the feature flag to disable' do
+ Feature.get(:auto_devops_banner_disabled).enable
+
+ expect(subject).to be(false)
+ end
+ end
+
context 'when dismissed' do
before do
helper.request.cookies[:auto_devops_settings_dismissed] = 'true'
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index a04c87b08eb..8b8080563d3 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe PreferencesHelper do
- describe 'dashboard_choices' do
+ describe '#dashboard_choices' do
it 'raises an exception when defined choices may be missing' do
expect(User).to receive(:dashboards).and_return(foo: 'foo')
expect { helper.dashboard_choices }.to raise_error(RuntimeError)
@@ -26,7 +26,33 @@ describe PreferencesHelper do
end
end
- describe 'user_color_scheme' do
+ describe '#user_application_theme' do
+ context 'with a user' do
+ it "returns user's theme's css_class" do
+ stub_user(theme_id: 3)
+
+ expect(helper.user_application_theme).to eq 'ui_light'
+ end
+
+ it 'returns the default when id is invalid' do
+ stub_user(theme_id: Gitlab::Themes.count + 5)
+
+ allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1)
+
+ expect(helper.user_application_theme).to eq 'ui_indigo'
+ end
+ end
+
+ context 'without a user' do
+ it 'returns the default theme' do
+ stub_user
+
+ expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class
+ end
+ end
+ end
+
+ describe '#user_color_scheme' do
context 'with a user' do
it "returns user's scheme's css_class" do
allow(helper).to receive(:current_user)
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
index 74bdbb01166..37cc08b3038 100644
--- a/spec/initializers/doorkeeper_spec.rb
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -10,7 +10,7 @@ describe Doorkeeper.configuration do
describe '#optional_scopes' do
it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
- expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES
+ expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES - Gitlab::Auth::REGISTRY_SCOPES
end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
deleted file mode 100644
index 1efd3113a43..00000000000
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ /dev/null
@@ -1,1697 +0,0 @@
-require 'spec_helper'
-
-module Ci
- describe GitlabCiYamlProcessor, :lib do
- subject { described_class.new(config, path) }
- let(:path) { 'path' }
-
- describe 'our current .gitlab-ci.yml' do
- let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
-
- it 'is valid' do
- error_message = described_class.validation_message(config)
-
- expect(error_message).to be_nil
- end
- end
-
- describe '#build_attributes' do
- subject { described_class.new(config, path).build_attributes(:rspec) }
-
- describe 'coverage entry' do
- describe 'code coverage regexp' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- coverage: '/Code coverage: \d+\.\d+/' })
- end
-
- it 'includes coverage regexp in build attributes' do
- expect(subject)
- .to include(coverage_regex: 'Code coverage: \d+\.\d+')
- end
- end
- end
-
- describe 'retry entry' do
- context 'when retry count is specified' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec', retry: 1 })
- end
-
- it 'includes retry count in build options attribute' do
- expect(subject[:options]).to include(retry: 1)
- end
- end
-
- context 'when retry count is not specified' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec' })
- end
-
- it 'does not persist retry count in the database' do
- expect(subject[:options]).not_to have_key(:retry)
- end
- end
- end
-
- describe 'allow failure entry' do
- context 'when job is a manual action' do
- context 'when allow_failure is defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- when: 'manual',
- allow_failure: false })
- end
-
- it 'is not allowed to fail' do
- expect(subject[:allow_failure]).to be false
- end
- end
-
- context 'when allow_failure is not defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- when: 'manual' })
- end
-
- it 'is allowed to fail' do
- expect(subject[:allow_failure]).to be true
- end
- end
- end
-
- context 'when job is not a manual action' do
- context 'when allow_failure is defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec',
- allow_failure: false })
- end
-
- it 'is not allowed to fail' do
- expect(subject[:allow_failure]).to be false
- end
- end
-
- context 'when allow_failure is not defined' do
- let(:config) do
- YAML.dump(rspec: { script: 'rspec' })
- end
-
- it 'is not allowed to fail' do
- expect(subject[:allow_failure]).to be false
- end
- end
- end
- end
- end
-
- describe '#stage_seeds' do
- context 'when no refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
- rspec: { stage: 'test', script: 'rspec' },
- spinach: { stage: 'test', script: 'spinach' })
- end
-
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- it 'correctly fabricates a stage seeds object' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.second.stage[:name]).to eq 'deploy'
- expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
- expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
- spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, ref: 'feature', tag: true)
- end
-
- it 'returns stage seeds only assigned to master to master' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
-
- context 'when source policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
- spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, source: :schedule)
- end
-
- it 'returns stage seeds only assigned to schedules' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
-
- context 'when kubernetes policy is specified' do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- let(:config) do
- YAML.dump(
- spinach: { stage: 'test', script: 'spinach' },
- production: {
- stage: 'deploy',
- script: 'cap',
- only: { kubernetes: 'active' }
- }
- )
- end
-
- context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
-
- it 'returns seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when kubernetes is not active' do
- it 'does not return seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
- end
- end
-
- describe "#builds_for_stage_and_ref" do
- let(:type) { 'test' }
-
- it "returns builds if no branch specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec" }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- describe 'only' do
- it "does not return builds if only has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["deploy"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
-
- it "does not return builds if only has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["/^deploy$/"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
-
- it "returns builds if only has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["master"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
-
- it "returns builds if only has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: %w(master deploy) }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "returns builds if only has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "does not return builds if only has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["tags"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns builds if only has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
- end
- end
-
- it "does not return builds if only has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
- end
- end
-
- it "returns builds if only has current repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches@path"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "does not return builds if only has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches@fork"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns build only for specified type" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", only: %w(master deploy) },
- staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
- production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, 'fork')
-
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
- expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
- let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
-
- context 'when it is integer' do
- let(:only) { 1 }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:only has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:only) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:only) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
- end
- end
- end
-
- describe 'except' do
- it "returns builds if except has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["deploy"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
-
- it "returns builds if except has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["/^deploy$/"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
-
- it "does not return builds if except has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["master"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
-
- it "does not return builds if except has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: %w(master deploy) }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "does not return builds if except has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns builds if except has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["tags"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "does not return builds if except has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
- end
- end
-
- it "returns builds if except has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
- end
- end
-
- it "does not return builds if except has current repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches@path"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
- end
-
- it "returns builds if except has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches@fork"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- end
-
- it "returns build except specified type" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
- staging: { script: "deploy", type: "deploy", except: ["master"] },
- production: { script: "deploy", type: "deploy", except: ["master@fork"] }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, 'fork')
-
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
- expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
- expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", except: except } } }
- let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
-
- context 'when it is integer' do
- let(:except) { 1 }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:except has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:except) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:except) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
- end
- end
- end
- end
-
- describe "Scripts handling" do
- let(:config_data) { YAML.dump(config) }
- let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
-
- subject { config_processor.builds_for_stage_and_ref("test", "master").first }
-
- describe "before_script" do
- context "in global context" do
- let(:config) do
- {
- before_script: ["global script"],
- test: { script: ["script"] }
- }
- end
-
- it "return commands with scripts concencaced" do
- expect(subject[:commands]).to eq("global script\nscript")
- end
- end
-
- context "overwritten in local context" do
- let(:config) do
- {
- before_script: ["global script"],
- test: { before_script: ["local script"], script: ["script"] }
- }
- end
-
- it "return commands with scripts concencaced" do
- expect(subject[:commands]).to eq("local script\nscript")
- end
- end
- end
-
- describe "script" do
- let(:config) do
- {
- test: { script: ["script"] }
- }
- end
-
- it "return commands with scripts concencaced" do
- expect(subject[:commands]).to eq("script")
- end
- end
-
- describe "after_script" do
- context "in global context" do
- let(:config) do
- {
- after_script: ["after_script"],
- test: { script: ["script"] }
- }
- end
-
- it "return after_script in options" do
- expect(subject[:options][:after_script]).to eq(["after_script"])
- end
- end
-
- context "overwritten in local context" do
- let(:config) do
- {
- after_script: ["local after_script"],
- test: { after_script: ["local after_script"], script: ["script"] }
- }
- end
-
- it "return after_script in options" do
- expect(subject[:options][:after_script]).to eq(["local after_script"])
- end
- end
- end
- end
-
- describe "Image and service handling" do
- context "when extended docker configuration is used" do
- it "returns image and service when defined" do
- config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
- services: ["mysql", { name: "docker:dind", alias: "docker",
- entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] }],
- before_script: ["pwd"],
- rspec: { script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
- services: [{ name: "mysql" },
- { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- it "returns image and service when overridden for job" do
- config = YAML.dump({ image: "ruby:2.1",
- services: ["mysql"],
- before_script: ["pwd"],
- rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
- services: [{ name: "postgresql", alias: "db-pg",
- entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
- script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
- services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
- command: ["/usr/local/bin/init", "run"] },
- { name: "docker:dind" }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
- end
-
- context "when etended docker configuration is not used" do
- it "returns image and service when defined" do
- config = YAML.dump({ image: "ruby:2.1",
- services: ["mysql", "docker:dind"],
- before_script: ["pwd"],
- rspec: { script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.1" },
- services: [{ name: "mysql" }, { name: "docker:dind" }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- it "returns image and service when overridden for job" do
- config = YAML.dump({ image: "ruby:2.1",
- services: ["mysql"],
- before_script: ["pwd"],
- rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.5" },
- services: [{ name: "postgresql" }, { name: "docker:dind" }]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
- end
- end
-
- describe 'Variables' do
- let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) }
-
- subject { config_processor.builds.first[:yaml_variables] }
-
- context 'when global variables are defined' do
- let(:variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
- let(:config) do
- {
- variables: variables,
- before_script: ['pwd'],
- rspec: { script: 'rspec' }
- }
- end
-
- it 'returns global variables' do
- expect(subject).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- end
- end
-
- context 'when job and global variables are defined' do
- let(:global_variables) do
- { 'VAR1' => 'global1', 'VAR3' => 'global3' }
- end
- let(:job_variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
- let(:config) do
- {
- before_script: ['pwd'],
- variables: global_variables,
- rspec: { script: 'rspec', variables: job_variables }
- }
- end
-
- it 'returns all unique variables' do
- expect(subject).to contain_exactly(
- { key: 'VAR3', value: 'global3', public: true },
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- end
- end
-
- context 'when job variables are defined' do
- let(:config) do
- {
- before_script: ['pwd'],
- rspec: { script: 'rspec', variables: variables }
- }
- end
-
- context 'when syntax is correct' do
- let(:variables) do
- { 'VAR1' => 'value1', 'VAR2' => 'value2' }
- end
-
- it 'returns job variables' do
- expect(subject).to contain_exactly(
- { key: 'VAR1', value: 'value1', public: true },
- { key: 'VAR2', value: 'value2', public: true }
- )
- end
- end
-
- context 'when syntax is incorrect' do
- context 'when variables defined but invalid' do
- let(:variables) do
- %w(VAR1 value1 VAR2 value2)
- end
-
- it 'raises error' do
- expect { subject }
- .to raise_error(GitlabCiYamlProcessor::ValidationError,
- /jobs:rspec:variables config should be a hash of key value pairs/)
- end
- end
-
- context 'when variables key defined but value not specified' do
- let(:variables) do
- nil
- end
-
- it 'returns empty array' do
- ##
- # When variables config is empty, we assume this is a valid
- # configuration, see issue #18775
- #
- expect(subject).to be_an_instance_of(Array)
- expect(subject).to be_empty
- end
- end
- end
- end
-
- context 'when job variables are not defined' do
- let(:config) do
- {
- before_script: ['pwd'],
- rspec: { script: 'rspec' }
- }
- end
-
- it 'returns empty array' do
- expect(subject).to be_an_instance_of(Array)
- expect(subject).to be_empty
- end
- end
- end
-
- describe "When" do
- %w(on_success on_failure always).each do |when_state|
- it "returns #{when_state} when defined" do
- config = YAML.dump({
- rspec: { script: "rspec", when: when_state }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- builds = config_processor.builds_for_stage_and_ref("test", "master")
- expect(builds.size).to eq(1)
- expect(builds.first[:when]).to eq(when_state)
- end
- end
- end
-
- describe 'cache' do
- context 'when cache definition has unknown keys' do
- it 'raises relevant validation error' do
- config = YAML.dump(
- { cache: { untracked: true, invalid: 'key' },
- rspec: { script: 'rspec' } })
-
- expect { GitlabCiYamlProcessor.new(config) }.to raise_error(
- GitlabCiYamlProcessor::ValidationError,
- 'cache config contains unknown keys: invalid'
- )
- end
- end
-
- it "returns cache when defined globally" do
- config = YAML.dump({
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
- rspec: {
- script: "rspec"
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
- paths: ["logs/", "binaries/"],
- untracked: true,
- key: 'key',
- policy: 'pull-push'
- )
- end
-
- it "returns cache when defined in a job" do
- config = YAML.dump({
- rspec: {
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
- script: "rspec"
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
- paths: ["logs/", "binaries/"],
- untracked: true,
- key: 'key',
- policy: 'pull-push'
- )
- end
-
- it "overwrite cache when defined for a job and globally" do
- config = YAML.dump({
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
- rspec: {
- script: "rspec",
- cache: { paths: ["test/"], untracked: false, key: 'local' }
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
- paths: ["test/"],
- untracked: false,
- key: 'local',
- policy: 'pull-push'
- )
- end
- end
-
- describe "Artifacts" do
- it "returns artifacts when defined" do
- config = YAML.dump({
- image: "ruby:2.1",
- services: ["mysql"],
- before_script: ["pwd"],
- rspec: {
- artifacts: {
- paths: ["logs/", "binaries/"],
- untracked: true,
- name: "custom_name",
- expire_in: "7d"
- },
- script: "rspec"
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config)
-
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"],
- image: { name: "ruby:2.1" },
- services: [{ name: "mysql" }],
- artifacts: {
- name: "custom_name",
- paths: ["logs/", "binaries/"],
- untracked: true,
- expire_in: "7d"
- }
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- end
-
- %w[on_success on_failure always].each do |when_state|
- it "returns artifacts for when #{when_state} defined" do
- config = YAML.dump({
- rspec: {
- script: "rspec",
- artifacts: { paths: ["logs/", "binaries/"], when: when_state }
- }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
-
- builds = config_processor.builds_for_stage_and_ref("test", "master")
- expect(builds.size).to eq(1)
- expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
- end
- end
- end
-
- describe '#environment' do
- let(:config) do
- {
- deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
- }
- end
-
- let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
- let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
-
- context 'when a production environment is specified' do
- let(:environment) { 'production' }
-
- it 'does return production' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to eq(environment)
- expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
- end
- end
-
- context 'when hash is specified' do
- let(:environment) do
- { name: 'production',
- url: 'http://production.gitlab.com' }
- end
-
- it 'does return production and URL' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to eq(environment[:name])
- expect(builds.first[:options]).to include(environment: environment)
- end
-
- context 'the url has a port as variable' do
- let(:environment) do
- { name: 'production',
- url: 'http://production.gitlab.com:$PORT' }
- end
-
- it 'allows a variable for the port' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to eq(environment[:name])
- expect(builds.first[:options]).to include(environment: environment)
- end
- end
- end
-
- context 'when no environment is specified' do
- let(:environment) { nil }
-
- it 'does return nil environment' do
- expect(builds.size).to eq(1)
- expect(builds.first[:environment]).to be_nil
- end
- end
-
- context 'is not a string' do
- let(:environment) { 1 }
-
- it 'raises error' do
- expect { builds }.to raise_error(
- 'jobs:deploy_to_production:environment config should be a hash or a string')
- end
- end
-
- context 'is not a valid string' do
- let(:environment) { 'production:staging' }
-
- it 'raises error' do
- expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
- end
- end
-
- context 'when on_stop is specified' do
- let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
- let(:config) { { review: review, close_review: close_review }.compact }
-
- context 'with matching job' do
- let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
-
- it 'does return a list of builds' do
- expect(builds.size).to eq(2)
- expect(builds.first[:environment]).to eq('review')
- end
- end
-
- context 'without matching job' do
- let(:close_review) { nil }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
- end
- end
-
- context 'with close job without environment' do
- let(:close_review) { { stage: 'deploy', script: 'test' } }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
- end
- end
-
- context 'with close job for different environment' do
- let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
- end
- end
-
- context 'with close job without stop action' do
- let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
-
- it 'raises error' do
- expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
- end
- end
- end
- end
-
- describe "Dependencies" do
- let(:config) do
- {
- build1: { stage: 'build', script: 'test' },
- build2: { stage: 'build', script: 'test' },
- test1: { stage: 'test', script: 'test', dependencies: dependencies },
- test2: { stage: 'test', script: 'test' },
- deploy: { stage: 'test', script: 'test' }
- }
- end
-
- subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
-
- context 'no dependencies' do
- let(:dependencies) { }
-
- it { expect { subject }.not_to raise_error }
- end
-
- context 'dependencies to builds' do
- let(:dependencies) { %w(build1 build2) }
-
- it { expect { subject }.not_to raise_error }
- end
-
- context 'dependencies to builds defined as symbols' do
- let(:dependencies) { [:build1, :build2] }
-
- it { expect { subject }.not_to raise_error }
- end
-
- context 'undefined dependency' do
- let(:dependencies) { ['undefined'] }
-
- it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
- end
-
- context 'dependencies to deploy' do
- let(:dependencies) { ['deploy'] }
-
- it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
- end
- end
-
- describe "Hidden jobs" do
- let(:config_processor) { GitlabCiYamlProcessor.new(config) }
- subject { config_processor.builds_for_stage_and_ref("test", "master") }
-
- shared_examples 'hidden_job_handling' do
- it "doesn't create jobs that start with dot" do
- expect(subject.size).to eq(1)
- expect(subject.first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "normal_job",
- commands: "test",
- coverage_regex: nil,
- tag_list: [],
- options: {
- script: ["test"]
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- end
- end
-
- context 'when hidden job have a script definition' do
- let(:config) do
- YAML.dump({
- '.hidden_job' => { image: 'ruby:2.1', script: 'test' },
- 'normal_job' => { script: 'test' }
- })
- end
-
- it_behaves_like 'hidden_job_handling'
- end
-
- context "when hidden job doesn't have a script definition" do
- let(:config) do
- YAML.dump({
- '.hidden_job' => { image: 'ruby:2.1' },
- 'normal_job' => { script: 'test' }
- })
- end
-
- it_behaves_like 'hidden_job_handling'
- end
- end
-
- describe "YAML Alias/Anchor" do
- let(:config_processor) { GitlabCiYamlProcessor.new(config) }
- subject { config_processor.builds_for_stage_and_ref("build", "master") }
-
- shared_examples 'job_templates_handling' do
- it "is correctly supported for jobs" do
- expect(subject.size).to eq(2)
- expect(subject.first).to eq({
- stage: "build",
- stage_idx: 0,
- name: "job1",
- commands: "execute-script-for-job",
- coverage_regex: nil,
- tag_list: [],
- options: {
- script: ["execute-script-for-job"]
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- expect(subject.second).to eq({
- stage: "build",
- stage_idx: 0,
- name: "job2",
- commands: "execute-script-for-job",
- coverage_regex: nil,
- tag_list: [],
- options: {
- script: ["execute-script-for-job"]
- },
- when: "on_success",
- allow_failure: false,
- environment: nil,
- yaml_variables: []
- })
- end
- end
-
- context 'when template is a job' do
- let(:config) do
- <<EOT
-job1: &JOBTMPL
- stage: build
- script: execute-script-for-job
-
-job2: *JOBTMPL
-EOT
- end
-
- it_behaves_like 'job_templates_handling'
- end
-
- context 'when template is a hidden job' do
- let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
- script: execute-script-for-job
-
-job1: *JOBTMPL
-
-job2: *JOBTMPL
-EOT
- end
-
- it_behaves_like 'job_templates_handling'
- end
-
- context 'when job adds its own keys to a template definition' do
- let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
-
-job1:
- <<: *JOBTMPL
- script: execute-script-for-job
-
-job2:
- <<: *JOBTMPL
- script: execute-script-for-job
-EOT
- end
-
- it_behaves_like 'job_templates_handling'
- end
- end
-
- describe "Error handling" do
- it "fails to parse YAML" do
- expect {GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
- end
-
- it "indicates that object is invalid" do
- expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
- end
-
- it "returns errors if tags parameter is invalid" do
- config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
- end
-
- it "returns errors if before_script parameter is invalid" do
- config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings")
- end
-
- it "returns errors if job before_script parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
- end
-
- it "returns errors if after_script parameter is invalid" do
- config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings")
- end
-
- it "returns errors if job after_script parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
- end
-
- it "returns errors if image parameter is invalid" do
- config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string")
- end
-
- it "returns errors if job name is blank" do
- config = YAML.dump({ '' => { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank")
- end
-
- it "returns errors if job name is non-string" do
- config = YAML.dump({ 10 => { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol")
- end
-
- it "returns errors if job image parameter is invalid" do
- config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
- end
-
- it "returns errors if services parameter is not an array" do
- config = YAML.dump({ services: "test", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array")
- end
-
- it "returns errors if services parameter is not an array of strings" do
- config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
- end
-
- it "returns errors if job services parameter is not an array" do
- config = YAML.dump({ rspec: { script: "test", services: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
- end
-
- it "returns errors if job services parameter is not an array of strings" do
- config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
- end
-
- it "returns error if job configuration is invalid" do
- config = YAML.dump({ extra: "bundle update" })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash")
- end
-
- it "returns errors if services configuration is not correct" do
- config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array")
- end
-
- it "returns errors if there are no jobs defined" do
- config = YAML.dump({ before_script: ["bundle update"] })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
- end
-
- it "returns errors if there are no visible jobs defined" do
- config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
- end
-
- it "returns errors if job allow_failure parameter is not an boolean" do
- config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
- end
-
- it "returns errors if job stage is not a string" do
- config = YAML.dump({ rspec: { script: "test", type: 1 } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
- end
-
- it "returns errors if job stage is not a pre-defined stage" do
- config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
- end
-
- it "returns errors if job stage is not a defined stage" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
- end
-
- it "returns errors if stages is not an array" do
- config = YAML.dump({ stages: "test", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
- end
-
- it "returns errors if stages is not an array of strings" do
- config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
- end
-
- it "returns errors if variables is not a map" do
- config = YAML.dump({ variables: "test", rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
- end
-
- it "returns errors if variables is not a map of key-value strings" do
- config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
- end
-
- it "returns errors if job when is not on_success, on_failure or always" do
- config = YAML.dump({ rspec: { script: "test", when: 1 } })
- expect do
- GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
- end
-
- it "returns errors if job artifacts:name is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
- end
-
- it "returns errors if job artifacts:when is not an a predefined value" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
- end
-
- it "returns errors if job artifacts:expire_in is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
- end
-
- it "returns errors if job artifacts:expire_in is not an a valid duration" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
- end
-
- it "returns errors if job artifacts:untracked is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
- end
-
- it "returns errors if job artifacts:paths is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
- end
-
- it "returns errors if cache:untracked is not an array of strings" do
- config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
- end
-
- it "returns errors if cache:paths is not an array of strings" do
- config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings")
- end
-
- it "returns errors if cache:key is not a string" do
- config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol")
- end
-
- it "returns errors if job cache:key is not an a string" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
- end
-
- it "returns errors if job cache:untracked is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
- end
-
- it "returns errors if job cache:paths is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
- end
-
- it "returns errors if job dependencies is not an array of strings" do
- config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
- expect do
- GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
- end
- end
-
- describe "Validate configuration templates" do
- templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")
-
- templates.each do |file|
- it "does not return errors for #{file}" do
- file = File.read(file)
-
- expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error
- end
- end
- end
-
- describe "#validation_message" do
- context "when the YAML could not be parsed" do
- it "returns an error about invalid configutaion" do
- content = YAML.dump("invalid: yaml: test")
-
- expect(GitlabCiYamlProcessor.validation_message(content))
- .to eq "Invalid configuration format"
- end
- end
-
- context "when the tags parameter is invalid" do
- it "returns an error about invalid tags" do
- content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
-
- expect(GitlabCiYamlProcessor.validation_message(content))
- .to eq "jobs:rspec tags should be an array of strings"
- end
- end
-
- context "when YAML content is empty" do
- it "returns an error about missing content" do
- expect(GitlabCiYamlProcessor.validation_message(''))
- .to eq "Please provide content of .gitlab-ci.yml"
- end
- end
-
- context "when the YAML is valid" do
- it "does not return any errors" do
- content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
-
- expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
index b155c20d8d3..cb52d971047 100644
--- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
@@ -215,9 +215,17 @@ end
# to a specific version of the database where said table is still present.
#
describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do
+ let(:user_class) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'users'
+ end
+ end
+
let(:migration) { described_class.new }
- let(:project) { create(:project_empty_repo) }
- let(:author) { create(:user) }
+ let(:user_class) { table(:users) }
+ let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) }
+ let(:namespace) { create(:namespace, owner: author) }
+ let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) }
# We can not rely on FactoryGirl as the state of Event may change in ways that
# the background migration does not expect, hence we use the Event class of
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index e49ecadde20..e6645985ba4 100644
--- a/spec/lib/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::Ansi2html do
+describe Gitlab::Ci::Ansi2html do
subject { described_class }
it "prints non-ansi as-is" do
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb
index f0769deef21..f8188675013 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/gitlab/ci/charts_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Ci::Charts do
+describe Gitlab::Ci::Charts do
context "pipeline_times" do
let(:project) { create(:project) }
- let(:chart) { Ci::Charts::PipelineTime.new(project) }
+ let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) }
subject { chart.pipeline_times }
diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb
index f7b753b022b..3789a142248 100644
--- a/spec/lib/ci/mask_secret_spec.rb
+++ b/spec/lib/gitlab/ci/mask_secret_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::MaskSecret do
+describe Gitlab::Ci::MaskSecret do
subject { described_class }
describe '#mask' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
new file mode 100644
index 00000000000..2278230f338
--- /dev/null
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -0,0 +1,1699 @@
+require 'spec_helper'
+
+module Gitlab
+ module Ci
+ describe YamlProcessor, :lib do
+ subject { described_class.new(config, path) }
+ let(:path) { 'path' }
+
+ describe 'our current .gitlab-ci.yml' do
+ let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
+
+ it 'is valid' do
+ error_message = described_class.validation_message(config)
+
+ expect(error_message).to be_nil
+ end
+ end
+
+ describe '#build_attributes' do
+ subject { described_class.new(config, path).build_attributes(:rspec) }
+
+ describe 'coverage entry' do
+ describe 'code coverage regexp' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ coverage: '/Code coverage: \d+\.\d+/' })
+ end
+
+ it 'includes coverage regexp in build attributes' do
+ expect(subject)
+ .to include(coverage_regex: 'Code coverage: \d+\.\d+')
+ end
+ end
+ end
+
+ describe 'retry entry' do
+ context 'when retry count is specified' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec', retry: 1 })
+ end
+
+ it 'includes retry count in build options attribute' do
+ expect(subject[:options]).to include(retry: 1)
+ end
+ end
+
+ context 'when retry count is not specified' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
+ end
+
+ it 'does not persist retry count in the database' do
+ expect(subject[:options]).not_to have_key(:retry)
+ end
+ end
+ end
+
+ describe 'allow failure entry' do
+ context 'when job is a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual' })
+ end
+
+ it 'is allowed to fail' do
+ expect(subject[:allow_failure]).to be true
+ end
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe '#stage_seeds' do
+ context 'when no refs policy is specified' do
+ let(:config) do
+ YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
+ rspec: { stage: 'test', script: 'rspec' },
+ spinach: { stage: 'test', script: 'spinach' })
+ end
+
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ it 'correctly fabricates a stage seeds object' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 2
+ expect(seeds.first.stage[:name]).to eq 'test'
+ expect(seeds.second.stage[:name]).to eq 'deploy'
+ expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
+ expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
+ expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ end
+ end
+
+ context 'when refs policy is specified' do
+ let(:config) do
+ YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
+ end
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, ref: 'feature', tag: true)
+ end
+
+ it 'returns stage seeds only assigned to master to master' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.stage[:name]).to eq 'test'
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ end
+ end
+
+ context 'when source policy is specified' do
+ let(:config) do
+ YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
+ end
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, source: :schedule)
+ end
+
+ it 'returns stage seeds only assigned to schedules' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.stage[:name]).to eq 'test'
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ end
+ end
+
+ context 'when kubernetes policy is specified' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ let(:config) do
+ YAML.dump(
+ spinach: { stage: 'test', script: 'spinach' },
+ production: {
+ stage: 'deploy',
+ script: 'cap',
+ only: { kubernetes: 'active' }
+ }
+ )
+ end
+
+ context 'when kubernetes is active' do
+ let(:project) { create(:kubernetes_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 2
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ end
+ end
+
+ context 'when kubernetes is not active' do
+ it 'does not return seeds for kubernetes dependent job' do
+ seeds = subject.stage_seeds(pipeline)
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ end
+ end
+ end
+ end
+
+ describe "#builds_for_stage_and_ref" do
+ let(:type) { 'test' }
+
+ it "returns builds if no branch specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec" }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ describe 'only' do
+ it "does not return builds if only has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["deploy"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
+
+ it "does not return builds if only has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["/^deploy$/"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
+
+ it "returns builds if only has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["master"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "returns builds if only has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: %w(master deploy) }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "returns builds if only has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if only has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["tags"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if only has special keywords specified and source matches" do
+ possibilities = [{ keyword: 'pushes', source: 'push' },
+ { keyword: 'web', source: 'web' },
+ { keyword: 'triggers', source: 'trigger' },
+ { keyword: 'schedules', source: 'schedule' },
+ { keyword: 'api', source: 'api' },
+ { keyword: 'external', source: 'external' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
+ end
+ end
+
+ it "does not return builds if only has special keywords specified and source doesn't match" do
+ possibilities = [{ keyword: 'pushes', source: 'web' },
+ { keyword: 'web', source: 'push' },
+ { keyword: 'triggers', source: 'schedule' },
+ { keyword: 'schedules', source: 'external' },
+ { keyword: 'api', source: 'trigger' },
+ { keyword: 'external', source: 'api' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
+ end
+ end
+
+ it "returns builds if only has current repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches@path"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if only has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches@fork"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns build only for specified type" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", only: %w(master deploy) },
+ staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
+ production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork')
+
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
+ end
+
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ context 'when it is integer' do
+ let(:only) { 1 }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only has to be either an array of conditions or a hash')
+ end
+ end
+
+ context 'when it is an array of integers' do
+ let(:only) { [1, 1] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is invalid regex' do
+ let(:only) { ["/*invalid/"] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
+ end
+ end
+ end
+ end
+
+ describe 'except' do
+ it "returns builds if except has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["deploy"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "returns builds if except has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["/^deploy$/"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "does not return builds if except has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["master"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
+
+ it "does not return builds if except has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: %w(master deploy) }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "does not return builds if except has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if except has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["tags"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if except has special keywords specified and source matches" do
+ possibilities = [{ keyword: 'pushes', source: 'push' },
+ { keyword: 'web', source: 'web' },
+ { keyword: 'triggers', source: 'trigger' },
+ { keyword: 'schedules', source: 'schedule' },
+ { keyword: 'api', source: 'api' },
+ { keyword: 'external', source: 'external' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
+ end
+ end
+
+ it "returns builds if except has special keywords specified and source doesn't match" do
+ possibilities = [{ keyword: 'pushes', source: 'web' },
+ { keyword: 'web', source: 'push' },
+ { keyword: 'triggers', source: 'schedule' },
+ { keyword: 'schedules', source: 'external' },
+ { keyword: 'api', source: 'trigger' },
+ { keyword: 'external', source: 'api' }]
+
+ possibilities.each do |possibility|
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
+ end
+ end
+
+ it "does not return builds if except has current repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches@path"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if except has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches@fork"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "returns build except specified type" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
+ staging: { script: "deploy", type: "deploy", except: ["master"] },
+ production: { script: "deploy", type: "deploy", except: ["master@fork"] }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork')
+
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
+ expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
+ end
+
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", except: except } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ context 'when it is integer' do
+ let(:except) { 1 }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except has to be either an array of conditions or a hash')
+ end
+ end
+
+ context 'when it is an array of integers' do
+ let(:except) { [1, 1] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is invalid regex' do
+ let(:except) { ["/*invalid/"] }
+
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
+ end
+ end
+ end
+ end
+ end
+
+ describe "Scripts handling" do
+ let(:config_data) { YAML.dump(config) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data, path) }
+
+ subject { config_processor.builds_for_stage_and_ref("test", "master").first }
+
+ describe "before_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("global script\nscript")
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { before_script: ["local script"], script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("local script\nscript")
+ end
+ end
+ end
+
+ describe "script" do
+ let(:config) do
+ {
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("script")
+ end
+ end
+
+ describe "after_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ after_script: ["after_script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["after_script"])
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ after_script: ["local after_script"],
+ test: { after_script: ["local after_script"], script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["local after_script"])
+ end
+ end
+ end
+ end
+
+ describe "Image and service handling" do
+ context "when extended docker configuration is used" do
+ it "returns image and service when defined" do
+ config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: ["mysql", { name: "docker:dind", alias: "docker",
+ entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }],
+ before_script: ["pwd"],
+ rspec: { script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "mysql" },
+ { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ it "returns image and service when overridden for job" do
+ config = YAML.dump({ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "postgresql", alias: "db-pg",
+ entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
+ script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
+ services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
+ command: ["/usr/local/bin/init", "run"] },
+ { name: "docker:dind" }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
+ context "when etended docker configuration is not used" do
+ it "returns image and service when defined" do
+ config = YAML.dump({ image: "ruby:2.1",
+ services: ["mysql", "docker:dind"],
+ before_script: ["pwd"],
+ rspec: { script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.1" },
+ services: [{ name: "mysql" }, { name: "docker:dind" }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ it "returns image and service when overridden for job" do
+ config = YAML.dump({ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.5" },
+ services: [{ name: "postgresql" }, { name: "docker:dind" }]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+ end
+
+ describe 'Variables' do
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), path) }
+
+ subject { config_processor.builds.first[:yaml_variables] }
+
+ context 'when global variables are defined' do
+ let(:variables) do
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ end
+ let(:config) do
+ {
+ variables: variables,
+ before_script: ['pwd'],
+ rspec: { script: 'rspec' }
+ }
+ end
+
+ it 'returns global variables' do
+ expect(subject).to contain_exactly(
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
+ )
+ end
+ end
+
+ context 'when job and global variables are defined' do
+ let(:global_variables) do
+ { 'VAR1' => 'global1', 'VAR3' => 'global3' }
+ end
+ let(:job_variables) do
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ end
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ variables: global_variables,
+ rspec: { script: 'rspec', variables: job_variables }
+ }
+ end
+
+ it 'returns all unique variables' do
+ expect(subject).to contain_exactly(
+ { key: 'VAR3', value: 'global3', public: true },
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
+ )
+ end
+ end
+
+ context 'when job variables are defined' do
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ rspec: { script: 'rspec', variables: variables }
+ }
+ end
+
+ context 'when syntax is correct' do
+ let(:variables) do
+ { 'VAR1' => 'value1', 'VAR2' => 'value2' }
+ end
+
+ it 'returns job variables' do
+ expect(subject).to contain_exactly(
+ { key: 'VAR1', value: 'value1', public: true },
+ { key: 'VAR2', value: 'value2', public: true }
+ )
+ end
+ end
+
+ context 'when syntax is incorrect' do
+ context 'when variables defined but invalid' do
+ let(:variables) do
+ %w(VAR1 value1 VAR2 value2)
+ end
+
+ it 'raises error' do
+ expect { subject }
+ .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ /jobs:rspec:variables config should be a hash of key value pairs/)
+ end
+ end
+
+ context 'when variables key defined but value not specified' do
+ let(:variables) do
+ nil
+ end
+
+ it 'returns empty array' do
+ ##
+ # When variables config is empty, we assume this is a valid
+ # configuration, see issue #18775
+ #
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when job variables are not defined' do
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ rspec: { script: 'rspec' }
+ }
+ end
+
+ it 'returns empty array' do
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ describe "When" do
+ %w(on_success on_failure always).each do |when_state|
+ it "returns #{when_state} when defined" do
+ config = YAML.dump({
+ rspec: { script: "rspec", when: when_state }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ builds = config_processor.builds_for_stage_and_ref("test", "master")
+ expect(builds.size).to eq(1)
+ expect(builds.first[:when]).to eq(when_state)
+ end
+ end
+ end
+
+ describe 'cache' do
+ context 'when cache definition has unknown keys' do
+ it 'raises relevant validation error' do
+ config = YAML.dump(
+ { cache: { untracked: true, invalid: 'key' },
+ rspec: { script: 'rspec' } })
+
+ expect { Gitlab::Ci::YamlProcessor.new(config) }.to raise_error(
+ Gitlab::Ci::YamlProcessor::ValidationError,
+ 'cache config contains unknown keys: invalid'
+ )
+ end
+ end
+
+ it "returns cache when defined globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
+ rspec: {
+ script: "rspec"
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ key: 'key',
+ policy: 'pull-push'
+ )
+ end
+
+ it "returns cache when defined in a job" do
+ config = YAML.dump({
+ rspec: {
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
+ script: "rspec"
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ key: 'key',
+ policy: 'pull-push'
+ )
+ end
+
+ it "overwrite cache when defined for a job and globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
+ rspec: {
+ script: "rspec",
+ cache: { paths: ["test/"], untracked: false, key: 'local' }
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["test/"],
+ untracked: false,
+ key: 'local',
+ policy: 'pull-push'
+ )
+ end
+ end
+
+ describe "Artifacts" do
+ it "returns artifacts when defined" do
+ config = YAML.dump({
+ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: {
+ artifacts: {
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ name: "custom_name",
+ expire_in: "7d"
+ },
+ script: "rspec"
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"],
+ image: { name: "ruby:2.1" },
+ services: [{ name: "mysql" }],
+ artifacts: {
+ name: "custom_name",
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ expire_in: "7d"
+ }
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+
+ %w[on_success on_failure always].each do |when_state|
+ it "returns artifacts for when #{when_state} defined" do
+ config = YAML.dump({
+ rspec: {
+ script: "rspec",
+ artifacts: { paths: ["logs/", "binaries/"], when: when_state }
+ }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+
+ builds = config_processor.builds_for_stage_and_ref("test", "master")
+ expect(builds.size).to eq(1)
+ expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
+ end
+ end
+ end
+
+ describe '#environment' do
+ let(:config) do
+ {
+ deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
+ }
+ end
+
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+ let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
+
+ context 'when a production environment is specified' do
+ let(:environment) { 'production' }
+
+ it 'does return production' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment)
+ expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
+ end
+ end
+
+ context 'when hash is specified' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com' }
+ end
+
+ it 'does return production and URL' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
+ end
+
+ context 'the url has a port as variable' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com:$PORT' }
+ end
+
+ it 'allows a variable for the port' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
+ end
+ end
+ end
+
+ context 'when no environment is specified' do
+ let(:environment) { nil }
+
+ it 'does return nil environment' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to be_nil
+ end
+ end
+
+ context 'is not a string' do
+ let(:environment) { 1 }
+
+ it 'raises error' do
+ expect { builds }.to raise_error(
+ 'jobs:deploy_to_production:environment config should be a hash or a string')
+ end
+ end
+
+ context 'is not a valid string' do
+ let(:environment) { 'production:staging' }
+
+ it 'raises error' do
+ expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
+ end
+ end
+
+ context 'when on_stop is specified' do
+ let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
+ let(:config) { { review: review, close_review: close_review }.compact }
+
+ context 'with matching job' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
+
+ it 'does return a list of builds' do
+ expect(builds.size).to eq(2)
+ expect(builds.first[:environment]).to eq('review')
+ end
+ end
+
+ context 'without matching job' do
+ let(:close_review) { nil }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
+ end
+ end
+
+ context 'with close job without environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
+ end
+ end
+
+ context 'with close job for different environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
+ end
+ end
+
+ context 'with close job without stop action' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
+ end
+ end
+ end
+ end
+
+ describe "Dependencies" do
+ let(:config) do
+ {
+ build1: { stage: 'build', script: 'test' },
+ build2: { stage: 'build', script: 'test' },
+ test1: { stage: 'test', script: 'test', dependencies: dependencies },
+ test2: { stage: 'test', script: 'test' },
+ deploy: { stage: 'test', script: 'test' }
+ }
+ end
+
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ context 'no dependencies' do
+ let(:dependencies) { }
+
+ it { expect { subject }.not_to raise_error }
+ end
+
+ context 'dependencies to builds' do
+ let(:dependencies) { %w(build1 build2) }
+
+ it { expect { subject }.not_to raise_error }
+ end
+
+ context 'dependencies to builds defined as symbols' do
+ let(:dependencies) { [:build1, :build2] }
+
+ it { expect { subject }.not_to raise_error }
+ end
+
+ context 'undefined dependency' do
+ let(:dependencies) { ['undefined'] }
+
+ it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
+ end
+
+ context 'dependencies to deploy' do
+ let(:dependencies) { ['deploy'] }
+
+ it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
+ end
+ end
+
+ describe "Hidden jobs" do
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
+ subject { config_processor.builds_for_stage_and_ref("test", "master") }
+
+ shared_examples 'hidden_job_handling' do
+ it "doesn't create jobs that start with dot" do
+ expect(subject.size).to eq(1)
+ expect(subject.first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "normal_job",
+ commands: "test",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ script: ["test"]
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
+ context 'when hidden job have a script definition' do
+ let(:config) do
+ YAML.dump({
+ '.hidden_job' => { image: 'ruby:2.1', script: 'test' },
+ 'normal_job' => { script: 'test' }
+ })
+ end
+
+ it_behaves_like 'hidden_job_handling'
+ end
+
+ context "when hidden job doesn't have a script definition" do
+ let(:config) do
+ YAML.dump({
+ '.hidden_job' => { image: 'ruby:2.1' },
+ 'normal_job' => { script: 'test' }
+ })
+ end
+
+ it_behaves_like 'hidden_job_handling'
+ end
+ end
+
+ describe "YAML Alias/Anchor" do
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
+ subject { config_processor.builds_for_stage_and_ref("build", "master") }
+
+ shared_examples 'job_templates_handling' do
+ it "is correctly supported for jobs" do
+ expect(subject.size).to eq(2)
+ expect(subject.first).to eq({
+ stage: "build",
+ stage_idx: 0,
+ name: "job1",
+ commands: "execute-script-for-job",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ script: ["execute-script-for-job"]
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ expect(subject.second).to eq({
+ stage: "build",
+ stage_idx: 0,
+ name: "job2",
+ commands: "execute-script-for-job",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ script: ["execute-script-for-job"]
+ },
+ when: "on_success",
+ allow_failure: false,
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
+ context 'when template is a job' do
+ let(:config) do
+ <<EOT
+job1: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
+
+job2: *JOBTMPL
+EOT
+ end
+
+ it_behaves_like 'job_templates_handling'
+ end
+
+ context 'when template is a hidden job' do
+ let(:config) do
+ <<EOT
+.template: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
+
+job1: *JOBTMPL
+
+job2: *JOBTMPL
+EOT
+ end
+
+ it_behaves_like 'job_templates_handling'
+ end
+
+ context 'when job adds its own keys to a template definition' do
+ let(:config) do
+ <<EOT
+.template: &JOBTMPL
+ stage: build
+
+job1:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+
+job2:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+EOT
+ end
+
+ it_behaves_like 'job_templates_handling'
+ end
+ end
+
+ describe "Error handling" do
+ it "fails to parse YAML" do
+ expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
+ end
+
+ it "indicates that object is invalid" do
+ expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
+ end
+
+ it "returns errors if tags parameter is invalid" do
+ config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
+ end
+
+ it "returns errors if before_script parameter is invalid" do
+ config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings")
+ end
+
+ it "returns errors if job before_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
+ end
+
+ it "returns errors if after_script parameter is invalid" do
+ config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings")
+ end
+
+ it "returns errors if job after_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
+ end
+
+ it "returns errors if image parameter is invalid" do
+ config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string")
+ end
+
+ it "returns errors if job name is blank" do
+ config = YAML.dump({ '' => { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank")
+ end
+
+ it "returns errors if job name is non-string" do
+ config = YAML.dump({ 10 => { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol")
+ end
+
+ it "returns errors if job image parameter is invalid" do
+ config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
+ end
+
+ it "returns errors if services parameter is not an array" do
+ config = YAML.dump({ services: "test", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array")
+ end
+
+ it "returns errors if services parameter is not an array of strings" do
+ config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end
+
+ it "returns errors if job services parameter is not an array" do
+ config = YAML.dump({ rspec: { script: "test", services: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
+ end
+
+ it "returns errors if job services parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end
+
+ it "returns error if job configuration is invalid" do
+ config = YAML.dump({ extra: "bundle update" })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash")
+ end
+
+ it "returns errors if services configuration is not correct" do
+ config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array")
+ end
+
+ it "returns errors if there are no jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"] })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ end
+
+ it "returns errors if there are no visible jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ end
+
+ it "returns errors if job allow_failure parameter is not an boolean" do
+ config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
+ end
+
+ it "returns errors if job stage is not a string" do
+ config = YAML.dump({ rspec: { script: "test", type: 1 } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
+ end
+
+ it "returns errors if job stage is not a pre-defined stage" do
+ config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end
+
+ it "returns errors if job stage is not a defined stage" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
+ end
+
+ it "returns errors if stages is not an array" do
+ config = YAML.dump({ stages: "test", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
+ end
+
+ it "returns errors if stages is not an array of strings" do
+ config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
+ end
+
+ it "returns errors if variables is not a map" do
+ config = YAML.dump({ variables: "test", rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
+ end
+
+ it "returns errors if variables is not a map of key-value strings" do
+ config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
+ end
+
+ it "returns errors if job when is not on_success, on_failure or always" do
+ config = YAML.dump({ rspec: { script: "test", when: 1 } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config, path)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
+ end
+
+ it "returns errors if job artifacts:name is not an a string" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
+ end
+
+ it "returns errors if job artifacts:when is not an a predefined value" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a string" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a valid duration" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
+ end
+
+ it "returns errors if job artifacts:untracked is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
+ end
+
+ it "returns errors if job artifacts:paths is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
+ end
+
+ it "returns errors if cache:untracked is not an array of strings" do
+ config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
+ end
+
+ it "returns errors if cache:paths is not an array of strings" do
+ config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:paths config should be an array of strings")
+ end
+
+ it "returns errors if cache:key is not a string" do
+ config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol")
+ end
+
+ it "returns errors if job cache:key is not an a string" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
+ end
+
+ it "returns errors if job cache:untracked is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
+ end
+
+ it "returns errors if job cache:paths is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
+ end
+
+ it "returns errors if job dependencies is not an array of strings" do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
+ end
+ end
+
+ describe "Validate configuration templates" do
+ templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")
+
+ templates.each do |file|
+ it "does not return errors for #{file}" do
+ file = File.read(file)
+
+ expect { Gitlab::Ci::YamlProcessor.new(file) }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#validation_message" do
+ context "when the YAML could not be parsed" do
+ it "returns an error about invalid configutaion" do
+ content = YAML.dump("invalid: yaml: test")
+
+ expect(Gitlab::Ci::YamlProcessor.validation_message(content))
+ .to eq "Invalid configuration format"
+ end
+ end
+
+ context "when the tags parameter is invalid" do
+ it "returns an error about invalid tags" do
+ content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+
+ expect(Gitlab::Ci::YamlProcessor.validation_message(content))
+ .to eq "jobs:rspec tags should be an array of strings"
+ end
+ end
+
+ context "when YAML content is empty" do
+ it "returns an error about missing content" do
+ expect(Gitlab::Ci::YamlProcessor.validation_message(''))
+ .to eq "Please provide content of .gitlab-ci.yml"
+ end
+ end
+
+ context "when the YAML is valid" do
+ it "does not return any errors" do
+ content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+
+ expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 14d64d8c4da..46e968cc398 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -401,7 +401,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe '#stats' do
+ shared_examples '#stats' do
subject { commit.stats }
describe '#additions' do
@@ -415,6 +415,14 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
+ describe '#stats with gitaly on' do
+ it_should_behave_like '#stats'
+ end
+
+ describe '#stats with gitaly disabled', skip_gitaly_mock: true do
+ it_should_behave_like '#stats'
+ end
+
describe '#to_diff' do
subject { commit.to_diff }
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index f32fe5d8150..ec3abcb0953 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -165,4 +165,29 @@ describe Gitlab::GitalyClient::CommitService do
expect(subject).to eq("my diff")
end
end
+
+ describe '#commit_stats' do
+ let(:request) do
+ Gitaly::CommitStatsRequest.new(
+ repository: repository_message, revision: revision
+ )
+ end
+ let(:response) do
+ Gitaly::CommitStatsResponse.new(
+ oid: revision,
+ additions: 11,
+ deletions: 15
+ )
+ end
+
+ subject { described_class.new(repository).commit_stats(revision) }
+
+ it 'sends an RPC request' do
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commit_stats)
+ .with(request, kind_of(Hash)).and_return(response)
+
+ expect(subject.additions).to eq(11)
+ expect(subject.deletions).to eq(15)
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 8e3554375e8..d9b86e1bf34 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -119,7 +119,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
it 'has no when YML attributes but only the DB column' do
allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
- expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes)
+ expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
saved_project_json
end
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
new file mode 100644
index 00000000000..ecacea6bb35
--- /dev/null
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Themes, lib: true do
+ describe '.body_classes' do
+ it 'returns a space-separated list of class names' do
+ css = described_class.body_classes
+
+ expect(css).to include('ui_indigo')
+ expect(css).to include(' ui_dark ')
+ expect(css).to include(' ui_blue')
+ end
+ end
+
+ describe '.by_id' do
+ it 'returns a Theme by its ID' do
+ expect(described_class.by_id(1).name).to eq 'Indigo'
+ expect(described_class.by_id(3).name).to eq 'Light'
+ end
+ end
+
+ describe '.default' do
+ it 'returns the default application theme' do
+ allow(described_class).to receive(:default_id).and_return(2)
+ expect(described_class.default.id).to eq 2
+ end
+
+ it 'prevents an infinite loop when configuration default is invalid' do
+ default = described_class::APPLICATION_DEFAULT
+ themes = described_class::THEMES
+
+ config = double(default_theme: 0).as_null_object
+ allow(Gitlab).to receive(:config).and_return(config)
+ expect(described_class.default.id).to eq default
+
+ config = double(default_theme: themes.size + 5).as_null_object
+ allow(Gitlab).to receive(:config).and_return(config)
+ expect(described_class.default.id).to eq default
+ end
+ end
+
+ describe '.each' do
+ it 'passes the block to the THEMES Array' do
+ ids = []
+ described_class.each { |theme| ids << theme.id }
+ expect(ids).not_to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 68429d792f2..c7d9f105f04 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -40,9 +40,13 @@ describe Gitlab::UsageData do
ci_builds
ci_internal_pipelines
ci_external_pipelines
+ ci_pipeline_config_auto_devops
+ ci_pipeline_config_repository
ci_runners
ci_triggers
ci_pipeline_schedules
+ auto_devops_enabled
+ auto_devops_disabled
deploy_keys
deployments
environments
diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
index 1396d12e5a9..759e77ac9db 100644
--- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
+++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns')
describe ConvertCustomNotificationSettingsToColumns, :migration do
+ let(:user_class) { table(:users) }
+
let(:settings_params) do
[
{ level: 0, events: [:new_note] }, # disabled, single event
@@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
- user = create(:user)
+ user = build(:user).becomes(user_class).tap(&:save!)
create_params = { user_id: user.id, level: params[:level], events: events }
notification_setting = described_class::NotificationSetting.create(create_params)
@@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
- user = create(:user)
+ user = build(:user).becomes(user_class).tap(&:save!)
create_params = events.merge(user_id: user.id, level: params[:level])
notification_setting = described_class::NotificationSetting.create(create_params)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3ba01313efb..c1affa812aa 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -716,6 +716,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
+ expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
expect(user.external).to be_falsey
end
end
@@ -726,6 +727,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
+ expect(user.theme_id).to eq(1)
end
end
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index cd55d63125e..688175369ae 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "color_scheme_id":2,
+ "theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index cd55d63125e..ce8dfe5ae75 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "color_scheme_id":2,
+ "theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
@@ -17,4 +17,4 @@
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
-}
+} \ No newline at end of file
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
index f2c19c7642a..7724d54c569 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -4,7 +4,7 @@ describe 'ci/lints/show' do
include Devise::Test::ControllerHelpers
describe 'XSS protection' do
- let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
before do
assign(:status, true)
assign(:builds, config_processor.builds)
@@ -59,7 +59,7 @@ describe 'ci/lints/show' do
}
end
- let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
context 'when the content is valid' do
before do