diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index 6a0c6cba41..ffc37ad617 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -1,14 +1,14 @@
- if show_last_push_widget?(event)
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
- %strong= event.ref_name
- %span at
- %strong= link_to_project event.project
- #{time_ago_with_tooltip(event.created_at)}
+ .gray-content-block.clear-block
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ %strong= event.ref_name
+ %span at
+ %strong= link_to_project event.project
+ #{time_ago_with_tooltip(event.created_at)}
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
- Create Merge Request
- %hr
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
+ Create Merge Request
diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml
new file mode 100644
index 0000000000..24c68a6dbf
--- /dev/null
+++ b/app/views/layouts/ci/_info.html.haml
@@ -0,0 +1,2 @@
+- if current_user && current_user.is_admin? && Ci::Runner.count.zero?
+ = render 'ci/shared/no_runners'
diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml
new file mode 100644
index 0000000000..c987ab876a
--- /dev/null
+++ b/app/views/layouts/ci/_nav_admin.html.haml
@@ -0,0 +1,33 @@
+%ul.nav.nav-sidebar
+ = nav_link do
+ = link_to ci_root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = icon('caret-square-o-left fw')
+ %span
+ Back to Dashboard
+
+ %li.separate-item
+ = nav_link path: 'projects#index' do
+ = link_to ci_admin_projects_path do
+ %i.fa.fa-list-alt
+ Projects
+ = nav_link path: 'events#index' do
+ = link_to ci_admin_events_path do
+ %i.fa.fa-book
+ Events
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to ci_admin_runners_path do
+ %i.fa.fa-cog
+ Runners
+ %small.pull-right
+ = Ci::Runner.count(:all)
+ = nav_link path: 'builds#index' do
+ = link_to ci_admin_builds_path do
+ %i.fa.fa-link
+ Builds
+ %small.pull-right
+ = Ci::Build.count(:all)
+ = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
+ = link_to ci_admin_application_settings_path do
+ %i.fa.fa-cogs
+ %span
+ Settings
diff --git a/app/views/layouts/ci/_nav_dashboard.html.haml b/app/views/layouts/ci/_nav_dashboard.html.haml
new file mode 100644
index 0000000000..fcff405d19
--- /dev/null
+++ b/app/views/layouts/ci/_nav_dashboard.html.haml
@@ -0,0 +1,24 @@
+%ul.nav.nav-sidebar
+ = nav_link do
+ = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = icon('caret-square-o-left fw')
+ %span
+ Back to GitLab
+ %li.separate-item
+ = nav_link path: 'projects#index' do
+ = link_to ci_root_path do
+ %i.fa.fa-home
+ %span
+ Projects
+ - if current_user && current_user.is_admin?
+ %li
+ = link_to ci_admin_projects_path do
+ %i.fa.fa-cogs
+ %span
+ Admin
+ %li
+ = link_to ci_help_path do
+ %i.fa.fa-info
+ %span
+ Help
+
diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml
new file mode 100644
index 0000000000..d747679c8c
--- /dev/null
+++ b/app/views/layouts/ci/_nav_project.html.haml
@@ -0,0 +1,53 @@
+%ul.nav.nav-sidebar
+ = nav_link do
+ = link_to ci_root_path, title: 'Back to CI projects', data: {placement: 'right'}, class: 'back-link' do
+ = icon('caret-square-o-left fw')
+ %span= 'Back to CI projects'
+ %li.separate-item
+ = nav_link path: ['projects#show', 'commits#show', 'builds#show'] do
+ = link_to ci_project_path(@project) do
+ %i.fa.fa-list-alt
+ %span
+ Commits
+ %small.pull-right= @project.commits.count
+ = nav_link path: 'charts#show' do
+ = link_to ci_project_charts_path(@project) do
+ %i.fa.fa-bar-chart
+ %span
+ Charts
+ = nav_link path: ['runners#index', 'runners#show', 'runners#edit'] do
+ = link_to ci_project_runners_path(@project) do
+ %i.fa.fa-cog
+ %span
+ Runners
+ = nav_link path: 'variables#show' do
+ = link_to ci_project_variables_path(@project) do
+ %i.fa.fa-code
+ %span
+ Variables
+ = nav_link path: 'web_hooks#index' do
+ = link_to ci_project_web_hooks_path(@project) do
+ %i.fa.fa-link
+ %span
+ Web Hooks
+ = nav_link path: 'triggers#index' do
+ = link_to ci_project_triggers_path(@project) do
+ %i.fa.fa-retweet
+ %span
+ Triggers
+ = nav_link path: ['services#index', 'services#edit'] do
+ = link_to ci_project_services_path(@project) do
+ %i.fa.fa-share
+ %span
+ Services
+ = nav_link path: 'events#index' do
+ = link_to ci_project_events_path(@project) do
+ %i.fa.fa-book
+ %span
+ Events
+ %li.separate-item
+ = nav_link path: 'projects#edit' do
+ = link_to edit_ci_project_path(@project) do
+ %i.fa.fa-cogs
+ %span
+ Settings
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
new file mode 100644
index 0000000000..c598f63c4c
--- /dev/null
+++ b/app/views/layouts/ci/_page.html.haml
@@ -0,0 +1,26 @@
+.page-with-sidebar{ class: nav_sidebar_class }
+ = render "layouts/broadcast"
+ .sidebar-wrapper.nicescroll
+ .header-logo
+ = link_to ci_root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = brand_header_logo
+ .gitlab-text-container
+ %h3 GitLab CI
+ - if defined?(sidebar) && sidebar
+ = render "layouts/ci/#{sidebar}"
+ - elsif current_user
+ = render 'layouts/nav/dashboard'
+ .collapse-nav
+ = render partial: 'layouts/collapse_button'
+ - if current_user
+ = link_to current_user, class: 'sidebar-user' do
+ = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36'
+ .username
+ = current_user.username
+ .content-wrapper
+ = render "layouts/flash"
+ = render 'layouts/ci/info'
+ %div{ class: container_class }
+ .content
+ .clearfix
+ = yield
diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml
new file mode 100644
index 0000000000..c8cb185d28
--- /dev/null
+++ b/app/views/layouts/ci/admin.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title = "Admin area"
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page', sidebar: 'nav_admin'
diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml
new file mode 100644
index 0000000000..b9f871d544
--- /dev/null
+++ b/app/views/layouts/ci/application.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title = "CI Projects"
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page', sidebar: 'nav_dashboard'
diff --git a/app/views/layouts/ci/build.html.haml b/app/views/layouts/ci/build.html.haml
new file mode 100644
index 0000000000..a1356f0dc2
--- /dev/null
+++ b/app/views/layouts/ci/build.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title ci_commit_title(@commit)
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/ci/commit.html.haml b/app/views/layouts/ci/commit.html.haml
new file mode 100644
index 0000000000..a1356f0dc2
--- /dev/null
+++ b/app/views/layouts/ci/commit.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title ci_commit_title(@commit)
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml
new file mode 100644
index 0000000000..270b206df5
--- /dev/null
+++ b/app/views/layouts/ci/notify.html.haml
@@ -0,0 +1,19 @@
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"}
+ %title
+ GitLab CI
+
+ %body
+ = yield :header
+
+ %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"}
+ %tr
+ %td{align: "left", style: "margin: 0; padding: 10px;"}
+ = yield
+ %br
+ %tr
+ %td{align: "left", style: "margin: 0; padding: 10px;"}
+ %p{style: "font-size:small;color:#777"}
+ - if @project
+ You're receiving this notification because you are the one who triggered a build on the #{@project.name} project.
diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml
new file mode 100644
index 0000000000..15478c3f5b
--- /dev/null
+++ b/app/views/layouts/ci/project.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title @project.name, ci_project_path(@project)
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index c3b0720062..56283cba6b 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -46,3 +46,8 @@
= icon('user fw')
%span
Profile Settings
+ = nav_link(controller: :ci) do
+ = link_to ci_root_path, title: 'Continuous Integration', data: {placement: 'right'} do
+ = icon('building fw')
+ %span
+ CI
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 6e53f55b0a..c1d5428f67 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -30,4 +30,8 @@
= render 'projects/buttons/dropdown'
+ - if @project.gitlab_ci?
+ = link_to ci_project_path(@project.gitlab_ci_project), class: 'btn btn-default' do
+ CI
+
= render "shared/clone_panel"
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 30622d8a91..f0a3e416db 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,14 +1,15 @@
- if event = last_push_event
- if show_last_push_widget?(event)
- .hidden-xs.center
- .slead
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
- %strong= event.ref_name
- branch
- #{time_ago_with_tooltip(event.created_at)}
- %div
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
- Create Merge Request
- %hr
+ .gray-content-block.top-block.clear-block.hidden-xs
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ %strong= event.ref_name
+ branch
+ #{time_ago_with_tooltip(event.created_at)}
+
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
+ Create Merge Request
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index 13f8271b97..373b3a0c5b 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -17,6 +17,6 @@
tree_join(@commit.sha, @path)), class: 'btn btn-sm'
- if allowed_tree_edit?
- = button_tag class: 'remove-blob btn btn-sm btn-remove',
- 'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do
- Remove
+ .btn-group{ role: "group" }
+ %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace
+ %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
new file mode 100644
index 0000000000..2cfb79486d
--- /dev/null
+++ b/app/views/projects/blob/_upload.html.haml
@@ -0,0 +1,28 @@
+#modal-upload-blob.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3.page-title #{@title}
+ %p.light
+ From branch
+ %strong= @ref
+ .modal-body
+ = form_tag @form_path, method: @method, class: 'blob-file-upload-form-js form-horizontal' do
+ .dropzone
+ .dropzone-previews.blob-upload-dropzone-previews
+ %p.dz-message.light
+ Attach a file by drag & drop or
+ = link_to 'click to upload', '#', class: "markdown-selector"
+ %br
+ .dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
+ = render 'shared/commit_message_container', params: params,
+ placeholder: @placeholder
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ = button_tag @button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:coffeescript
+ disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'
+ new BlobFileDropzone($('.blob-file-upload-form-js'), '#{@method}')
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a12cd660fc..648f15418e 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,6 +1,6 @@
- page_title "Edit", @blob.path, @ref
.file-editor
- %ul.nav.nav-tabs.js-edit-mode
+ %ul.center-top-menu.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
%i.fa.fa-edit
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 7c2a4fece9..68c9ec7f80 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,5 +1,11 @@
-- page_title "New File", @ref
-%h3.page-title New file
+.gray-content-block.top-block
+ Create a new file or
+ = link_to 'upload', '#modal-upload-blob',
+ { class: 'upload-link', 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'}
+ an existing one
+
+= render 'projects/blob/upload'
+
.file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do
= render 'projects/blob/editor', ref: @ref
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index bd2fc43633..4e66a43bbd 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -10,3 +10,4 @@
- if allowed_tree_edit?
= render 'projects/blob/remove'
+ = render 'projects/blob/upload'
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 798f1c47da..185ebf2393 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -4,7 +4,7 @@
= render "home_panel"
-.center.light-well
+.gray-content-block.center
%h3.page-title
The repository for this project is empty
%p
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 39fe0fc1c4..06886d215a 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -3,8 +3,12 @@
.center
%h2
%i.fa.fa-spinner.fa-spin
- Import in progress.
- %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
+ - if @project.forked?
+ Forking in progress.
+ - else
+ Import in progress.
+ - unless @project.forked?
+ %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
new ProjectImport();
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index aa4e8722fb..37d5dba033 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -16,8 +16,8 @@
- if @user == current_user
.pull-right.hidden-xs
= link_to profile_path, class: 'btn btn-sm' do
- %i.fa.fa-pencil-square-o
- Edit Profile settings
+ = icon('user')
+ Profile settings
- elsif current_user
.pull-right
%span.dropdown
diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb
new file mode 100644
index 0000000000..ebb43570e2
--- /dev/null
+++ b/app/workers/ci/hip_chat_notifier_worker.rb
@@ -0,0 +1,19 @@
+module Ci
+ class HipChatNotifierWorker
+ include Sidekiq::Worker
+
+ def perform(message, options={})
+ room = options.delete('room')
+ token = options.delete('token')
+ server = options.delete('server')
+ name = options.delete('service_name')
+ client_opts = {
+ api_version: 'v2',
+ server_url: server
+ }
+
+ client = HipChat::Client.new(token, client_opts)
+ client[room].send(name, message, options.symbolize_keys)
+ end
+ end
+end
diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb
new file mode 100644
index 0000000000..3bbb9b4bec
--- /dev/null
+++ b/app/workers/ci/slack_notifier_worker.rb
@@ -0,0 +1,10 @@
+module Ci
+ class SlackNotifierWorker
+ include Sidekiq::Worker
+
+ def perform(webhook_url, message, options={})
+ notifier = Slack::Notifier.new(webhook_url)
+ notifier.ping(message, options)
+ end
+ end
+end
diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb
new file mode 100644
index 0000000000..0bb8384557
--- /dev/null
+++ b/app/workers/ci/web_hook_worker.rb
@@ -0,0 +1,9 @@
+module Ci
+ class WebHookWorker
+ include Sidekiq::Worker
+
+ def perform(hook_id, data)
+ Ci::WebHook.find(hook_id).execute data
+ end
+ end
+end
diff --git a/bin/background_jobs b/bin/background_jobs
index a4895cf658..d4578f6a22 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -37,7 +37,7 @@ start_no_deamonize()
start_sidekiq()
{
- bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
+ bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
}
load_ok()
diff --git a/bin/ci/upgrade.rb b/bin/ci/upgrade.rb
new file mode 100644
index 0000000000..aab4f60ec6
--- /dev/null
+++ b/bin/ci/upgrade.rb
@@ -0,0 +1,3 @@
+require_relative "../lib/ci/upgrader"
+
+Ci::Upgrader.new.execute
diff --git a/builds/.gitkeep b/builds/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 03af7f0786..d7d6aed160 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -24,6 +24,11 @@ Gitlab::Application.configure do
# Expands the lines which load the assets
# config.assets.debug = true
+
+ # Adds additional error checking when serving assets at runtime.
+ # Checks for improperly declared sprockets dependencies.
+ # Raises helpful error messages.
+ config.assets.raise_runtime_errors = true
# For having correct urls in mails
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 9eb99dae45..0005d44e0f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -110,7 +110,23 @@ production: &base
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
#
- # 2. Auth settings
+ # 2. GitLab CI settings
+ # ==========================
+
+ gitlab_ci:
+ # Default project notifications settings:
+ #
+ # Send emails only on broken builds (default: true)
+ # all_broken_builds: true
+ #
+ # Add pusher to recipients list (default: false)
+ # add_pusher: true
+
+ # The location where build traces are stored (default: builds/). Relative paths are relative to Rails.root
+ # builds_path: builds/
+
+ #
+ # 3. Auth settings
# ==========================
## LDAP settings
@@ -143,7 +159,7 @@ production: &base
method: 'plain' # "tls" or "ssl" or "plain"
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
-
+
# This setting specifies if LDAP server is Active Directory LDAP server.
# For non AD servers it skips the AD specific queries.
# If your LDAP server is not AD, set this to false.
@@ -180,6 +196,26 @@ production: &base
#
user_filter: ''
+ # LDAP attributes that GitLab will use to create an account for the LDAP user.
+ # The specified attribute can either be the attribute name as a string (e.g. 'mail'),
+ # or an array of attribute names to try in order (e.g. ['mail', 'email']).
+ # Note that the user's LDAP login will always be the attribute specified as `uid` above.
+ attributes:
+ # The username will be used in paths for the user's own projects
+ # (like `gitlab.example.com/username/project`) and when mentioning
+ # them in issues, merge request and comments (like `@username`).
+ # If the attribute specified for `username` contains an email address,
+ # the GitLab username will be the part of the email address before the '@'.
+ username: ['uid', 'userid', 'sAMAccountName']
+ email: ['mail', 'email', 'userPrincipalName']
+
+ # If no full name could be found at the attribute specified for `name`,
+ # the full name is determined using the attributes specified for
+ # `first_name` and `last_name`.
+ name: 'cn'
+ first_name: 'givenName'
+ last_name: 'sn'
+
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
# so that GitLab can remember which LDAP server a user belongs to.
@@ -256,7 +292,7 @@ production: &base
#
- # 3. Advanced settings
+ # 4. Advanced settings
# ==========================
# GitLab Satellites
@@ -315,7 +351,7 @@ production: &base
timeout: 10
#
- # 4. Extra customization
+ # 5. Extra customization
# ==========================
extra:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 689c3f3049..fe81ffd420 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -18,7 +18,19 @@ class Settings < Settingslogic
host.start_with?('www.') ? host[4..-1] : host
end
- private
+ def build_gitlab_ci_url
+ if gitlab_on_standard_port?
+ custom_port = nil
+ else
+ custom_port = ":#{gitlab.port}"
+ end
+ [ gitlab.protocol,
+ "://",
+ gitlab.host,
+ custom_port,
+ gitlab.relative_url_root
+ ].join('')
+ end
def build_gitlab_shell_ssh_path_prefix
if gitlab_shell.ssh_port != 22
@@ -97,6 +109,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
server['block_auto_created_users'] = false if server['block_auto_created_users'].nil?
server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil?
server['active_directory'] = true if server['active_directory'].nil?
+ server['attributes'] = {} if server['attributes'].nil?
server['provider_name'] ||= "ldap#{key}".downcase
server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name'])
end
@@ -160,6 +173,16 @@ Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitla
Settings.gitlab['restricted_signup_domains'] ||= []
Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
+
+#
+# CI
+#
+Settings['gitlab_ci'] ||= Settingslogic.new({})
+Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil?
+Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil?
+Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
+Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root)
+
#
# Reply by email
#
diff --git a/config/initializers/3_grit_ext.rb b/config/initializers/3_grit_ext.rb
deleted file mode 100644
index 6540ac839c..0000000000
--- a/config/initializers/3_grit_ext.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'grit'
-
-Grit::Git.git_binary = Gitlab.config.git.bin_path
-Grit::Git.git_timeout = Gitlab.config.git.timeout
-Grit::Git.git_max_size = Gitlab.config.git.max_size
diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb
new file mode 100644
index 0000000000..cac8edb32b
--- /dev/null
+++ b/config/initializers/4_ci_app.rb
@@ -0,0 +1,10 @@
+module GitlabCi
+ VERSION = Gitlab::VERSION
+ REVISION = Gitlab::REVISION
+
+ REGISTRATION_TOKEN = SecureRandom.hex(10)
+
+ def self.config
+ Settings
+ end
+end
diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb
new file mode 100644
index 0000000000..d831a1838e
--- /dev/null
+++ b/config/initializers/connection_fix.rb
@@ -0,0 +1,32 @@
+# from http://gist.github.com/238999
+#
+# If your workers are inactive for a long period of time, they'll lose
+# their MySQL connection.
+#
+# This hack ensures we re-connect whenever a connection is
+# lost. Because, really. why not?
+#
+# Stick this in RAILS_ROOT/config/initializers/connection_fix.rb (or somewhere similar)
+#
+# From:
+# http://coderrr.wordpress.com/2009/01/08/activerecord-threading-issues-and-resolutions/
+
+if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
+ module ActiveRecord::ConnectionAdapters
+ class Mysql2Adapter
+ alias_method :execute_without_retry, :execute
+
+ def execute(*args)
+ execute_without_retry(*args)
+ rescue ActiveRecord::StatementInvalid => e
+ if e.message =~ /server has gone away/i
+ warn "Server timed out, retrying"
+ reconnect!
+ retry
+ else
+ raise e
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..43adac8b2c
--- /dev/null
+++ b/config/initializers/cookies_serializer.rb
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Gitlab::Application.config.action_dispatch.cookies_serializer = :hybrid
diff --git a/config/initializers/8_default_url_options.rb b/config/initializers/default_url_options.rb
similarity index 80%
rename from config/initializers/8_default_url_options.rb
rename to config/initializers/default_url_options.rb
index 8fd27b1d88..f9f88f95db 100644
--- a/config/initializers/8_default_url_options.rb
+++ b/config/initializers/default_url_options.rb
@@ -8,4 +8,4 @@ unless Gitlab.config.gitlab_on_standard_port?
default_url_options[:port] = Gitlab.config.gitlab.port
end
-Rails.application.routes.default_url_options = default_url_options
+Gitlab::Application.routes.default_url_options = default_url_options
diff --git a/config/initializers/7_omniauth.rb b/config/initializers/omniauth.rb
similarity index 100%
rename from config/initializers/7_omniauth.rb
rename to config/initializers/omniauth.rb
diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example
index b1bbcca1d6..2155ea1456 100644
--- a/config/initializers/rack_attack.rb.example
+++ b/config/initializers/rack_attack.rb.example
@@ -4,13 +4,13 @@
# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
paths_to_be_protected = [
- "#{Rails.application.config.relative_url_root}/users/password",
- "#{Rails.application.config.relative_url_root}/users/sign_in",
- "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session.json",
- "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session",
- "#{Rails.application.config.relative_url_root}/users",
- "#{Rails.application.config.relative_url_root}/users/confirmation",
- "#{Rails.application.config.relative_url_root}/unsubscribes/"
+ "#{Gitlab::Application.config.relative_url_root}/users/password",
+ "#{Gitlab::Application.config.relative_url_root}/users/sign_in",
+ "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session.json",
+ "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session",
+ "#{Gitlab::Application.config.relative_url_root}/users",
+ "#{Gitlab::Application.config.relative_url_root}/users/confirmation",
+ "#{Gitlab::Application.config.relative_url_root}/unsubscribes/"
]
diff --git a/config/initializers/6_rack_profiler.rb b/config/initializers/rack_profiler.rb
similarity index 81%
rename from config/initializers/6_rack_profiler.rb
rename to config/initializers/rack_profiler.rb
index 1d958904e8..7710eeac45 100644
--- a/config/initializers/6_rack_profiler.rb
+++ b/config/initializers/rack_profiler.rb
@@ -2,7 +2,7 @@ if Rails.env.development?
require 'rack-mini-profiler'
# initialization is skipped so trigger it
- Rack::MiniProfilerRails.initialize!(Rails.application)
+ Rack::MiniProfilerRails.initialize!(Gitlab::Application)
Rack::MiniProfiler.config.position = 'right'
Rack::MiniProfiler.config.start_hidden = false
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index 62a54bc8c6..1b518c3bec 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -24,3 +24,27 @@ end
Gitlab::Application.config.secret_token = find_secure_token
Gitlab::Application.config.secret_key_base = find_secure_token
+
+# CI
+def generate_new_secure_token
+ SecureRandom.hex(64)
+end
+
+if Gitlab::Application.secrets.db_key_base.blank?
+ warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`"
+
+ all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml')
+ all_secrets ||= {}
+
+ # generate secrets
+ env_secrets = all_secrets[Rails.env.to_s] || {}
+ env_secrets['db_key_base'] ||= generate_new_secure_token
+ all_secrets[Rails.env.to_s] = env_secrets
+
+ # save secrets
+ File.open('config/secrets.yml', 'w', 0600) do |file|
+ file.write(YAML.dump(all_secrets))
+ end
+
+ Gitlab::Application.secrets.db_key_base = env_secrets['db_key_base']
+end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 88651394d1..04ed9e90df 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -16,5 +16,5 @@ Gitlab::Application.config.session_store(
secure: Gitlab.config.gitlab.https,
httponly: true,
expire_after: Settings.gitlab['session_expire_delay'] * 60,
- path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root
+ path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root
)
diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/sidekiq.rb
similarity index 100%
rename from config/initializers/4_sidekiq.rb
rename to config/initializers/sidekiq.rb
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index d9042c652b..e6d5600edb 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -1,4 +1,4 @@
-app = Rails.application
+app = Gitlab::Application
if app.config.serve_static_assets
# The `ActionDispatch::Static` middleware intercepts requests for static files
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index f3db5b7476..d8bf0878a3 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -32,10 +32,11 @@ en:
send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.'
updated: 'Your password was changed successfully. You are now signed in.'
updated_not_active: 'Your password was changed successfully.'
- send_paranoid_instructions: "If your e-mail exists on our database, you will receive a password recovery link on your e-mail"
+ send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
+ no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
confirmations:
send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.'
- send_paranoid_instructions: 'If your e-mail exists on our database, you will receive an email with instructions about how to confirm your account in a few minutes.'
+ send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.'
confirmed: 'Your account was successfully confirmed. You are now signed in.'
registrations:
signed_up: 'Welcome! You have signed up successfully.'
@@ -57,4 +58,4 @@ en:
reset_password_instructions:
subject: 'Reset password instructions'
unlock_instructions:
- subject: 'Unlock Instructions'
\ No newline at end of file
+ subject: 'Unlock Instructions'
diff --git a/config/routes.rb b/config/routes.rb
index fad6b3c569..41970d2af8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,6 +2,105 @@ require 'sidekiq/web'
require 'api/api'
Gitlab::Application.routes.draw do
+ namespace :ci do
+ # CI API
+ Ci::API::API.logger Rails.logger
+ mount Ci::API::API => '/api'
+
+ resource :lint, only: [:show, :create]
+
+ resource :help do
+ get :oauth2
+ end
+
+ resources :projects do
+ collection do
+ post :add
+ get :gitlab
+ end
+
+ member do
+ get :status, to: 'projects#badge'
+ get :integration
+ post :build
+ post :toggle_shared_runners
+ get :dumped_yaml
+ end
+
+ resources :services, only: [:index, :edit, :update] do
+ member do
+ get :test
+ end
+ end
+
+ resource :charts, only: [:show]
+
+ resources :refs, constraints: { ref_id: /.*/ }, only: [] do
+ resources :commits, only: [:show] do
+ member do
+ get :status
+ get :cancel
+ end
+ end
+ end
+
+ resources :builds, only: [:show] do
+ member do
+ get :cancel
+ get :status
+ post :retry
+ end
+ end
+
+ resources :web_hooks, only: [:index, :create, :destroy] do
+ member do
+ get :test
+ end
+ end
+
+ resources :triggers, only: [:index, :create, :destroy]
+
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ member do
+ get :resume
+ get :pause
+ end
+ end
+
+ resources :runner_projects, only: [:create, :destroy]
+
+ resources :events, only: [:index]
+ resource :variables, only: [:show, :update]
+ end
+
+ resource :user_sessions do
+ get :auth
+ get :callback
+ end
+
+ namespace :admin do
+ resources :runners, only: [:index, :show, :update, :destroy] do
+ member do
+ put :assign_all
+ get :resume
+ get :pause
+ end
+ end
+
+ resources :events, only: [:index]
+
+ resources :projects do
+ resources :runner_projects
+ end
+
+ resources :builds, only: :index
+
+ resource :application_settings, only: [:show, :update]
+ end
+
+ root to: 'projects#index'
+ end
+
use_doorkeeper do
controllers applications: 'oauth/applications',
authorized_applications: 'oauth/authorized_applications',
@@ -358,6 +457,16 @@ Gitlab::Application.routes.draw do
to: 'blob#destroy',
constraints: { id: /.+/, format: false }
)
+ put(
+ '/blob/*id',
+ to: 'blob#update',
+ constraints: { id: /.+/, format: false }
+ )
+ post(
+ '/blob/*id',
+ to: 'blob#create',
+ constraints: { id: /.+/, format: false }
+ )
end
scope do
diff --git a/config/schedule.rb b/config/schedule.rb
new file mode 100644
index 0000000000..8122f7cc69
--- /dev/null
+++ b/config/schedule.rb
@@ -0,0 +1,8 @@
+# Use this file to easily define all of your cron jobs.
+#
+# If you make changes to this file, please create also an issue on
+# https://gitlab.com/gitlab-org/omnibus-gitlab/issues . This is necessary
+# because the omnibus packages manage cron jobs using Chef instead of Whenever.
+every 1.hour do
+ rake "ci:schedule_builds"
+end
diff --git a/config/secrets.yml.example b/config/secrets.yml.example
new file mode 100644
index 0000000000..6b408ac603
--- /dev/null
+++ b/config/secrets.yml.example
@@ -0,0 +1,12 @@
+production:
+ # db_key_base is used to encrypt for Variables. Ensure that you don't lose it.
+ # If you change or lose this key you will be unable to access variables stored in database.
+ # Make sure the secret is at least 30 characters and all random,
+ # no regular words or you'll be exposed to dictionary attacks.
+ # db_key_base:
+
+development:
+ db_key_base: development
+
+test:
+ db_key_base: test
diff --git a/config/sidekiq.yml.example b/config/sidekiq.yml.example
new file mode 100644
index 0000000000..c691db67c6
--- /dev/null
+++ b/config/sidekiq.yml.example
@@ -0,0 +1,2 @@
+--
+:concurrency: 5
\ No newline at end of file
diff --git a/db/migrate/20150826001931_add_ci_tables.rb b/db/migrate/20150826001931_add_ci_tables.rb
new file mode 100644
index 0000000000..c4f51363e5
--- /dev/null
+++ b/db/migrate/20150826001931_add_ci_tables.rb
@@ -0,0 +1,190 @@
+class AddCiTables < ActiveRecord::Migration
+ def change
+ create_table "ci_application_settings", force: true do |t|
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_builds", force: true do |t|
+ t.integer "project_id"
+ t.string "status"
+ t.datetime "finished_at"
+ t.text "trace"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.datetime "started_at"
+ t.integer "runner_id"
+ t.float "coverage"
+ t.integer "commit_id"
+ t.text "commands"
+ t.integer "job_id"
+ t.string "name"
+ t.boolean "deploy", default: false
+ t.text "options"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage"
+ t.integer "trigger_request_id"
+ end
+
+ add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
+ add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
+ add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
+ add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+
+ create_table "ci_commits", force: true do |t|
+ t.integer "project_id"
+ t.string "ref"
+ t.string "sha"
+ t.string "before_sha"
+ t.text "push_data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "tag", default: false
+ t.text "yaml_errors"
+ t.datetime "committed_at"
+ end
+
+ add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
+ add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
+ add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
+ add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
+
+ create_table "ci_events", force: true do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree
+ add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
+ add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
+
+ create_table "ci_jobs", force: true do |t|
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
+ t.datetime "deleted_at"
+ end
+
+ add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
+ add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
+
+ create_table "ci_projects", force: true do |t|
+ t.string "name", null: false
+ t.integer "timeout", default: 3600, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
+ end
+
+ create_table "ci_runner_projects", force: true do |t|
+ t.integer "runner_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
+ add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
+
+ create_table "ci_runners", force: true do |t|
+ t.string "token"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "description"
+ t.datetime "contacted_at"
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name"
+ t.string "version"
+ t.string "revision"
+ t.string "platform"
+ t.string "architecture"
+ end
+
+ create_table "ci_services", force: true do |t|
+ t.string "type"
+ t.string "title"
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
+ t.text "properties"
+ end
+
+ add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
+
+ create_table "ci_sessions", force: true do |t|
+ t.string "session_id", null: false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
+ add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
+
+ create_table "ci_trigger_requests", force: true do |t|
+ t.integer "trigger_id", null: false
+ t.text "variables"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "commit_id"
+ end
+
+ create_table "ci_triggers", force: true do |t|
+ t.string "token"
+ t.integer "project_id", null: false
+ t.datetime "deleted_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
+
+ create_table "ci_variables", force: true do |t|
+ t.integer "project_id", null: false
+ t.string "key"
+ t.text "value"
+ t.text "encrypted_value"
+ t.string "encrypted_value_salt"
+ t.string "encrypted_value_iv"
+ end
+
+ add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+
+ create_table "ci_web_hooks", force: true do |t|
+ t.string "url", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ end
+end
diff --git a/db/migrate/20150914215247_add_ci_tags.rb b/db/migrate/20150914215247_add_ci_tags.rb
new file mode 100644
index 0000000000..df3390e8a8
--- /dev/null
+++ b/db/migrate/20150914215247_add_ci_tags.rb
@@ -0,0 +1,23 @@
+class AddCiTags < ActiveRecord::Migration
+ def change
+ create_table "ci_taggings", force: true do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
+ add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
+
+ create_table "ci_tags", force: true do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
+ end
+end
diff --git a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb
new file mode 100644
index 0000000000..78d9e5f61a
--- /dev/null
+++ b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb
@@ -0,0 +1,5 @@
+class AddIndexForCommittedAtAndId < ActiveRecord::Migration
+ def change
+ add_index :ci_commits, [:project_id, :committed_at, :id]
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 2b7afae6d7..73605d4c5e 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -6,5 +6,9 @@ class LimitsToMysql < ActiveRecord::Migration
change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
+
+ # CI
+ change_column :ci_builds, :trace, :text, limit: 1073741823
+ change_column :ci_commits, :push_data, :text, limit: 16777215
end
end
diff --git a/db/schema.rb b/db/schema.rb
index 55cbd8c293..48314b8db6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150902001023) do
+ActiveRecord::Schema.define(version: 20150916145038) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -72,6 +72,214 @@ ActiveRecord::Schema.define(version: 20150902001023) do
t.string "font"
end
+ create_table "ci_application_settings", force: true do |t|
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_builds", force: true do |t|
+ t.integer "project_id"
+ t.string "status"
+ t.datetime "finished_at"
+ t.text "trace"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.datetime "started_at"
+ t.integer "runner_id"
+ t.float "coverage"
+ t.integer "commit_id"
+ t.text "commands"
+ t.integer "job_id"
+ t.string "name"
+ t.boolean "deploy", default: false
+ t.text "options"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage"
+ t.integer "trigger_request_id"
+ end
+
+ add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
+ add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
+ add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
+ add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+
+ create_table "ci_commits", force: true do |t|
+ t.integer "project_id"
+ t.string "ref"
+ t.string "sha"
+ t.string "before_sha"
+ t.text "push_data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "tag", default: false
+ t.text "yaml_errors"
+ t.datetime "committed_at"
+ end
+
+ add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree
+ add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
+ add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
+ add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
+ add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
+
+ create_table "ci_events", force: true do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree
+ add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
+ add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
+
+ create_table "ci_jobs", force: true do |t|
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
+ t.datetime "deleted_at"
+ end
+
+ add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
+ add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
+
+ create_table "ci_projects", force: true do |t|
+ t.string "name", null: false
+ t.integer "timeout", default: 3600, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
+ end
+
+ create_table "ci_runner_projects", force: true do |t|
+ t.integer "runner_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
+ add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
+
+ create_table "ci_runners", force: true do |t|
+ t.string "token"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "description"
+ t.datetime "contacted_at"
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name"
+ t.string "version"
+ t.string "revision"
+ t.string "platform"
+ t.string "architecture"
+ end
+
+ create_table "ci_services", force: true do |t|
+ t.string "type"
+ t.string "title"
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
+ t.text "properties"
+ end
+
+ add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
+
+ create_table "ci_sessions", force: true do |t|
+ t.string "session_id", null: false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
+ add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
+
+ create_table "ci_taggings", force: true do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
+ add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
+
+ create_table "ci_tags", force: true do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
+
+ create_table "ci_trigger_requests", force: true do |t|
+ t.integer "trigger_id", null: false
+ t.text "variables"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "commit_id"
+ end
+
+ create_table "ci_triggers", force: true do |t|
+ t.string "token"
+ t.integer "project_id", null: false
+ t.datetime "deleted_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
+
+ create_table "ci_variables", force: true do |t|
+ t.integer "project_id", null: false
+ t.string "key"
+ t.text "value"
+ t.text "encrypted_value"
+ t.string "encrypted_value_salt"
+ t.string "encrypted_value_iv"
+ end
+
+ add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+
+ create_table "ci_web_hooks", force: true do |t|
+ t.string "url", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
create_table "deploy_keys_projects", force: true do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
diff --git a/doc/README.md b/doc/README.md
index 337c4e6a62..f5f1f56b1e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -15,6 +15,23 @@
- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
+## CI Documentation
+
++ [Quick Start](ci/quick_start/README.md)
++ [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
++ [Configuring runner](ci/runners/README.md)
++ [Configuring deployment](ci/deployment/README.md)
++ [Using Docker Images](ci/docker/using_docker_images.md)
++ [Using Docker Build](ci/docker/using_docker_build.md)
++ [Using Variables](ci/variables/README.md)
+
+### CI Examples
+
++ [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
++ [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
++ [Test Clojure applications](ci/examples/test-clojure-application.md)
++ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
+
## Administrator documentation
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough.
@@ -30,6 +47,12 @@
- [Update](update/README.md) Update guides to upgrade your installation.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
- [Reply by email](reply_by_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
+- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
+
+### Administrator documentation
+
++ [User permissions](permissions/README.md)
++ [API](api/README.md)
## Contributor documentation
diff --git a/doc/ci/README.md b/doc/ci/README.md
new file mode 100644
index 0000000000..97325069ce
--- /dev/null
+++ b/doc/ci/README.md
@@ -0,0 +1,23 @@
+## GitLab CI Documentation
+
+### User documentation
+
++ [Quick Start](quick_start/README.md)
++ [Configuring project (.gitlab-ci.yml)](yaml/README.md)
++ [Configuring runner](runners/README.md)
++ [Configuring deployment](deployment/README.md)
++ [Using Docker Images](docker/using_docker_images.md)
++ [Using Docker Build](docker/using_docker_build.md)
++ [Using Variables](variables/README.md)
+
+### Examples
+
++ [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
++ [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
++ [Test Clojure applications](examples/test-clojure-application.md)
++ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
+
+### Administrator documentation
+
++ [User permissions](permissions/README.md)
++ [API](api/README.md)
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
new file mode 100644
index 0000000000..e47e5c4673
--- /dev/null
+++ b/doc/ci/api/README.md
@@ -0,0 +1,87 @@
+# GitLab CI API
+
+## Resources
+
+- [Projects](projects.md)
+- [Runners](runners.md)
+- [Commits](commits.md)
+- [Builds](builds.md)
+- [Forks](forks.md)
+
+
+## Authentication
+
+GitLab CI API uses different types of authentication depends on what API you use.
+Each API document has section with information about authentication you need to use.
+
+GitLab CI API has 4 authentication methods:
+
+* GitLab user token & GitLab url
+* GitLab CI project token
+* GitLab CI runners registration token
+* GitLab CI runner token
+
+
+### Authentication #1: GitLab user token & GitLab url
+
+Authentication is done by
+sending the `private-token` of a valid user and the `url` of an
+authorized Gitlab instance via a query string along with the API
+request:
+
+ GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
+
+If preferred, you may instead send the `private-token` as a header in
+your request:
+
+ curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/"
+
+
+### Authentication #2: GitLab CI project token
+
+Each project in GitLab CI has it own token.
+It can be used to get project commits and builds information.
+You can use project token only for certain project.
+
+### Authentication #3: GitLab CI runners registration token
+
+This token is not persisted and is generated on each application start.
+It can be used only for registering new runners in system. You can find it on
+GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners
+
+### Authentication #4: GitLab CI runner token
+
+Every GitLab CI runner has it own token that allow it to receive and update
+GitLab CI builds. This token exists of internal purposes and should be used only
+by runners
+
+## JSON
+
+All API requests are serialized using JSON. You don't need to specify
+`.json` at the end of API URL.
+
+## Status codes
+
+The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave.
+
+API request types:
+
+- `GET` requests access one or more resources and return the result as JSON
+- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON
+- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON
+- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not.
+
+The following list shows the possible return codes for API requests.
+
+Return values:
+
+- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON
+- `201 Created` - The `POST` request was successful and the resource is returned as JSON
+- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given
+- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above
+- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project
+- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found
+- `405 Method Not Allowed` - The request is not supported
+- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
+- `422 Unprocessable` - The entity could not be processed
+- `500 Server Error` - While handling the request something went wrong on the server side
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
new file mode 100644
index 0000000000..3b5008ccdb
--- /dev/null
+++ b/doc/ci/api/builds.md
@@ -0,0 +1,41 @@
+# Builds API
+
+This API used by runners to receive and update builds.
+
+__Authentication is done by runner token__
+
+## Builds
+
+### Runs oldest pending build by runner
+
+ POST /ci/builds/register
+
+Parameters:
+
+ * `token` (required) - The unique token of runner
+
+Returns:
+
+```json
+{
+ "id" : 79,
+ "commands" : "",
+ "path" : "",
+ "ref" : "",
+ "sha" : "",
+ "project_id" : 6,
+ "repo_url" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "before_sha" : ""
+}
+```
+
+
+### Update details of an existing build
+
+ PUT /ci/builds/:id
+
+Parameters:
+
+ * `id` (required) - The ID of a project
+ * `state` (optional) - The state of a build
+ * `trace` (optional) - The trace of a build
diff --git a/doc/ci/api/commits.md b/doc/ci/api/commits.md
new file mode 100644
index 0000000000..4df7afc6c5
--- /dev/null
+++ b/doc/ci/api/commits.md
@@ -0,0 +1,101 @@
+# Commits API
+
+__Authentication is done by GitLab CI project token__
+
+## Commits
+
+### Retrieve all commits per project
+
+Get list of commits per project
+
+ GET /ci/commits
+
+Parameters:
+
+ * `project_id` (required) - The ID of a project
+ * `project_token` (requires) - Project token
+ * `page` (optional)
+ * `per_page` (optional) - items per request (default is 20)
+
+Returns:
+
+```json
+[{
+ "id": 3,
+ "ref": "master",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "project_id": 2,
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "created_at": "2014-11-05T09:46:35.247Z",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "duration": 5.062692165374756,
+ "git_commit_message": "wow\n",
+ "git_author_name": "Administrator",
+ "git_author_email": "admin@example.com",
+ "builds": [{
+ "id": 7,
+ "project_id": 2,
+ "ref": "master",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "created_at": "2014-11-05T09:46:35.259Z",
+ "updated_at": "2014-11-05T09:46:44.255Z",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "started_at": "2014-11-05T09:46:39.192Z",
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "runner_id": 1,
+ "coverage": null,
+ "commit_id": 3
+ }]
+}]
+```
+
+### Create commit
+
+Inform GitLab CI about new commit you want it to build.
+
+__If commit already exists in GitLab CI it will not be created__
+
+
+ POST /ci/commits
+
+Parameters:
+
+ * `project_id` (required) - The ID of a project
+ * `project_token` (requires) - Project token
+ * `data` (required) - Push data. For example see comment in `lib/api/commits.rb`
+
+Returns:
+
+```json
+{
+ "id": 3,
+ "ref": "master",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "project_id": 2,
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "created_at": "2014-11-05T09:46:35.247Z",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "duration": 5.062692165374756,
+ "git_commit_message": "wow\n",
+ "git_author_name": "Administrator",
+ "git_author_email": "admin@example.com",
+ "builds": [{
+ "id": 7,
+ "project_id": 2,
+ "ref": "master",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "created_at": "2014-11-05T09:46:35.259Z",
+ "updated_at": "2014-11-05T09:46:44.255Z",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "started_at": "2014-11-05T09:46:39.192Z",
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "runner_id": 1,
+ "coverage": null,
+ "commit_id": 3
+ }]
+}
+```
diff --git a/doc/ci/api/forks.md b/doc/ci/api/forks.md
new file mode 100644
index 0000000000..8f32e2d3b4
--- /dev/null
+++ b/doc/ci/api/forks.md
@@ -0,0 +1,23 @@
+# Forks API
+
+This API is intended to aid in the setup and configuration of
+forked projects on Gitlab CI.
+
+__Authentication is done by GitLab user token & GitLab project token__
+
+## Forks
+
+### Create fork for project
+
+
+
+```
+POST /ci/forks
+```
+
+Parameters:
+
+ project_id (required) - The ID of a project
+ project_token (requires) - Project token
+ private_token(required) - User private token
+ data (required) - GitLab project data (name_with_namespace, web_url, default_branch, ssh_url_to_repo)
diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md
new file mode 100644
index 0000000000..54584db093
--- /dev/null
+++ b/doc/ci/api/projects.md
@@ -0,0 +1,154 @@
+# Projects API
+
+This API is intended to aid in the setup and configuration of
+projects on Gitlab CI.
+
+__Authentication is done by GitLab user token & GitLab url__
+
+## Projects
+
+### List Authorized Projects
+
+Lists all projects that the authenticated user has access to.
+
+```
+GET /ci/projects
+```
+
+Returns:
+
+```json
+ [
+ {
+ "id" : 271,
+ "name" : "gitlabhq",
+ "timeout" : 1800,
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "path" : "gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 3
+ },
+ {
+ "id" : 272,
+ "name" : "gitlab-ci",
+ "timeout" : 1800,
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "path" : "gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 4
+ }
+]
+```
+
+### List Owned Projects
+
+Lists all projects that the authenticated user owns.
+
+```
+GET /ci/projects/owned
+```
+
+Returns:
+
+```json
+[
+ {
+ "id" : 272,
+ "name" : "gitlab-ci",
+ "timeout" : 1800,
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "path" : "gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 4
+ }
+]
+```
+
+### Single Project
+
+Returns information about a single project for which the user is
+authorized.
+
+ GET /ci/projects/:id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+
+### Create Project
+
+Creates a Gitlab CI project using Gitlab project details.
+
+ POST /ci/projects
+
+Parameters:
+
+ * `name` (required) - The name of the project
+ * `gitlab_id` (required) - The ID of the project on the Gitlab instance
+ * `path` (required) - The gitlab project path
+ * `ssh_url_to_repo` (required) - The gitlab SSH url to the repo
+ * `default_ref` (optional) - The branch to run on (default to `master`)
+
+### Update Project
+
+Updates a Gitlab CI project using Gitlab project details that the
+authenticated user has access to.
+
+ PUT /ci/projects/:id
+
+Parameters:
+
+ * `name` - The name of the project
+ * `gitlab_id` - The ID of the project on the Gitlab instance
+ * `path` - The gitlab project path
+ * `ssh_url_to_repo` - The gitlab SSH url to the repo
+ * `default_ref` - The branch to run on (default to `master`)
+
+### Remove Project
+
+Removes a Gitlab CI project that the authenticated user has access to.
+
+ DELETE /ci/projects/:id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+
+### Link Project to Runner
+
+Links a runner to a project so that it can make builds (only via
+authorized user).
+
+ POST /ci/projects/:id/runners/:runner_id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+ * `runner_id` (required) - The ID of the Gitlab CI runner
+
+### Remove Project from Runner
+
+Removes a runner from a project so that it can not make builds (only
+via authorized user).
+
+ DELETE /ci/projects/:id/runners/:runner_id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+ * `runner_id` (required) - The ID of the Gitlab CI runner
\ No newline at end of file
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
new file mode 100644
index 0000000000..e9f88ee066
--- /dev/null
+++ b/doc/ci/api/runners.md
@@ -0,0 +1,77 @@
+# Runners API
+
+## Runners
+
+### Retrieve all runners
+
+__Authentication is done by GitLab user token & GitLab url__
+
+Used to get information about all runners registered on the Gitlab CI
+instance.
+
+ GET /ci/runners
+
+Returns:
+
+```json
+[
+ {
+ "id" : 85,
+ "token" : "12b68e90394084703135"
+ },
+ {
+ "id" : 86,
+ "token" : "76bf894e969364709864"
+ },
+]
+```
+
+### Register a new runner
+
+
+__Authentication is done with a Shared runner registration token or a project Specific runner registration token__
+
+Used to make Gitlab CI aware of available runners.
+
+ POST /ci/runners/register
+
+Parameters:
+
+ * `token` (required) - The registration token. It is 2 types of token you can pass here.
+
+1. Shared runner registration token
+2. Project specific registration token
+
+Returns:
+
+```json
+{
+ "id" : 85,
+ "token" : "12b68e90394084703135"
+}
+```
+
+### Delete a runner
+
+
+__Authentication is done by runner token__
+
+Used to removing runners.
+
+ DELETE /ci/runners/delete
+
+Parameters:
+
+ * `token` (required) - The runner token.
+
+Returns:
+
+```json
+{
+ "id" : 1,
+ "token" : "d14963981a428f70121777e50643d1",
+ "created_at" : "2015-02-26T11:39:39.232Z",
+ "updated_at" : "2015-02-26T11:39:39.232Z",
+ "description" : "awesome runner"
+}
+```
\ No newline at end of file
diff --git a/doc/ci/deployment/README.md b/doc/ci/deployment/README.md
new file mode 100644
index 0000000000..ffd841ca9e
--- /dev/null
+++ b/doc/ci/deployment/README.md
@@ -0,0 +1,98 @@
+## Using Dpl as deployment tool
+Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI.
+
+**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**.
+
+### Requirements
+To use Dpl you need at least Ruby 1.8.7 with ability to install gems.
+
+### Basic usage
+The Dpl can be installed on any machine with:
+```
+gem install dpl
+```
+
+This allows you to test all commands from your shell, rather than having to test it on a CI server.
+
+If you don't have Ruby installed you can do it on Debian-compatible Linux with:
+```
+apt-get update
+apt-get install ruby-dev
+```
+
+The Dpl provides support for vast number of services, including: Heroku, Cloud Foundry, AWS/S3, and more.
+To use it simply define provider and any additional parameters required by the provider.
+
+For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`.
+There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku
+
+```
+staging:
+ type: deploy
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
+```
+
+In the above example we use Dpl to deploy `my-app-staging` to Heroku server with api-key stored in `HEROKU_STAGING_API_KEY` secure variable.
+
+To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers).
+
+### Using Dpl with Docker
+When you use GitLab Runner you most likely configured it to use your server's shell commands.
+This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner).
+It also means that most probably in your Docker container you don't have the Ruby runtime installed.
+You will have to install it:
+```
+staging:
+ type: deploy
+ - apt-get update -yq
+ - apt-get install -y ruby-dev
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
+ only:
+ - master
+```
+
+The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system.
+The above example is valid for all Debian-compatible systems.
+
+### Usage in staging and production
+It's pretty common in developer workflow to have staging (development) and production environment.
+If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment.
+The final `.gitlab-ci.yml` for that setup would look like this:
+
+```
+staging:
+ type: deploy
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
+ only:
+ - master
+
+production:
+ type: deploy
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY
+ only:
+ - tags
+```
+
+We created two deploy jobs that are executed on different events:
+1. `staging` is executed for all commits that were pushed to `master` branch,
+2. `production` is executed for all pushed tags.
+
+We also use two secure variables:
+1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
+2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
+
+### Storing API keys
+In GitLab CI 7.12 a new feature was introduced: Secure Variables.
+Secure Variables can added by going to `Project > Variables > Add Variable`.
+**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.**
+The variables that are defined in the project settings are send along with the build script to the runner.
+The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml.
+It is also important that secret's value is hidden in the build log.
+
+You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners):
+1. `$SECRET_VARIABLE` - use it for non-Windows runners
+2. `%SECRET_VARIABLE%` - use it for Windows Batch runners
diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md
new file mode 100644
index 0000000000..84eaf29efd
--- /dev/null
+++ b/doc/ci/docker/README.md
@@ -0,0 +1,4 @@
+# Docker integration
+
++ [Using Docker Images](using_docker_images.md)
++ [Using Docker Build](using_docker_build.md)
\ No newline at end of file
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
new file mode 100644
index 0000000000..a698fbc818
--- /dev/null
+++ b/doc/ci/docker/using_docker_build.md
@@ -0,0 +1,112 @@
+# Using Docker Build
+
+GitLab CI can allows you to use Docker Engine to build and test docker-based projects.
+
+**This also allows to you to use `docker-compose` and other docker-enabled tools.**
+
+This is one of new trends in Continuous Integration/Deployment to:
+
+1. create application image,
+1. run test against created image,
+1. push image to remote registry,
+1. deploy server from pushed image
+
+It's also useful in case when your application already has the `Dockerfile` that can be used to create and test image:
+```bash
+$ docker build -t my-image dockerfiles/
+$ docker run my-docker-image /script/to/run/tests
+$ docker tag my-image my-registry:5000/my-image
+$ docker push my-registry:5000/my-image
+```
+
+However, this requires special configuration of GitLab Runner to enable `docker` support during build.
+**This requires running GitLab Runner in privileged mode which can be harmful when untrusted code is run.**
+
+There are two methods to enable the use of `docker build` and `docker run` during build.
+
+## 1. Use shell executor
+
+The simplest approach is to install GitLab Runner in `shell` execution mode.
+GitLab Runner then executes build scripts as `gitlab-runner` user.
+
+1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+
+1. During GitLab Runner installation select `shell` as method of executing build scripts or use command:
+
+ ```bash
+ $ sudo gitlab-runner register -n \
+ --url http://gitlab.com/ci \
+ --token RUNNER_TOKEN \
+ --executor shell
+ --description "My Runner"
+ ```
+
+2. Install Docker on server.
+
+ For more information how to install Docker on different systems checkout the [Supported installations](https://docs.docker.com/installation/).
+
+3. Add `gitlab-runner` user to `docker` group:
+
+ ```bash
+ $ sudo usermod -aG docker gitlab-runner
+ ```
+
+4. Verify that `gitlab-runner` has access to Docker:
+
+ ```bash
+ $ sudo -u gitlab-runner -H docker info
+ ```
+
+ You can now verify that everything works by adding `docker info` to `.gitlab-ci.yml`:
+ ```yaml
+ before_script:
+ - docker info
+
+ build_image:
+ script:
+ - docker build -t my-docker-image .
+ - docker run my-docker-image /script/to/run/tests
+ ```
+
+5. You can now use `docker` command and install `docker-compose` if needed.
+
+6. However, by adding `gitlab-runner` to `docker` group you are effectively granting `gitlab-runner` full root permissions.
+For more information please checkout [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
+
+## 2. Use docker-in-docker executor
+
+Second approach is to use special Docker image with all tools installed (`docker` and `docker-compose`) and run build script in context of that image in privileged mode.
+In order to do that follow the steps:
+
+1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+
+1. Register GitLab Runner from command line to use `docker` and `privileged` mode:
+
+ ```bash
+ $ sudo gitlab-runner register -n \
+ --url http://gitlab.com/ci \
+ --token RUNNER_TOKEN \
+ --executor docker \
+ --description "My Docker Runner" \
+ --docker-image "gitlab/dind:latest" \
+ --docker-privileged
+ ```
+
+ The above command will register new Runner to use special [gitlab/dind](https://registry.hub.docker.com/u/gitlab/dind/) image which is provided by GitLab Inc.
+ The image at the start runs Docker daemon in [docker-in-docker](https://blog.docker.com/2013/09/docker-can-now-run-within-docker/) mode.
+
+1. You can now use `docker` from build script:
+
+ ```yaml
+ before_script:
+ - docker info
+
+ build_image:
+ script:
+ - docker build -t my-docker-image .
+ - docker run my-docker-image /script/to/run/tests
+ ```
+
+1. However, by enabling `--docker-privileged` you are effectively disables all security mechanisms of containers and exposing your host to privilege escalation which can lead to container breakout.
+For more information you could be interested in checking out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration).
+
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
new file mode 100644
index 0000000000..191e3a8144
--- /dev/null
+++ b/doc/ci/docker/using_docker_images.md
@@ -0,0 +1,203 @@
+# Using Docker Images
+GitLab CI can use [Docker Engine](https://www.docker.com/) to build projects.
+
+Docker is an open-source project that allows to use predefined images to run applications
+in independent "containers" that are run within a single Linux instance.
+[Docker Hub](https://registry.hub.docker.com/) have rich database of built images that can be used to build applications.
+
+Docker when used with GitLab CI runs each build in separate and isolated container using predefined image and always from scratch.
+It makes it easier to have simple and reproducible build environment that can also be run on your workstation.
+This allows you to test all commands from your shell, rather than having to test them on a CI server.
+
+### Register Docker runner
+To use GitLab Runner with Docker you need to register new runner to use `docker` executor:
+
+```bash
+gitlab-ci-multi-runner register \
+ --url "https://gitlab.com/" \
+ --registration-token "PROJECT_REGISTRATION_TOKEN" \
+ --description "docker-ruby-2.1" \
+ --executor "docker" \
+ --docker-image ruby:2.1 \
+ --docker-postgres latest \
+ --docker-mysql latest
+```
+
+**The registered runner will use `ruby:2.1` image and will run two services (`postgres:latest` and `mysql:latest`) that will be accessible for time of the build.**
+
+### What is image?
+The image is the name of any repository that is present in local Docker Engine or any repository that can be found at [Docker Hub](https://registry.hub.docker.com/).
+For more information about the image and Docker Hub please read the [Docker Fundamentals](https://docs.docker.com/introduction/understanding-docker/).
+
+### What is service?
+Service is just another image that is run for time of your build and is linked to your build. This allows you to access the service image during build time.
+The service image can run any application, but most common use case is to run some database container, ie.: `mysql`.
+It's easier and faster to use existing image, run it as additional container than install `mysql` every time project is built.
+
+#### How is service linked to the build?
+There's good document that describes how Docker linking works: [Linking containers together](https://docs.docker.com/userguide/dockerlinks/).
+To summarize: if you add `mysql` as service to your application, the image will be used to create container that is linked to build container.
+The service container for MySQL will be accessible under hostname `mysql`.
+So, **to access your database service you have to connect to host: `mysql` instead of socket or `localhost`**.
+
+### How to use other images as services?
+You are not limited to have only database services.
+You can hand modify `config.toml` to add any image as service found at [Docker Hub](https://registry.hub.docker.com/).
+Look for `[runners.docker]` section:
+```
+[runners.docker]
+ image = "ruby:2.1"
+ services = ["mysql:latest", "postgres:latest"]
+```
+
+For example you need `wordpress` instance to test some API integration with `Wordpress`.
+You can for example use this image: [tutum/wordpress](https://registry.hub.docker.com/u/tutum/wordpress/).
+This is image that have fully preconfigured `wordpress` and have `MySQL` server built-in:
+```
+[runners.docker]
+ image = "ruby:2.1"
+ services = ["mysql:latest", "postgres:latest", "tutum/wordpress:latest"]
+```
+
+Next time when you run your application the `tutum/wordpress` will be started
+and you will have access to it from your build container under hostname: `tutum_wordpress`.
+
+Alias hostname for the service is made from the image name:
+1. Everything after `:` is stripped,
+2. '/' is replaced to `_`.
+
+### Configuring services
+Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment.
+
+GitLab Runner 0.5.0 and up passes all YAML-defined variables to created service containers.
+
+1. To configure database name for [postgres](https://registry.hub.docker.com/u/library/postgres/) service,
+you need to set POSTGRES_DB.
+
+ ```yaml
+ services:
+ - postgres
+
+ variables:
+ POSTGRES_DB: gitlab
+ ```
+
+1. To use [mysql](https://registry.hub.docker.com/u/library/mysql/) service with empty password for time of build,
+you need to set MYSQL_ALLOW_EMPTY_PASSWORD.
+
+ ```yaml
+ services:
+ - mysql
+
+ variables:
+ MYSQL_ALLOW_EMPTY_PASSWORD: yes
+ ```
+
+For other possible configuration variables check the
+https://registry.hub.docker.com/u/library/mysql/ or https://registry.hub.docker.com/u/library/postgres/
+or README page for any other Docker image.
+
+**Note: All variables will passed to all service containers. It's not designed to distinguish which variable should go where.**
+
+### Overwrite image and services
+It's possible to overwrite `docker-image` and specify services from `.gitlab-ci.yml`.
+If you add to your YAML the `image` and the `services` these parameters
+be used instead of the ones that were specified during runner's registration.
+```
+image: ruby:2.2
+services:
+ - postgres:9.3
+before_install:
+ - bundle install
+
+test:
+ script:
+ - bundle exec rake spec
+```
+
+It's possible to define image and service per-job:
+```
+before_install:
+ - bundle install
+
+test:2.1:
+ image: ruby:2.1
+ services:
+ - postgres:9.3
+ script:
+ - bundle exec rake spec
+
+test:2.2:
+ image: ruby:2.2
+ services:
+ - postgres:9.4
+ script:
+ - bundle exec rake spec
+```
+
+#### How to enable overwriting?
+To enable overwriting you have to **enable it first** (it's disabled by default for security reasons).
+You can do that by hand modifying runner configuration: `config.toml`.
+Please go to section where is `[runners.docker]` definition for your runner.
+Add `allowed_images` and `allowed_services` to specify what images are allowed to be picked from `.gitlab-ci.yml`:
+```
+[runners.docker]
+ image = "ruby:2.1"
+ allowed_images = ["ruby:*", "python:*"]
+ allowed_services = ["mysql:*", "redis:*"]
+```
+This enables you to use in your `.gitlab-ci.yml` any image that matches above wildcards.
+You will be able to pick only `ruby` and `python` images.
+The same rule can be applied to limit services.
+
+If you are courageous enough, you can make it fully open and accept everything:
+```
+[runners.docker]
+ image = "ruby:2.1"
+ allowed_images = ["*", "*/*"]
+ allowed_services = ["*", "*/*"]
+```
+
+**It the feature is not enabled, or image isn't allowed the error message will be put into the build log.**
+
+### How Docker integration works
+1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`.
+1. Create cache container to store all volumes as defined in `config.toml` and `Dockerfile` of build image (`ruby:2.1` as in above example).
+1. Create build container and link any service container to build container.
+1. Start build container and send build script to the container.
+1. Run build script.
+1. Checkout code in: `/builds/group-name/project-name/`.
+1. Run any step defined in `.gitlab-ci.yml`.
+1. Check exit status of build script.
+1. Remove build container and all created service containers.
+
+### How to debug a build locally
+1. Create a file with build script:
+```bash
+$ cat < build_script
+git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner
+cd /builds/gitlab-org/gitlab-ci-multi-runner
+make <- or any other build step
+EOF
+```
+
+1. Create service containers:
+```
+$ docker run -d -n service-mysql mysql:latest
+$ docker run -d -n service-postgres postgres:latest
+```
+This will create two service containers (MySQL and PostgreSQL).
+
+1. Create a build container and execute script in its context:
+```
+$ cat build_script | docker run -n build -i -l mysql:service-mysql -l postgres:service-postgres ruby:2.1 /bin/bash
+```
+This will create build container that has two service containers linked.
+The build_script is piped using STDIN to bash interpreter which executes the build script in container.
+
+1. At the end remove all containers:
+```
+docker rm -f -v build service-mysql service-postgres
+```
+This will forcefully (the `-f` switch) remove build container and service containers
+and all volumes (the `-v` switch) that were created with the container creation.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
new file mode 100644
index 0000000000..e0b9fa0e25
--- /dev/null
+++ b/doc/ci/examples/README.md
@@ -0,0 +1,5 @@
+# Build script examples
+
++ [Test and deploy Ruby Application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
++ [Test and deploy Python Application to Heroku](test-and-deploy-python-application-to-heroku.md)
++ [Test Clojure applications](examples/test-clojure-application.md)
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
new file mode 100644
index 0000000000..036b03dd6b
--- /dev/null
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -0,0 +1,72 @@
+## Test and Deploy a python application
+This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application.
+
+You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://ci.gitlab.com/projects/4080).
+
+### Configure project
+This is what the `.gitlab-ci.yml` file looks like for this project:
+```yaml
+test:
+ script:
+ # this configures django application to use attached postgres database that is run on `postgres` host
+ - export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app
+ - apt-get update -qy
+ - apt-get install -y python-dev python-pip
+ - pip install -r requirements.txt
+ - python manage.py test
+
+staging:
+ type: deploy
+ script:
+ - apt-get update -qy
+ - apt-get install -y ruby-dev
+ - gem install dpl
+ - dpl --provider=heroku --app=gitlab-ci-python-test-staging --api-key=$HEROKU_STAGING_API_KEY
+ only:
+ - master
+
+production:
+ type: deploy
+ script:
+ - apt-get update -qy
+ - apt-get install -y ruby-dev
+ - gem install dpl
+ - dpl --provider=heroku --app=gitlab-ci-python-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY
+ only:
+ - tags
+```
+
+This project has three jobs:
+1. `test` - used to test rails application,
+2. `staging` - used to automatically deploy staging environment every push to `master` branch
+3. `production` - used to automatically deploy production environmnet for every created tag
+
+### Store API keys
+You'll need to create two variables in `Project > Variables`:
+1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
+2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
+
+Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
+
+### Create Heroku application
+For each of your environments, you'll need to create a new Heroku application.
+You can do this through the [Dashboard](https://dashboard.heroku.com/).
+
+### Create runner
+First install [Docker Engine](https://docs.docker.com/installation/).
+To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
+You can use public runners available on `gitlab.com/ci`, but you can register your own:
+```
+gitlab-ci-multi-runner register \
+ --non-interactive \
+ --url "https://gitlab.com/ci/" \
+ --registration-token "PROJECT_REGISTRATION_TOKEN" \
+ --description "python-3.2" \
+ --executor "docker" \
+ --docker-image python:3.2 \
+ --docker-postgres latest
+```
+
+With the command above, you create a runner that uses [python:3.2](https://registry.hub.docker.com/u/library/python/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database.
+
+To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
new file mode 100644
index 0000000000..d2a872f193
--- /dev/null
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -0,0 +1,67 @@
+## Test and Deploy a ruby application
+This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application.
+
+You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://ci.gitlab.com/projects/4050).
+
+### Configure project
+This is what the `.gitlab-ci.yml` file looks like for this project:
+```yaml
+test:
+ script:
+ - apt-get update -qy
+ - apt-get install -y nodejs
+ - bundle install --path /cache
+ - bundle exec rake db:create RAILS_ENV=test
+ - bundle exec rake test
+
+staging:
+ type: deploy
+ script:
+ - gem install dpl
+ - dpl --provider=heroku --app=gitlab-ci-ruby-test-staging --api-key=$HEROKU_STAGING_API_KEY
+ only:
+ - master
+
+production:
+ type: deploy
+ script:
+ - gem install dpl
+ - dpl --provider=heroku --app=gitlab-ci-ruby-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY
+ only:
+ - tags
+```
+
+This project has three jobs:
+1. `test` - used to test rails application,
+2. `staging` - used to automatically deploy staging environment every push to `master` branch
+3. `production` - used to automatically deploy production environmnet for every created tag
+
+### Store API keys
+You'll need to create two variables in `Project > Variables`:
+1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
+2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
+
+Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account).
+
+### Create Heroku application
+For each of your environments, you'll need to create a new Heroku application.
+You can do this through the [Dashboard](https://dashboard.heroku.com/).
+
+### Create runner
+First install [Docker Engine](https://docs.docker.com/installation/).
+To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
+You can use public runners available on `gitlab.com/ci`, but you can register your own:
+```
+gitlab-ci-multi-runner register \
+ --non-interactive \
+ --url "https://gitlab.com/ci/" \
+ --registration-token "PROJECT_REGISTRATION_TOKEN" \
+ --description "ruby-2.1" \
+ --executor "docker" \
+ --docker-image ruby:2.1 \
+ --docker-postgres latest
+```
+
+With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database.
+
+To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
\ No newline at end of file
diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md
new file mode 100644
index 0000000000..eaee94a10f
--- /dev/null
+++ b/doc/ci/examples/test-clojure-application.md
@@ -0,0 +1,35 @@
+## Test Clojure applications
+
+This example will guide you how to run tests in your Clojure application.
+
+You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://ci.gitlab.com/projects/6306).
+
+### Configure project
+
+This is what the `.gitlab-ci.yml` file looks like for this project:
+
+```yaml
+variables:
+ POSTGRES_DB: sample-test
+ DATABASE_URL: "postgresql://postgres@postgres:5432/sample-test"
+
+before_script:
+ - apt-get update -y
+ - apt-get install default-jre postgresql-client -y
+ - wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
+ - chmod a+x lein
+ - export LEIN_ROOT=1
+ - PATH=$PATH:.
+ - lein deps
+ - lein migratus migrate
+
+test:
+ script:
+ - lein test
+```
+
+In before script we install JRE and [Leiningen](http://leiningen.org/).
+Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations.
+So we added database migration as last step of `before_script` section
+
+You can use public runners available on `gitlab.com` for testing your application with such configuration.
diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md
new file mode 100644
index 0000000000..d77061c14c
--- /dev/null
+++ b/doc/ci/permissions/README.md
@@ -0,0 +1,24 @@
+# Users Permissions
+
+GitLab CI relies on user's role on the GitLab. There are three permissions levels on GitLab CI: admin, master, developer, other.
+
+Admin user can perform any actions on GitLab CI in scope of instance and project. Also user with admin permission can use admin interface.
+
+
+
+
+| Action | Guest, Reporter | Developer | Master | Admin |
+|---------------------------------------|-----------------|-------------|----------|--------|
+| See commits and builds | ✓ | ✓ | ✓ | ✓ |
+| Retry or cancel build | | ✓ | ✓ | ✓ |
+| Remove project | | | ✓ | ✓ |
+| Create project | | | ✓ | ✓ |
+| Change project configuration | | | ✓ | ✓ |
+| Add specific runners | | | ✓ | ✓ |
+| Add shared runners | | | | ✓ |
+| See events in the system | | | | ✓ |
+| Admin interface | | | | ✓ |
+
+
+
+
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
new file mode 100644
index 0000000000..a87a1f806f
--- /dev/null
+++ b/doc/ci/quick_start/README.md
@@ -0,0 +1,119 @@
+# Quick Start
+
+To start building projects with GitLab CI a few steps needs to be done.
+
+## 1. Install GitLab and CI
+
+First you need to have a working GitLab and GitLab CI instance.
+
+You can omit this step if you use [GitLab.com](http://GitLab.com/).
+
+## 2. Create repository on GitLab
+
+Once you login on your GitLab add a new repository where you will store your source code.
+Push your application to that repository.
+
+## 3. Add project to CI
+
+The next part is to login to GitLab CI.
+Point your browser to the URL you have set GitLab or use [gitlab.com/ci](http://gitlab.com/ci/).
+
+On the first screen you will see a list of GitLab's projects that you have access to:
+
+
+
+Click **Add Project to CI**.
+This will create project in CI and authorize GitLab CI to fetch sources from GitLab.
+
+> GitLab CI creates unique token that is used to configure GitLab CI service in GitLab.
+> This token allows to access GitLab's repository and configures GitLab to trigger GitLab CI webhook on **Push events** and **Tag push events**.
+> You can see that token by going to Project's Settings > Services > GitLab CI.
+> You will see there token, the same token is assigned in GitLab CI settings of project.
+
+## 4. Create project's configuration - .gitlab-ci.yml
+
+The next: You have to define how your project will be built.
+GitLab CI uses [YAML](https://en.wikipedia.org/wiki/YAML) file to store build configuration.
+You need to create `.gitlab-ci.yml` in root directory of your repository:
+
+```yaml
+before_script:
+ - bundle install
+
+rspec:
+ script:
+ - bundle exec rspec
+
+rubocop:
+ script:
+ - bundle exec rubocop
+```
+
+This is the simplest possible build configuration that will work for most Ruby applications:
+1. Define two jobs `rspec` and `rubocop` with two different commands to be executed.
+1. Before every job execute commands defined by `before_script`.
+
+The `.gitlab-ci.yml` defines set of jobs with constrains how and when they should be run.
+The jobs are defined as top-level elements with name and always have to contain the `script`.
+Jobs are used to create builds, which are then picked by [runners](../runners/README.md) and executed within environment of the runner.
+What is important that each job is run independently from each other.
+
+For more information and complete `.gitlab-ci.yml` syntax, please check the [Configuring project (.gitlab-ci.yml)](../yaml/README.md).
+
+## 5. Add file and push .gitlab-ci.yml to repository
+
+Once you created `.gitlab-ci.yml` you should add it to git repository and push it to GitLab.
+
+```bash
+git add .gitlab-ci.yml
+git commit
+git push origin master
+```
+
+If you refresh the project's page on GitLab CI you will notice a one new commit:
+
+
+
+However the commit has status **pending** which means that commit was not yet picked by runner.
+
+## 6. Configure runner
+
+In GitLab CI, Runners run your builds.
+A runner is a machine (can be virtual, bare-metal or VPS) that picks up builds through the coordinator API of GitLab CI.
+
+A runner can be specific to a certain project or serve any project in GitLab CI.
+A runner that serves all projects is called a shared runner.
+More information about different runner types can be found in [Configuring runner](../runners/README.md).
+
+To check if you have runners assigned to your project go to **Runners**. You will find there information how to setup project specific runner:
+
+1. Install GitLab Runner software. Checkout the [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner) section to install it.
+1. Specify following URL during runner setup: https://gitlab.com/ci/
+1. Use the following registration token during setup: TOKEN
+
+If you do it correctly your runner should be shown under **Runners activated for this project**:
+
+
+
+### Shared runners
+
+If you use [gitlab.com/ci](http://gitlab.com/ci/) you can use **Shared runners** provided by GitLab Inc.
+These are special virtual machines that are run on GitLab's infrastructure that can build any project.
+To enable **Shared runners** you have to go to **Runners** and click **Enable shared runners** for this project.
+
+## 7. Check status of commit
+
+If everything went OK and you go to commit, the status of the commit should change from **pending** to either **running**, **success** or **failed**.
+
+
+
+You can click **Build ID** to view build log for specific job.
+
+## 8. Congratulations!
+
+You managed to build your first project using GitLab CI.
+You may need to tune your `.gitlab-ci.yml` file to implement build plan for your project.
+A few examples how it can be done you can find on [Examples](../examples/README.md) page.
+
+GitLab CI also offers **the Lint** tool to verify validity of your `.gitlab-ci.yml` which can be useful to troubleshoot potential problems.
+The Lint is available from project's settings or by adding `/lint` to GitLab CI url.
diff --git a/doc/ci/quick_start/build_status.png b/doc/ci/quick_start/build_status.png
new file mode 100644
index 0000000000..333259e6ac
Binary files /dev/null and b/doc/ci/quick_start/build_status.png differ
diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/commit_status.png
new file mode 100644
index 0000000000..725b79e6f9
Binary files /dev/null and b/doc/ci/quick_start/commit_status.png differ
diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/new_commit.png
new file mode 100644
index 0000000000..3839e893c1
Binary files /dev/null and b/doc/ci/quick_start/new_commit.png differ
diff --git a/doc/ci/quick_start/projects.png b/doc/ci/quick_start/projects.png
new file mode 100644
index 0000000000..0b3430a69d
Binary files /dev/null and b/doc/ci/quick_start/projects.png differ
diff --git a/doc/ci/quick_start/runners.png b/doc/ci/quick_start/runners.png
new file mode 100644
index 0000000000..25b4046bc0
Binary files /dev/null and b/doc/ci/quick_start/runners.png differ
diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/runners_activated.png
new file mode 100644
index 0000000000..c934bd12f4
Binary files /dev/null and b/doc/ci/quick_start/runners_activated.png differ
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
new file mode 100644
index 0000000000..68dcfe23ff
--- /dev/null
+++ b/doc/ci/runners/README.md
@@ -0,0 +1,145 @@
+# Runners
+
+In GitLab CI, Runners run your [yaml](../yaml/README.md).
+A runner is an isolated (virtual) machine that picks up builds
+through the coordinator API of GitLab CI.
+
+A runner can be specific to a certain project or serve any project
+in GitLab CI. A runner that serves all projects is called a shared runner.
+
+## Shared vs. Specific Runners
+
+A runner that is specific only runs for the specified project. A shared runner
+can run jobs for every project that has enabled the option
+`Allow shared runners`.
+
+**Shared runners** are useful for jobs that have similar requirements,
+between multiple projects. Rather than having multiple runners idling for
+many projects, you can have a single or a small number of runners that handle
+multiple projects. This makes it easier to maintain and update runners.
+
+**Specific runners** are useful for jobs that have special requirements or for
+projects with a very demand. If a job has certain requirements, you can set
+up the specific runner with this in mind, while not having to do this for all
+runners. For example, if you want to deploy a certain project, you can setup
+a specific runner to have the right credentials for this.
+
+Projects with high demand of CI activity can also benefit from using specific runners.
+By having dedicated runners you are guaranteed that the runner is not being held
+up by another project's jobs.
+
+You can set up a specific runner to be used by multiple projects. The difference
+with a shared runner is that you have to enable each project explicitly for
+the runner to be able to run its jobs.
+
+Specific runners do not get shared with forked projects automatically.
+A fork does copy the CI settings (jobs, allow shared, etc) of the cloned repository.
+
+# Creating and Registering a Runner
+
+There are several ways to create a runner. Only after creation, upon
+registration its status as Shared or Specific is determined.
+
+[See the documentation for](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation)
+the different methods of installing a Runner instance.
+
+After installing the runner, you can either register it as `Shared` or as `Specific`.
+You can only register a Shared Runner if you have admin access to the GitLab instance.
+
+## Registering a Shared Runner
+
+You can only register a shared runner if you are an admin on the linked
+GitLab instance.
+
+Grab the shared-runner token on the `admin/runners` page of your GitLab CI
+instance.
+
+
+
+Now simply register the runner as any runner:
+
+```
+sudo gitlab-runner register
+```
+
+Note that you will have to enable `Allows shared runners` for each project
+that you want to make use of a shared runner. This is by default `off`.
+
+## Registering a Specific Runner
+
+Registering a specific can be done in two ways:
+
+1. Creating a runner with the project registration token
+1. Converting a shared runner into a specific runner (one-way, admin only)
+
+There are several ways to create a runner instance. The steps below only
+concern registering the runner on GitLab CI.
+
+### Registering a Specific Runner with a Project Registration token
+
+To create a specific runner without having admin rights to the GitLab instance,
+visit the project you want to make the runner work for in GitLab CI.
+
+Click on the runner tab and use the registration token you find there to
+setup a specific runner for this project.
+
+
+
+To register the runner, run the command below and follow instructions:
+
+```
+sudo gitlab-runner register
+```
+
+### Making an existing Shared Runner Specific
+
+If you are an admin on your GitLab instance,
+you can make any shared runner a specific runner, _but you can not
+make a specific runner a shared runner_.
+
+To make a shared runner specific, go to the runner page (`/admin/runners`)
+and find your runner. Add any projects on the left to make this runner
+run exclusively for these projects, therefore making it a specific runner.
+
+
+
+## Using Shared Runners Effectively
+
+If you are planning to use shared runners, there are several things you
+should keep in mind.
+
+### Use Tags
+
+You must setup a runner to be able to run all the different types of jobs
+that it may encounter on the projects it's shared over. This would be
+problematic for large amounts of projects, if it wasn't for tags.
+
+By tagging a Runner for the types of jobs it can handle, you can make sure
+shared runners will only run the jobs they are equipped to run.
+
+For instance, at GitLab we have runners tagged with "rails" if they contain
+the appropriate dependencies to run Rails test suites.
+
+### Be Careful with Sensitive Information
+
+If you can run a build on a runner, you can get access to any code it runs
+and get the token of the runner. With shared runners, this means that anyone
+that runs jobs on the runner, can access anyone else's code that runs on the runner.
+
+In addition, because you can get access to the runner token, it is possible
+to create a clone of a runner and submit false builds, for example.
+
+The above is easily avoided by restricting the usage of shared runners
+on large public GitLab instances and controlling access to your GitLab instance.
+
+### Forks
+
+Whenever a project is forked, it copies the settings of the jobs that relate
+to it. This means that if you have shared runners setup for a project and
+someone forks that project, the shared runners will also serve jobs of this
+project.
+
+# Attack vectors in runners
+
+Mentioned briefly earlier, but the following things of runners can be exploited.
+We're always looking for contributions that can mitigate these [Security Considerations](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md).
diff --git a/doc/ci/runners/project_specific.png b/doc/ci/runners/project_specific.png
new file mode 100644
index 0000000000..f51ea694e7
Binary files /dev/null and b/doc/ci/runners/project_specific.png differ
diff --git a/doc/ci/runners/shared_runner.png b/doc/ci/runners/shared_runner.png
new file mode 100644
index 0000000000..9755144eb0
Binary files /dev/null and b/doc/ci/runners/shared_runner.png differ
diff --git a/doc/ci/runners/shared_to_specific_admin.png b/doc/ci/runners/shared_to_specific_admin.png
new file mode 100644
index 0000000000..44a4bef22f
Binary files /dev/null and b/doc/ci/runners/shared_to_specific_admin.png differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
new file mode 100644
index 0000000000..04c6bf1e3a
--- /dev/null
+++ b/doc/ci/variables/README.md
@@ -0,0 +1,95 @@
+## Variables
+When receiving a build from GitLab CI, the runner prepares the build environment.
+It starts by setting a list of **predefined variables** (Environment Variables) and a list of **user-defined variables**
+
+The variables can be overwritten. They take precedence over each other in this order:
+1. Secure variables
+1. YAML-defined variables
+1. Predefined variables
+
+For example, if you define:
+1. API_TOKEN=SECURE as Secure Variable
+1. API_TOKEN=YAML as YAML-defined variable
+
+The API_TOKEN will take the Secure Variable value: `SECURE`.
+
+### Predefined variables (Environment Variables)
+
+| Variable | Description |
+|-------------------------|-------------|
+| **CI** | Mark that build is executed in CI environment |
+| **GITLAB_CI** | Mark that build is executed in GitLab CI environment |
+| **CI_SERVER** | Mark that build is executed in CI environment |
+| **CI_SERVER_NAME** | CI server that is used to coordinate builds |
+| **CI_SERVER_VERSION** | Not yet defined |
+| **CI_SERVER_REVISION** | Not yet defined |
+| **CI_BUILD_REF** | The commit revision for which project is built |
+| **CI_BUILD_BEFORE_SHA** | The first commit that were included in push request |
+| **CI_BUILD_REF_NAME** | The branch or tag name for which project is built |
+| **CI_BUILD_ID** | The unique id of the current build that GitLab CI uses internally |
+| **CI_BUILD_REPO** | The URL to clone the Git repository |
+| **CI_PROJECT_ID** | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_DIR** | The full path where the repository is cloned and where the build is ran |
+
+Example values:
+
+```bash
+export CI_BUILD_BEFORE_SHA="9df57456fa9de2a6d335ca5edf9750ed812b9df0"
+export CI_BUILD_ID="50"
+export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
+export CI_BUILD_REF_NAME="master"
+export CI_BUILD_REPO="https://gitlab.com/gitlab-org/gitlab-ce.git"
+export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
+export CI_PROJECT_ID="34"
+export CI_SERVER="yes"
+export CI_SERVER_NAME="GitLab CI"
+export CI_SERVER_REVISION=""
+export CI_SERVER_VERSION=""
+```
+
+### YAML-defined variables
+**This feature requires GitLab Runner 0.5.0 or higher**
+
+GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment.
+The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL.
+
+```yaml
+variables:
+ DATABASE_URL: "postgres://postgres@postgres/my_database"
+```
+
+These variables can be later used in all executed commands and scripts.
+
+The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
+
+More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
+
+### User-defined variables (Secure Variables)
+**This feature requires GitLab Runner 0.4.0 or higher**
+
+GitLab CI allows you to define per-project **Secure Variables** that are set in build environment.
+The secure variables are stored out of the repository (the `.gitlab-ci.yml`).
+These variables are securely stored in GitLab CI database and are hidden in the build log.
+It's desired method to use them for storing passwords, secret keys or whatever you want.
+
+Secure Variables can added by going to `Project > Variables > Add Variable`.
+
+They will be available for all subsequent builds.
+
+### Use variables
+The variables are set as environment variables in build environment and are accessible with normal methods that are used to access such variables.
+In most cases the **bash** is used to execute build script.
+To access variables (predefined and user-defined) in bash environment, prefix the variable name with `$`:
+```
+job_name:
+ script:
+ - echo $CI_BUILD_ID
+```
+
+You can also list all environment variables with `export` command,
+but be aware that this will also expose value of all **Secure Variables** in build log:
+```
+job_name:
+ script:
+ - export
+```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
new file mode 100644
index 0000000000..4caeccacb7
--- /dev/null
+++ b/doc/ci/yaml/README.md
@@ -0,0 +1,204 @@
+# Configuration of your builds with .gitlab-ci.yml
+From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (**.gitlab-ci.yml**) for the project configuration.
+It is placed in the root of your repository and contains definitions of how your project should be built.
+
+The YAML file defines a set of jobs with constraints stating when they should be run.
+The jobs are defined as top-level elements with a name and always have to contain the `script` clause:
+
+```yaml
+job1:
+ script: "execute-script-for-job1"
+
+job2:
+ script: "execute-script-for-job2"
+```
+
+The above example is the simplest possible CI configuration with two separate jobs,
+where each of the jobs executes a different command.
+Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository.
+
+Jobs are used to create builds, which are then picked up by [runners](../runners/README.md) and executed within the environment of the runner.
+What is important, is that each job is run independently from each other.
+
+## .gitlab-ci.yml
+The YAML syntax allows for using more complex job specifications than in the above example:
+
+```yaml
+image: ruby:2.1
+services:
+ - postgres
+
+before_script:
+ - bundle_install
+
+stages:
+ - build
+ - test
+ - deploy
+
+job1:
+ stage: build
+ script:
+ - execute-script-for-job1
+ only:
+ - master
+ tags:
+ - docker
+```
+
+There are a few `keywords` that can't be used as job names:
+
+| keyword | required | description |
+|---------------|----------|-------------|
+| image | optional | Use docker image, covered in [Use Docker](../docker/README.md) |
+| services | optional | Use docker services, covered in [Use Docker](../docker/README.md) |
+| stages | optional | Define build stages |
+| types | optional | Alias for `stages` |
+| before_script | optional | Define commands prepended for each job's script |
+| variables | optional | Define build variables |
+
+### image and services
+This allows to specify a custom Docker image and a list of services that can be used for time of the build.
+The configuration of this feature is covered in separate document: [Use Docker](../docker/README.md).
+
+### before_script
+`before_script` is used to define the command that should be run before all builds, including deploy builds. This can be an array or a multiline string.
+
+### stages
+`stages` is used to define build stages that can be used by jobs.
+The specification of `stages` allows for having flexible multi stage pipelines.
+
+The ordering of elements in `stages` defines the ordering of builds' execution:
+
+1. Builds of the same stage are run in parallel.
+1. Builds of next stage are run after success.
+
+Let's consider the following example, which defines 3 stages:
+```
+stages:
+ - build
+ - test
+ - deploy
+```
+
+1. First all jobs of `build` are executed in parallel.
+1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel.
+1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel.
+1. If all jobs of `deploy` succeeds, the commit is marked as `success`.
+1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed.
+
+There are also two edge cases worth mentioning:
+
+1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, `test` and `deploy` are allowed to be used as job's stage by default.
+2. If a job doesn't specify `stage`, the job is assigned the `test` stage.
+
+### types
+Alias for [stages](#stages).
+
+### variables
+**This feature requires `gitlab-runner` with version equal or greater than 0.5.0.**
+
+GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment.
+The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL.
+
+```yaml
+variables:
+ DATABASE_URL: "postgres://postgres@postgres/my_database"
+```
+
+These variables can be later used in all executed commands and scripts.
+
+The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
+
+## Jobs
+`.gitlab-ci.yml` allows you to specify an unlimited number of jobs.
+Each job has to have a unique `job_name`, which is not one of the keywords mentioned above.
+A job is defined by a list of parameters that define the build behaviour.
+
+```yaml
+job_name:
+ script:
+ - rake spec
+ - coverage
+ stage: test
+ only:
+ - master
+ except:
+ - develop
+ tags:
+ - ruby
+ - postgres
+ allow_failure: true
+```
+
+| keyword | required | description |
+|---------------|----------|-------------|
+| script | required | Defines a shell script which is executed by runner |
+| stage | optional (default: test) | Defines a build stage |
+| type | optional | Alias for `stage` |
+| only | optional | Defines a list of git refs for which build is created |
+| except | optional | Defines a list of git refs for which build is not created |
+| tags | optional | Defines a list of tags which are used to select runner |
+| allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status |
+
+### script
+`script` is a shell script which is executed by runner. The shell script is prepended with `before_script`.
+
+```yaml
+job:
+ script: "bundle exec rspec"
+```
+
+This parameter can also contain several commands using an array:
+```yaml
+job:
+ script:
+ - uname -a
+ - bundle exec rspec
+```
+
+### stage
+`stage` allows to group build into different stages. Builds of the same `stage` are executed in `parallel`.
+For more info about the use of `stage` please check the [stages](#stages).
+
+### only and except
+This are two parameters that allow for setting a refs policy to limit when jobs are built:
+1. `only` defines the names of branches and tags for which job will be built.
+2. `except` defines the names of branches and tags for which the job wil **not** be built.
+
+There are a few rules that apply to usage of refs policy:
+
+1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account.
+1. `only` and `except` allow for using the regexp expressions.
+1. `only` and `except` allow for using special keywords: `branches` and `tags`.
+These names can be used for example to exclude all tags and all branches.
+
+```yaml
+job:
+ only:
+ - /^issue-.*$/ # use regexp
+ except:
+ - branches # use special keyword
+```
+
+### tags
+`tags` is used to select specific runners from the list of all runners that are allowed to run this project.
+
+During registration of a runner, you can specify the runner's tags, ie.: `ruby`, `postgres`, `development`.
+`tags` allow you to run builds with runners that have the specified tags assigned:
+
+```
+job:
+ tags:
+ - ruby
+ - postgres
+```
+
+The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined.
+
+## Validate the .gitlab-ci.yml
+Each instance of GitLab CI has an embedded debug tool called Lint.
+You can find the link to the Lint in the project's settings page or use short url `/lint`.
+
+## Skipping builds
+There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped
\ No newline at end of file
diff --git a/doc/install/installation.md b/doc/install/installation.md
index ee13b0f253..3b074fc846 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -207,9 +207,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-14-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-0-stable gitlab
-**Note:** You can change `7-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -222,6 +222,10 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Update GitLab config file, follow the directions at top of file
sudo -u git -H editor config/gitlab.yml
+ # Copy the example secrets file
+ sudo -u git -H cp config/secrets.yml.example config/secrets.yml
+ sudo -u git -H chmod 0600 config/secrets.yml
+
# Make sure GitLab can write to the log/ and tmp/ directories
sudo chown -R git log/
sudo chown -R git tmp/
@@ -235,6 +239,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Make sure GitLab can write to the public/uploads/ directory
sudo chmod -R u+rwX public/uploads
+ # Change the permissions of the directory where CI build traces are stored
+ sudo chmod -R u+rwX builds/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -328,6 +335,17 @@ GitLab Shell is an SSH access and repository management software developed speci
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword
+### Secure secrets.yml
+
+The `secrets.yml` file stores encryption keys for sessions and secure variables.
+Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups.
+Otherwise your secrets are exposed if one of your backups is compromised.
+
+### Install schedules
+
+ # Setup schedules
+ sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production
+
### Install Init Script
Download the init script (will be `/etc/init.d/gitlab`):
@@ -491,3 +509,8 @@ You can configure LDAP authentication in `config/gitlab.yml`. Please restart Git
### Using Custom Omniauth Providers
See the [omniauth integration document](../integration/omniauth.md)
+
+### Build your projects
+
+GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you.
+Checkout the [Gitlab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 904d5d7fee..3bc5df21ef 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -78,6 +78,26 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
#
user_filter: ''
+ # LDAP attributes that GitLab will use to create an account for the LDAP user.
+ # The specified attribute can either be the attribute name as a string (e.g. 'mail'),
+ # or an array of attribute names to try in order (e.g. ['mail', 'email']).
+ # Note that the user's LDAP login will always be the attribute specified as `uid` above.
+ attributes:
+ # The username will be used in paths for the user's own projects
+ # (like `gitlab.example.com/username/project`) and when mentioning
+ # them in issues, merge request and comments (like `@username`).
+ # If the attribute specified for `username` contains an email address,
+ # the GitLab username will be the part of the email address before the '@'.
+ username: ['uid', 'userid', 'sAMAccountName']
+ email: ['mail', 'email', 'userPrincipalName']
+
+ # If no full name could be found at the attribute specified for `name`,
+ # the full name is determined using the attributes specified for
+ # `first_name` and `last_name`.
+ name: 'cn'
+ first_name: 'givenName'
+ last_name: 'sn'
+
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
# so that GitLab can remember which LDAP server a user belongs to.
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index 322111ae9e..6fdb2fe149 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -413,7 +413,7 @@ Some text to show that the reference links can follow later.
Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example:
-`[I'm a reference-style link][style]`
+`[I'm a reference-style link](style)`
will point the link to `wikis/style` when the link is inside of a wiki markdown file.
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
new file mode 100644
index 0000000000..13efc8442d
--- /dev/null
+++ b/doc/migrate_ci_to_ce/README.md
@@ -0,0 +1,273 @@
+## Migrate GitLab CI to GitLab CE/EE
+
+## Notice
+
+**You need to have working GitLab CI 7.14 to perform migration.
+The older versions are not supported and will most likely break migration procedure.**
+
+This migration can't be done online and takes significant amount of time.
+Make sure to plan it ahead.
+
+If you are running older version please follow the upgrade guide first:
+https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/update/7.13-to-7.14.md
+
+The migration is divided into a two parts:
+1. **[CI]** You will be making a changes to GitLab CI instance.
+1. **[CE]** You will be making a changes to GitLab CE/EE instance.
+
+### 1. Stop CI server [CI]
+
+ sudo service gitlab_ci stop
+
+### 2. Backup [CI]
+
+**The migration procedure is database breaking.
+You need to create backup if you still want to access GitLab CI in case of failure.**
+
+```bash
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec backup:create RAILS_ENV=production
+```
+
+### 3. Prepare GitLab CI database to migration [CI]
+
+Copy and paste the command in terminal to rename all tables.
+This also breaks your database structure disallowing you to use it anymore.
+
+ cat < gitlab_ci.sql
+
+#### b. Dump PostgreSQL
+
+ pg_dump -h DB_HOSTNAME -U DB_USERNAME -p DB_PORT --data-only GITLAB_CI_DATABASE -t "ci_*" > gitlab_ci.sql
+
+#### c. Dump MySQL and migrate to PostgreSQL
+
+ # Dump existing MySQL database first
+ mysqldump --default-character-set=utf8 --compatible=postgresql --complete-insert \
+ --host=DB_USERNAME --port=DB_PORT --user=DB_HOSTNAME -p
+ GITLAB_CI_DATABASE \
+ ci_application_settings ci_builds ci_commits ci_events ci_jobs ci_projects \
+ ci_runner_projects ci_runners ci_services ci_tags ci_taggings ci_trigger_requests \
+ ci_triggers ci_variables ci_web_hooks > gitlab_ci.sql.tmp
+
+ # Convert database to be compatible with PostgreSQL
+ git clone https://github.com/gitlabhq/mysql-postgresql-converter.git -b gitlab
+ python mysql-postgresql-converter/db_converter.py gitlab_ci.sql.tmp gitlab_ci.sql.tmp2
+ ed -s gitlab_ci.sql.tmp2 < mysql-postgresql-converter/move_drop_indexes.ed
+
+ # Filter to only include INSERT statements
+ grep "^\(START\|SET\|INSERT\|COMMIT\)" gitlab_ci.sql.tmp2 > gitlab_ci.sql
+
+### 5. Make sure that your GitLab CE/EE is 8.0 [CE]
+
+Please verify that you use GitLab CE/EE 8.0.
+If not, please follow the update guide: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.14-to-8.0.md
+
+### 6. Stop GitLab CE/EE [CE]
+
+Before you can migrate data you need to stop GitLab CE/EE first.
+
+ sudo service gitlab stop
+
+### 7. Backup GitLab CE/EE [CE]
+
+This migration poses a **significant risk** of breaking your GitLab CE/EE.
+**You should create the GitLab CI/EE backup before doing it.**
+
+ cd /home/git/gitlab
+ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+
+### 8. Copy secret tokens [CE]
+
+The `secrets.yml` file stores encryption keys for secure variables.
+
+You need to copy the content of `config/secrets.yml` to the same file in GitLab CE.
+
+ sudo cp /home/gitlab_ci/gitlab-ci/config/secrets.yml /home/git/gitlab/config/secrets.yml
+ sudo chown git:git /home/git/gitlab/config/secrets.yml
+ sudo chown 0600 /home/git/gitlab/config/secrets.yml
+
+### 9. New configuration options for `gitlab.yml` [CE]
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example).
+View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/7-14-stable:config/gitlab.yml.example origin/8-0-stable:config/gitlab.yml.example
+```
+
+The new options include configuration of GitLab CI that are now being part of GitLab CE and EE.
+
+### 10. Copy build logs [CE]
+
+You need to copy the contents of `builds/` to the same directory in GitLab CE/EE.
+
+ sudo rsync -av /home/gitlab_ci/gitlab-ci/builds /home/git/gitlab/builds
+ sudo chown -R git:git /home/git/gitlab/builds
+
+The build traces are usually quite big so it will take a significant amount of time.
+
+### 11. Import GitLab CI database [CE]
+
+The one of the last steps is to import existing GitLab CI database.
+
+ sudo mv /home/gitlab_ci/gitlab-ci/gitlab_ci.sql /home/git/gitlab/gitlab_ci.sql
+ sudo chown git:git /home/git/gitlab/gitlab_ci.sql
+ sudo -u git -H bundle exec rake ci:migrate CI_DUMP=/home/git/gitlab/gitlab_ci.sql RAILS_ENV=production
+
+The task does:
+1. Delete data from all existing CI tables
+1. Import database data
+1. Fix database auto increments
+1. Fix tags assigned to Builds and Runners
+1. Fix services used by CI
+
+### 12. Start GitLab [CE]
+
+You can start GitLab CI/EE now and see if everything is working.
+
+ sudo service gitlab start
+
+### 13. Update nginx [CI]
+
+Now get back to GitLab CI and update **Nginx** configuration in order to:
+1. Have all existing runners able to communicate with a migrated GitLab CI.
+1. Have GitLab able send build triggers to CI address specified in Project's settings -> Services -> GitLab CI.
+
+You need to edit `/etc/nginx/sites-available/gitlab_ci` and paste:
+
+ # GITLAB CI
+ server {
+ listen 80 default_server; # e.g., listen 192.168.1.1:80;
+ server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com;
+
+ access_log /var/log/nginx/gitlab_ci_access.log;
+ error_log /var/log/nginx/gitlab_ci_error.log;
+
+ # expose API to fix runners
+ location /api {
+ proxy_read_timeout 300;
+ proxy_connect_timeout 300;
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN
+ resolver 8.8.8.8 8.8.4.4;
+ proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # expose build endpoint to allow trigger builds
+ location ~ ^/projects/\d+/build$ {
+ proxy_read_timeout 300;
+ proxy_connect_timeout 300;
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN
+ resolver 8.8.8.8 8.8.4.4;
+ proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # redirect all other CI requests
+ location / {
+ return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # adjust this to match the largest build log your runners might submit,
+ # set to 0 to disable limit
+ client_max_body_size 10m;
+ }
+
+Make sure to fill the blanks to match your setup:
+1. **YOUR_CI_SERVER_FQDN**: The existing public facing address of GitLab CI, eg. ci.gitlab.com.
+1. **YOUR_GITLAB_SERVER_FQDN**: The public facing address of GitLab CE/EE, eg. gitlab.com.
+
+**Make sure to not remove the `/ci$request_uri`. This is required to properly forward the requests.**
+
+You should also make sure that you can do:
+1. `curl https://YOUR_GITLAB_SERVER_FQDN/` from your previous GitLab CI server.
+1. `curl https://YOUR_CI_SERVER_FQDN/` from your GitLab CE/EE server.
+
+## Check your configuration
+
+ sudo nginx -t
+
+## Restart nginx
+
+ sudo /etc/init.d/nginx restart
+
+### 14. Done!
+
+If everything went OK you should be able to access all your GitLab CI data by pointing your browser to:
+https://gitlab.example.com/ci/.
+
+The GitLab CI should also work when using the previous address, redirecting you to the GitLab CE/EE.
+
+**Enjoy!**
diff --git a/doc/release/monthly.md b/doc/release/monthly.md
index c1ed9e3b80..c56e99a700 100644
--- a/doc/release/monthly.md
+++ b/doc/release/monthly.md
@@ -195,7 +195,7 @@ This can happen before tagging because Omnibus uses tags in its own repo and SHA
## Update GitLab.com with the stable version
- Deploy the package (should not need downtime because of the small difference with RC1)
-- Deploy the package for ci.gitlab.com
+- Deploy the package for gitlab.com/ci
## Release CE, EE and CI
diff --git a/doc/reply_by_email/README.md b/doc/reply_by_email/README.md
index 5d36f5121d..e9187298d7 100644
--- a/doc/reply_by_email/README.md
+++ b/doc/reply_by_email/README.md
@@ -2,11 +2,48 @@
GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails.
-In order to do this, you need access to an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the [Postfix](http://www.postfix.org/) mail server which you can run on-premises.
+## Get a mailbox
+
+Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises.
+
+If you want to use Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md).
## Set it up
-In this example, we'll use the Gmail address `gitlab-replies@gmail.com`. If you're actually using Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+In this example, we'll use the Gmail address `gitlab-replies@gmail.com`.
+
+### Omnibus package installations
+
+1. Find the `reply_by_email` section in `/etc/gitlab/gitlab.rb`, enable the feature, enter the email address including a placeholder for the `reply_key` and fill in the details for your specific IMAP server and email account:
+
+ ```ruby
+ gitlab_rails['reply_by_email_enabled'] = true
+ gitlab_rails['reply_by_email_address'] = "gitlab-replies+%{reply_key}@gmail.com"
+ gitlab_rails['reply_by_email_host'] = "imap.gmail.com" # IMAP server host
+ gitlab_rails['reply_by_email_port'] = 993 # IMAP server port
+ gitlab_rails['reply_by_email_ssl'] = true # Whether the IMAP server uses SSL
+ gitlab_rails['reply_by_email_email'] = "gitlab-replies@gmail.com" # Email account username. Usually the full email address.
+ gitlab_rails['reply_by_email_password'] = "password" # Email account password
+ gitlab_rails['reply_by_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox".
+ ```
+
+ As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-replies@gmail.com`.
+
+1. Reconfigure GitLab for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo gitlab-rake gitlab:reply_by_email:check
+ ```
+
+1. Reply by email should now be working.
### Installations from source
@@ -21,17 +58,17 @@ In this example, we'll use the Gmail address `gitlab-replies@gmail.com`. If you'
```sh
sudo editor config/gitlab.yml
```
-
+
```yaml
reply_by_email:
enabled: true
address: "gitlab-replies+%{reply_key}@gmail.com"
```
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`.
+ As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-replies@gmail.com`.
+
+2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`:
-2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`:
-
```sh
sudo cp config/mail_room.yml.example config/mail_room.yml
```
@@ -72,47 +109,33 @@ In this example, we'll use the Gmail address `gitlab-replies@gmail.com`. If you'
:worker: EmailReceiverWorker
```
-
-4. Find `lib/support/init.d/gitlab.default.example` and copy it to `/etc/default/gitlab`:
-
- ```sh
- sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab
- ```
-
-5. Edit `/etc/default/gitlab` to enable `mail_room`:
+5. Edit the init script configuration at `/etc/default/gitlab` to enable `mail_room`:
```sh
- sudo editor /etc/default/gitlab
- ```
-
- ```sh
- mail_room_enabled=true
+ sudo mkdir -p /etc/default
+ echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
```
6. Restart GitLab:
-
+
```sh
sudo service gitlab restart
```
-7. Check if everything is configured correctly:
+7. Verify that everything is configured correctly:
```sh
- sudo bundle exec rake gitlab:reply_by_email:check RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:reply_by_email:check RAILS_ENV=production
```
8. Reply by email should now be working.
-### Omnibus package installations
-
-TODO
-
### Development
1. Go to the GitLab installation directory.
1. Find the `reply_by_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `reply_key`:
-
+
```yaml
reply_by_email:
enabled: true
@@ -121,8 +144,8 @@ TODO
As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`.
-2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`:
-
+2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`:
+
```sh
sudo cp config/mail_room.yml.example config/mail_room.yml
```
@@ -166,12 +189,12 @@ TODO
```
6. Restart GitLab:
-
+
```sh
bundle exec foreman start
```
-7. Check if everything is configured correctly:
+7. Verify that everything is configured correctly:
```sh
bundle exec rake gitlab:reply_by_email:check RAILS_ENV=development
diff --git a/doc/reply_by_email/postfix.md b/doc/reply_by_email/postfix.md
new file mode 100644
index 0000000000..b8ab07d9fe
--- /dev/null
+++ b/doc/reply_by_email/postfix.md
@@ -0,0 +1,310 @@
+# Set up Postfix for Reply by email
+
+This document will take you through the steps of setting up a basic Postfix mail server with IMAP authentication on Ubuntu, to be used with Reply by email.
+
+The instructions make the assumption that you will be using the email address `replies@gitlab.example.com`, that is, username `replies` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
+
+## Configure your server firewall
+
+1. Open up port 25 on your server so that people can send email into the server over SMTP.
+2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
+
+## Install packages
+
+1. Install the `postfix` package if it is not installed already:
+
+ ```sh
+ sudo apt-get install postfix
+ ```
+
+ When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
+
+1. Install the `mailutils` package.
+
+ ```sh
+ sudo apt-get install mailutils
+ ```
+
+## Create user
+
+1. Create a user for replies.
+
+ ```sh
+ sudo useradd -m -s /bin/bash replies
+ ```
+
+1. Set a password for this user.
+
+ ```sh
+ sudo passwd replies
+ ```
+
+ Be sure not to forget this, you'll need it later.
+
+## Test the out-of-the-box setup
+
+1. Connect to the local SMTP server:
+
+ ```sh
+ telnet localhost 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 127.0.0.1...
+ Connected to localhost.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, verify that `postfix` is running:
+
+ ```sh
+ sudo postfix status
+ ```
+
+ If it is not, start it:
+
+ ```sh
+ sudo postfix start
+ ```
+
+1. Send the new `replies` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo localhost
+ mail from: root@localhost
+ rcpt to: replies@localhost
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ (Note: The `.` is a literal period on its own line)
+
+1. Check if the `replies` user received the email:
+
+ ```sh
+ su - replies
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/var/mail/replies": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+1. Log out of the `replies` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Configure Postfix to use Maildir-style mailboxes
+
+Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
+
+1. Configure Postfix to use Maildir-style mailboxes:
+
+ ```sh
+ sudo postconf -e "home_mailbox = Maildir/"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo /etc/init.d/postfix restart
+ ```
+
+1. Test the new setup:
+
+ 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
+ 2. Check if the `replies` user received the email:
+
+ ```sh
+ su - replies
+ MAIL=/home/replies/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/replies/Maildir": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+1. Log out of the `replies` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Install the Courier IMAP server
+
+1. Install the `courier-imap` package:
+
+ ```sh
+ sudo apt-get install courier-imap
+ ```
+
+## Configure Postfix to receive email from the internet
+
+1. Let Postfix know about the domains that it should consider local:
+
+ ```sh
+ sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
+ ```
+
+1. Let Postfix know about the IPs that it should consider part of the LAN:
+
+ We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
+
+ ```sh
+ sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
+ ```
+
+1. Configure Postfix to receive mail on all interfaces, which includes the internet:
+
+ ```sh
+ sudo postconf -e "inet_interfaces = all"
+ ```
+
+1. Configure Postfix to use the `+` delimiter for sub-addressing:
+
+ ```sh
+ sudo postconf -e "recipient_delimiter = +"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo service postfix restart
+ ```
+
+## Test the final setup
+
+1. Test SMTP under the new setup:
+
+ 1. Connect to the SMTP server:
+
+ ```sh
+ telnet gitlab.example.com 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to gitlab.example.com.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
+
+ 1. Send the `replies` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo gitlab.example.com
+ mail from: root@gitlab.example.com
+ rcpt to: replies@gitlab.example.com
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ (Note: The `.` is a literal period on its own line)
+
+ 1. Check if the `replies` user received the email:
+
+ ```sh
+ su - replies
+ MAIL=/home/replies/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/replies/Maildir": 1 message 1 unread
+ >U 1 root@gitlab.example.com 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+ 1. Log out of the `replies` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+1. Test IMAP under the new setup:
+
+ 1. Connect to the IMAP server:
+
+ ```sh
+ telnet gitlab.example.com 143
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to mail.example.gitlab.com.
+ Escape character is '^]'.
+ - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information.
+ ```
+
+ 1. Sign in as the `replies` user to test IMAP, by entering the following into the IMAP prompt:
+
+ ```
+ a login replies PASSWORD
+ ```
+
+ Replace PASSWORD with the password you set on the `replies` user earlier.
+
+ You should see output like this:
+
+ ```
+ a OK LOGIN Ok.
+ ```
+
+ 1. Disconnect from the IMAP server:
+
+ ```sh
+ a logout
+ ```
+
+## Done!
+
+If all the tests were successfull, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+
+---------
+
+_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 29a38d07b3..2c7003ed06 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -10,9 +10,9 @@ months after this vulnerability became known the GitLab installation guide
still contained instructions that would install the outdated, 'vulnerable' Git
version 2.1.2.
-Run the following command to get your current Git version.
+Run the following command to get your current Git version:
-```
+```sh
/usr/local/bin/git --version
```
@@ -63,39 +63,56 @@ sudo -u git -H git checkout 8-0-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch
-sudo -u git -H git checkout v2.6.4
+sudo -u git -H git checkout v2.6.5
```
### 5. Install gitlab-git-http-server
-First we download Go 1.5 and install it into /usr/local/go.
+First we download Go 1.5 and install it into `/usr/local/go`:
- curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
- echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \
- sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
- sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
- rm go1.5.linux-amd64.tar.gz
+```bash
+curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
+echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \
+ sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.5.linux-amd64.tar.gz
+```
-Now we download gitlab-git-http-server and install it in /home/git/gitlab-git-http-server.
+Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git-http-server`:
- cd /home/git
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
- cd gitlab-git-http-server
- sudo -u git -H make
+```bash
+cd /home/git
+sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
+cd gitlab-git-http-server
+sudo -u git -H make
+```
-If you put your Git repositories in a directory different from /home/git/repositories, you need to tell gitlab-git-http-server about it via /etc/gitlab/default.
-See lib/support/init.d/gitlab.default.example for the options.
+If your Git repositories are in a directory other than `/home/git/repositories`,
+you need to tell `gitlab-git-http-server` about it via `/etc/gitlab/default`.
+See `lib/support/init.d/gitlab.default.example` for the options.
-### 6. Install libs, migrations, etc.
+### 6. Copy secrets
+
+The `secrets.yml` file is used to store keys to encrypt sessions and encrypt secure variables.
+When you run migrations make sure to store it someplace safe.
+Don't store it in the same place as your database backups,
+otherwise your secrets are exposed if one of your backups is compromised.
+
+```
+sudo -u git -H cp config/secrets.yml.example config/secrets.yml
+sudo -u git -H chmod 0600 config/secrets.yml
+```
+
+### 7. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
-# MySQL installations (note: the line below states '--without ... postgres')
-sudo -u git -H bundle install --without development test postgres --deployment
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
-# PostgreSQL installations (note: the line below states '--without ... mysql')
-sudo -u git -H bundle install --without development test mysql --deployment
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
@@ -107,38 +124,52 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
-### 7. Update config files
+### 8. Update config files
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
-```
+```sh
git diff origin/7-14-stable:config/gitlab.yml.example origin/8-0-stable:config/gitlab.yml.example
-``````
-
-#### New NGINX configuration
-
-Because of the new gitlab-git-http-server you need to update your NGINX configuration.
-If you skip this step 'git clone' and 'git push' over HTTP(S) will stop working.
-
```
-# Remove '-ssl' twice in the diff command below if you use HTTP instead of HTTPS
+
+The new options include configuration of GitLab CI that are now being part of GitLab CE and EE.
+
+#### New Nginx configuration
+
+Because of the new `gitlab-git-http-server` you need to update your Nginx
+configuration. If you skip this step 'git clone' and 'git push' over HTTP(S)
+will stop working.
+
+View changes between the previous recommended Nginx configuration and the
+current one:
+
+```sh
+# For HTTPS configurations
git diff origin/7-14-stable:lib/support/nginx/gitlab-ssl origin/8-0-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/7-14-stable:lib/support/nginx/gitlab origin/8-0-stable:lib/support/nginx/gitlab
```
-### 8. Start application
+### 9. Migrate GitLab CI to GitLab CE/EE
+
+Now, GitLab CE and EE has CI integrated. However, migrations don't happen automatically and you need to do it manually.
+Please follow the following guide [to migrate](../migrate_ci_to_ce/README.md) your GitLab CI instance to GitLab CE/EE.
+
+### 10. Start application
sudo service gitlab start
sudo service nginx restart
-### 9. Check application status
+### 11. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
-To make sure you didn't miss anything run a more thorough check with:
+To make sure you didn't miss anything run a more thorough check:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
@@ -147,13 +178,15 @@ If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (7.14)
### 1. Revert the code to the previous version
+
Follow the [upgrade guide from 7.13 to 7.14](7.13-to-7.14.md), except for the database migration
(The backup is already migrated to the previous version)
-### 2. Restore from the backup:
+### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
-If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index d3a77466a3..58574166ef 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -34,6 +34,29 @@ Feature: Project Source Browse Files
Then I am redirected to the new file
And I should see its new content
+ @javascript
+ Scenario: I can upload file and commit
+ Given I click on "new file" link in repo
+ Then I can see new file page
+ And I can see "upload an existing one"
+ And I click on "upload"
+ And I upload a new text file
+ And I fill the upload file commit message
+ And I click on "Upload file"
+ Then I can see the new text file
+ And I can see the new commit message
+
+ @javascript
+ Scenario: I can replace file and commit
+ Given I click on ".gitignore" file in repo
+ And I see the ".gitignore"
+ And I click on "Replace"
+ And I replace it with a text file
+ And I fill the replace file commit message
+ And I click on "Replace file"
+ Then I can see the new text file
+ And I can see the replacement commit message
+
@javascript
Scenario: I can create and commit file and specify new branch
Given I click on "new file" link in repo
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 5cb085db20..a1a49dd58a 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
@@ -78,7 +79,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the commit message' do
- fill_in :commit_message, with: 'Not yet a commit message.'
+ fill_in :commit_message, with: 'Not yet a commit message.', visible: true
end
step 'I click link "Diff"' do
@@ -97,6 +98,14 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_button 'Remove file'
end
+ step 'I click on "Replace"' do
+ click_button "Replace"
+ end
+
+ step 'I click on "Replace file"' do
+ click_button 'Replace file'
+ end
+
step 'I see diff' do
expect(page).to have_css '.line_holder.new'
end
@@ -106,10 +115,55 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I can see new file page' do
- expect(page).to have_content "New file"
+ expect(page).to have_content "new file"
expect(page).to have_content "Commit message"
end
+ step 'I can see "upload an existing one"' do
+ expect(page).to have_content "upload an existing one"
+ end
+
+ step 'I click on "upload"' do
+ click_link 'upload'
+ end
+
+ step 'I click on "Upload file"' do
+ click_button 'Upload file'
+ end
+
+ step 'I can see the new commit message' do
+ expect(page).to have_content "New upload commit message"
+ end
+
+ step 'I upload a new text file' do
+ drop_in_dropzone test_text_file
+ end
+
+ step 'I fill the upload file commit message' do
+ page.within('#modal-upload-blob') do
+ fill_in :commit_message, with: 'New upload commit message'
+ end
+ end
+
+ step 'I replace it with a text file' do
+ drop_in_dropzone test_text_file
+ end
+
+ step 'I fill the replace file commit message' do
+ page.within('#modal-upload-blob') do
+ fill_in :commit_message, with: 'Replacement file commit message'
+ end
+ end
+
+ step 'I can see the replacement commit message' do
+ expect(page).to have_content "Replacement file commit message"
+ end
+
+ step 'I can see the new text file' do
+ expect(page).to have_content "Lorem ipsum dolor sit amet"
+ expect(page).to have_content "Sed ut perspiciatis unde omnis"
+ end
+
step 'I click on files directory' do
click_link 'files'
end
@@ -232,4 +286,29 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
def new_file_name
'not_a_file.md'
end
+
+ def drop_in_dropzone(file_path)
+ # Generate a fake input selector
+ page.execute_script <<-JS
+ var fakeFileInput = window.$('').attr(
+ {id: 'fakeFileInput', type: 'file'}
+ ).appendTo('body');
+ JS
+ # Attach the file to the fake input selector with Capybara
+ attach_file("fakeFileInput", file_path)
+ # Add the file to a fileList array and trigger the fake drop event
+ page.execute_script <<-JS
+ var fileList = [$('#fakeFileInput')[0].files[0]];
+ var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
+ $('.dropzone')[0].dropzone.listeners[0].events.drop(e);
+ JS
+ end
+
+ def test_text_file
+ File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')
+ end
+
+ def test_image_file
+ File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
+ end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 8dddcd7ccc..33b6224a81 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -8,7 +8,7 @@ module API
expose :id, :state, :avatar_url
expose :web_url do |user, options|
- Rails.application.routes.url_helpers.user_url(user)
+ Gitlab::Application.routes.url_helpers.user_url(user)
end
end
@@ -81,7 +81,7 @@ module API
expose :avatar_url
expose :web_url do |group, options|
- Rails.application.routes.url_helpers.group_url(group)
+ Gitlab::Application.routes.url_helpers.group_url(group)
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 76c9cc2e3a..7fada98fcd 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -148,15 +148,14 @@ module API
end
end
- def attributes_for_keys(keys)
+ def attributes_for_keys(keys, custom_params = nil)
+ params_hash = custom_params || params
attrs = {}
-
keys.each do |key|
if params[key].present? or (params.has_key?(key) and params[key] == false)
attrs[key] = params[key]
end
end
-
ActionController::Parameters.new(attrs).permit!
end
@@ -246,6 +245,44 @@ module API
error!({ 'message' => message }, status)
end
+ # Projects helpers
+
+ def filter_projects(projects)
+ # If the archived parameter is passed, limit results accordingly
+ if params[:archived].present?
+ projects = projects.where(archived: parse_boolean(params[:archived]))
+ end
+
+ if params[:search].present?
+ projects = projects.search(params[:search])
+ end
+
+ if params[:ci_enabled_first].present?
+ projects.includes(:gitlab_ci_service).
+ reorder("services.active DESC, projects.#{project_order_by} #{project_sort}")
+ else
+ projects.reorder(project_order_by => project_sort)
+ end
+ end
+
+ def project_order_by
+ order_fields = %w(id name path created_at updated_at last_activity_at)
+
+ if order_fields.include?(params['order_by'])
+ params['order_by']
+ else
+ 'created_at'
+ end
+ end
+
+ def project_sort
+ if params["sort"] == 'asc'
+ :asc
+ else
+ :desc
+ end
+ end
+
private
def add_pagination_headers(paginated, per_page)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 7412274b04..63ea2f0543 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -55,7 +55,7 @@ module API
else merge_requests
end
- merge_requests.reorder(issuable_order_by => issuable_sort)
+ merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort)
present paginate(merge_requests), with: Entities::MergeRequest
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 1f2251c9b9..c2fb36b414 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -11,42 +11,6 @@ module API
attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true
attrs
end
-
- def filter_projects(projects)
- # If the archived parameter is passed, limit results accordingly
- if params[:archived].present?
- projects = projects.where(archived: parse_boolean(params[:archived]))
- end
-
- if params[:search].present?
- projects = projects.search(params[:search])
- end
-
- if params[:ci_enabled_first].present?
- projects.includes(:gitlab_ci_service).
- reorder("services.active DESC, projects.#{project_order_by} #{project_sort}")
- else
- projects.reorder(project_order_by => project_sort)
- end
- end
-
- def project_order_by
- order_fields = %w(id name path created_at updated_at last_activity_at)
-
- if order_fields.include?(params['order_by'])
- params['order_by']
- else
- 'created_at'
- end
- end
-
- def project_sort
- if params["sort"] == 'asc'
- :asc
- else
- :desc
- end
- end
end
# Get a projects list for authenticated user
diff --git a/lib/api/services.rb b/lib/api/services.rb
index d170b3067e..6727e80ac1 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -20,7 +20,7 @@ module API
end
required_attributes! validators.map(&:attributes).flatten.uniq
- attrs = attributes_for_keys service_attributes
+ attrs = attributes_for_keys service_attributes
if project_service.update_attributes(attrs.merge(active: true))
true
@@ -41,7 +41,7 @@ module API
attrs = service_attributes.inject({}) do |hash, key|
hash.merge!(key => nil)
end
-
+
if project_service.update_attributes(attrs.merge(active: false))
true
else
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
new file mode 100644
index 0000000000..6f56f680bb
--- /dev/null
+++ b/lib/backup/builds.rb
@@ -0,0 +1,34 @@
+module Backup
+ class Builds
+ attr_reader :app_builds_dir, :backup_builds_dir, :backup_dir
+
+ def initialize
+ @app_builds_dir = Settings.gitlab_ci.builds_path
+ @backup_dir = Gitlab.config.backup.path
+ @backup_builds_dir = File.join(Gitlab.config.backup.path, 'builds')
+ end
+
+ # Copy builds from builds directory to backup/builds
+ def dump
+ FileUtils.rm_rf(backup_builds_dir)
+ # Ensure the parent dir of backup_builds_dir exists
+ FileUtils.mkdir_p(Gitlab.config.backup.path)
+ # Fail if somebody raced to create backup_builds_dir before us
+ FileUtils.mkdir(backup_builds_dir, mode: 0700)
+ FileUtils.cp_r(app_builds_dir, backup_dir)
+ end
+
+ def restore
+ backup_existing_builds_dir
+
+ FileUtils.cp_r(backup_builds_dir, app_builds_dir)
+ end
+
+ def backup_existing_builds_dir
+ timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
+ if File.exists?(app_builds_dir)
+ FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
+ end
+ end
+ end
+end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 13c68d9354..ac63f89c6e 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -153,7 +153,7 @@ module Backup
end
def folders_to_backup
- folders = %w{repositories db uploads}
+ folders = %w{repositories db uploads builds}
if ENV["SKIP"]
return folders.reject{ |folder| ENV["SKIP"].include?(folder) }
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
new file mode 100644
index 0000000000..ac6d667cf8
--- /dev/null
+++ b/lib/ci/ansi2html.rb
@@ -0,0 +1,224 @@
+# 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
+ }
+
+ STYLE_SWITCHES = {
+ bold: 0x01,
+ italic: 0x02,
+ underline: 0x04,
+ conceal: 0x08,
+ cross: 0x10,
+ }
+
+ def self.convert(ansi)
+ Converter.new().convert(ansi)
+ 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
+
+ def convert(ansi)
+ @out = ""
+ @n_open_tags = 0
+ reset()
+
+ s = StringScanner.new(ansi.gsub("<", "<"))
+ while(!s.eos?)
+ if s.scan(/\e([@-_])(.*?)([@-~])/)
+ handle_sequence(s)
+ else
+ @out << s.scan(/./m)
+ end
+ end
+
+ close_open_tags()
+ @out
+ 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 == '[' and terminator == 'm'
+
+ close_open_tags()
+
+ if commands.empty?()
+ reset()
+ return
+ end
+
+ evaluate_command_stack(commands)
+
+ 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
+
+ open_new_tag(css_classes) if css_classes.length > 0
+ end
+
+ def evaluate_command_stack(stack)
+ return unless command = stack.shift()
+
+ if self.respond_to?("on_#{command}", true)
+ self.send("on_#{command}", stack)
+ end
+
+ evaluate_command_stack(stack)
+ end
+
+ def open_new_tag(css_classes)
+ @out << %{}
+ @n_open_tags += 1
+ end
+
+ def close_open_tags
+ while @n_open_tags > 0
+ @out << %{}
+ @n_open_tags -= 1
+ 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/api/api.rb b/lib/ci/api/api.rb
new file mode 100644
index 0000000000..172c6f2216
--- /dev/null
+++ b/lib/ci/api/api.rb
@@ -0,0 +1,39 @@
+Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file}
+
+module Ci
+ module API
+ class API < Grape::API
+ include APIGuard
+ version 'v1', using: :path
+
+ rescue_from ActiveRecord::RecordNotFound do
+ rack_response({ 'message' => '404 Not found' }.to_json, 404)
+ end
+
+ rescue_from :all do |exception|
+ # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
+ # why is this not wrapped in something reusable?
+ trace = exception.backtrace
+
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << trace.join("\n ")
+
+ API.logger.add Logger::FATAL, message
+ rack_response({ 'message' => '500 Internal Server Error' }, 500)
+ end
+
+ format :json
+
+ helpers Helpers
+ helpers ::API::APIHelpers
+
+ mount Builds
+ mount Commits
+ mount Runners
+ mount Projects
+ mount Forks
+ mount Triggers
+ end
+ end
+end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
new file mode 100644
index 0000000000..83ca1e6481
--- /dev/null
+++ b/lib/ci/api/builds.rb
@@ -0,0 +1,53 @@
+module Ci
+ module API
+ # Builds API
+ class Builds < Grape::API
+ resource :builds do
+ # Runs oldest pending build by runner - Runners only
+ #
+ # Parameters:
+ # token (required) - The uniq token of runner
+ #
+ # Example Request:
+ # POST /builds/register
+ post "register" do
+ authenticate_runner!
+ update_runner_last_contact
+ required_attributes! [:token]
+ not_found! unless current_runner.active?
+
+ build = Ci::RegisterBuildService.new.execute(current_runner)
+
+ if build
+ update_runner_info
+ present build, with: Entities::Build
+ else
+ not_found!
+ end
+ end
+
+ # Update an existing build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # state (optional) - The state of a build
+ # trace (optional) - The trace of a build
+ # Example Request:
+ # PUT /builds/:id
+ put ":id" do
+ authenticate_runner!
+ update_runner_last_contact
+ build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
+ build.update_attributes(trace: params[:trace]) if params[:trace]
+
+ case params[:state].to_s
+ when 'success'
+ build.success
+ when 'failed'
+ build.drop
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb
new file mode 100644
index 0000000000..bac463a590
--- /dev/null
+++ b/lib/ci/api/commits.rb
@@ -0,0 +1,66 @@
+module Ci
+ module API
+ class Commits < Grape::API
+ resource :commits do
+ # Get list of commits per project
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # page (optional)
+ # per_page (optional) - items per request (default is 20)
+ #
+ get do
+ required_attributes! [:project_id, :project_token]
+ project = Ci::Project.find(params[:project_id])
+ authenticate_project_token!(project)
+
+ commits = project.commits.page(params[:page]).per(params[:per_page] || 20)
+ present commits, with: Entities::CommitWithBuilds
+ end
+
+ # Create a commit
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # data (required) - GitLab push data
+ #
+ # Sample GitLab push data:
+ # {
+ # "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ # "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ # "ref": "refs/heads/master",
+ # "commits": [
+ # {
+ # "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ # "message": "Update Catalan translation to e38cb41.",
+ # "timestamp": "2011-12-12T14:27:31+02:00",
+ # "url": "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ # "author": {
+ # "name": "Jordi Mallach",
+ # "email": "jordi@softcatala.org",
+ # }
+ # }, .... more commits
+ # ]
+ # }
+ #
+ # Example Request:
+ # POST /commits
+ post do
+ required_attributes! [:project_id, :data, :project_token]
+ project = Ci::Project.find(params[:project_id])
+ authenticate_project_token!(project)
+ commit = Ci::CreateCommitService.new.execute(project, params[:data])
+
+ if commit.persisted?
+ present commit, with: Entities::CommitWithBuilds
+ else
+ errors = commit.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
new file mode 100644
index 0000000000..f47bc1236b
--- /dev/null
+++ b/lib/ci/api/entities.rb
@@ -0,0 +1,56 @@
+module Ci
+ module API
+ module Entities
+ class Commit < Grape::Entity
+ expose :id, :ref, :sha, :project_id, :before_sha, :created_at
+ expose :status, :finished_at, :duration
+ expose :git_commit_message, :git_author_name, :git_author_email
+ end
+
+ class CommitWithBuilds < Commit
+ expose :builds
+ end
+
+ class Build < Grape::Entity
+ expose :id, :commands, :ref, :sha, :project_id, :repo_url,
+ :before_sha, :allow_git_fetch, :project_name
+
+ expose :options do |model|
+ model.options
+ end
+
+ expose :timeout do |model|
+ model.timeout
+ end
+
+ expose :variables
+ end
+
+ class Runner < Grape::Entity
+ expose :id, :token
+ end
+
+ class Project < Grape::Entity
+ expose :id, :name, :token, :default_ref, :gitlab_url, :path,
+ :always_build, :polling_interval, :public, :ssh_url_to_repo, :gitlab_id
+
+ expose :timeout do |model|
+ model.timeout
+ end
+ end
+
+ class RunnerProject < Grape::Entity
+ expose :id, :project_id, :runner_id
+ end
+
+ class WebHook < Grape::Entity
+ expose :id, :project_id, :url
+ end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ expose :commit, using: Commit
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/forks.rb b/lib/ci/api/forks.rb
new file mode 100644
index 0000000000..152883a599
--- /dev/null
+++ b/lib/ci/api/forks.rb
@@ -0,0 +1,37 @@
+module Ci
+ module API
+ class Forks < Grape::API
+ resource :forks do
+ # Create a fork
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # private_token(required) - User private token
+ # data (required) - GitLab project data (name_with_namespace, web_url, default_branch, ssh_url_to_repo)
+ #
+ #
+ # Example Request:
+ # POST /forks
+ post do
+ required_attributes! [:project_id, :data, :project_token, :private_token]
+ project = Ci::Project.find_by!(gitlab_id: params[:project_id])
+ authenticate_project_token!(project)
+
+ fork = Ci::CreateProjectService.new.execute(
+ current_user,
+ params[:data],
+ Ci::RoutesHelper.ci_project_url(":project_id"),
+ project
+ )
+
+ if fork
+ present fork, with: Entities::Project
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
new file mode 100644
index 0000000000..e602cda81d
--- /dev/null
+++ b/lib/ci/api/helpers.rb
@@ -0,0 +1,35 @@
+module Ci
+ module API
+ module Helpers
+ UPDATE_RUNNER_EVERY = 60
+
+ def authenticate_runners!
+ forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN
+ end
+
+ def authenticate_runner!
+ forbidden! unless current_runner
+ end
+
+ def authenticate_project_token!(project)
+ forbidden! unless project.valid_token?(params[:project_token])
+ end
+
+ def update_runner_last_contact
+ if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= UPDATE_RUNNER_EVERY
+ current_runner.update_attributes(contacted_at: Time.now)
+ end
+ end
+
+ def current_runner
+ @runner ||= Runner.find_by_token(params[:token].to_s)
+ end
+
+ def update_runner_info
+ return unless params["info"].present?
+ info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+ current_runner.update(info)
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb
new file mode 100644
index 0000000000..66bcf65e8c
--- /dev/null
+++ b/lib/ci/api/projects.rb
@@ -0,0 +1,210 @@
+module Ci
+ module API
+ # Projects API
+ class Projects < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Register new webhook for project
+ #
+ # Parameters
+ # project_id (required) - The ID of a project
+ # web_hook (required) - WebHook URL
+ # Example Request
+ # POST /projects/:project_id/webhooks
+ post ":project_id/webhooks" do
+ required_attributes! [:web_hook]
+
+ project = Ci::Project.find(params[:project_id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ web_hook = project.web_hooks.new({ url: params[:web_hook] })
+
+ if web_hook.save
+ present web_hook, with: Entities::WebHook
+ else
+ errors = web_hook.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Retrieve all Gitlab CI projects that the user has access to
+ #
+ # Example Request:
+ # GET /projects
+ get do
+ gitlab_projects = current_user.authorized_projects
+ gitlab_projects = filter_projects(gitlab_projects)
+ gitlab_projects = paginate gitlab_projects
+
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Ci::Project.where("gitlab_id IN (?)", ids).load
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve all Gitlab CI projects that the user owns
+ #
+ # Example Request:
+ # GET /projects/owned
+ get "owned" do
+ gitlab_projects = current_user.owned_projects
+ gitlab_projects = filter_projects(gitlab_projects)
+ gitlab_projects = paginate gitlab_projects
+
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Ci::Project.where("gitlab_id IN (?)", ids).load
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve info for a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # GET /projects/:id
+ get ":id" do
+ project = Ci::Project.find(params[:id])
+ unauthorized! unless can?(current_user, :read_project, project.gl_project)
+
+ present project, with: Entities::Project
+ end
+
+ # Create Gitlab CI project using Gitlab project info
+ #
+ # Parameters:
+ # name (required) - The name of the project
+ # gitlab_id (required) - The gitlab id of the project
+ # path (required) - The gitlab project path, ex. randx/six
+ # ssh_url_to_repo (required) - The gitlab ssh url to the repo
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # POST /projects
+ post do
+ required_attributes! [:name, :gitlab_id, :ssh_url_to_repo]
+
+ filtered_params = {
+ name: params[:name],
+ gitlab_id: params[:gitlab_id],
+ # we accept gitlab_url for backward compatibility for a while (added to 7.11)
+ path: params[:path] || params[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1'),
+ default_ref: params[:default_ref] || 'master',
+ ssh_url_to_repo: params[:ssh_url_to_repo]
+ }
+
+ project = Ci::Project.new(filtered_params)
+ project.build_missing_services
+
+ if project.save
+ present project, with: Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Update a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # name - The name of the project
+ # gitlab_id - The gitlab id of the project
+ # path - The gitlab project path, ex. randx/six
+ # ssh_url_to_repo - The gitlab ssh url to the repo
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # PUT /projects/:id
+ put ":id" do
+ project = Ci::Project.find(params[:id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ attrs = attributes_for_keys [:name, :gitlab_id, :path, :gitlab_url, :default_ref, :ssh_url_to_repo]
+
+ # we accept gitlab_url for backward compatibility for a while (added to 7.11)
+ if attrs[:gitlab_url] && !attrs[:path]
+ attrs[:path] = attrs[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1')
+ end
+
+ if project.update_attributes(attrs)
+ present project, with: Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Remove a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # DELETE /projects/:id
+ delete ":id" do
+ project = Ci::Project.find(params[:id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ project.destroy
+ end
+
+ # Link a Gitlab CI project to a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # POST /projects/:id/runners/:runner_id
+ post ":id/runners/:runner_id" do
+ project = Ci::Project.find(params[:id])
+ runner = Ci::Runner.find(params[:runner_id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ options = {
+ project_id: project.id,
+ runner_id: runner.id
+ }
+
+ runner_project = Ci::RunnerProject.new(options)
+
+ if runner_project.save
+ present runner_project, with: Entities::RunnerProject
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Remove a Gitlab CI project from a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # DELETE /projects/:id/runners/:runner_id
+ delete ":id/runners/:runner_id" do
+ project = Ci::Project.find(params[:id])
+ runner = Ci::Runner.find(params[:runner_id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ options = {
+ project_id: project.id,
+ runner_id: runner.id
+ }
+
+ runner_project = Ci::RunnerProject.find_by(options)
+
+ if runner_project.present?
+ runner_project.destroy
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
new file mode 100644
index 0000000000..1466fe4356
--- /dev/null
+++ b/lib/ci/api/runners.rb
@@ -0,0 +1,69 @@
+module Ci
+ module API
+ # Runners API
+ class Runners < Grape::API
+ resource :runners do
+ # Get list of all available runners
+ #
+ # Example Request:
+ # GET /runners
+ get do
+ authenticate!
+ runners = Ci::Runner.all
+
+ present runners, with: Entities::Runner
+ end
+
+ # Delete runner
+ # Parameters:
+ # token (required) - The unique token of runner
+ #
+ # Example Request:
+ # GET /runners/delete
+ delete "delete" do
+ required_attributes! [:token]
+ authenticate_runner!
+ Ci::Runner.find_by_token(params[:token]).destroy
+ end
+
+ # Register a new runner
+ #
+ # Note: This is an "internal" API called when setting up
+ # runners, so it is authenticated differently.
+ #
+ # Parameters:
+ # token (required) - The unique token of runner
+ #
+ # Example Request:
+ # POST /runners/register
+ post "register" do
+ required_attributes! [:token]
+
+ runner =
+ if params[:token] == GitlabCi::REGISTRATION_TOKEN
+ # Create shared runner. Requires admin access
+ Ci::Runner.create(
+ description: params[:description],
+ tag_list: params[:tag_list],
+ is_shared: true
+ )
+ elsif project = Ci::Project.find_by(token: params[:token])
+ # Create a specific runner for project.
+ project.runners.create(
+ description: params[:description],
+ tag_list: params[:tag_list]
+ )
+ end
+
+ return forbidden! unless runner
+
+ if runner.id
+ present runner, with: Entities::Runner
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
new file mode 100644
index 0000000000..40907d6db5
--- /dev/null
+++ b/lib/ci/api/triggers.rb
@@ -0,0 +1,49 @@
+module Ci
+ module API
+ # Build Trigger API
+ class Triggers < Grape::API
+ resource :projects do
+ # Trigger a GitLab CI project build
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # ref (required) - The name of project's branch or tag
+ # token (required) - The uniq token of trigger
+ # Example Request:
+ # POST /projects/:id/ref/:ref/trigger
+ post ":id/refs/:ref/trigger" do
+ required_attributes! [:token]
+
+ project = Ci::Project.find(params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ not_found! unless project && trigger
+ unauthorized! unless trigger.project == project
+
+ # validate variables
+ variables = params[:variables]
+ if variables
+ unless variables.is_a?(Hash)
+ render_api_error!('variables needs to be a hash', 400)
+ end
+
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ # convert variables from Mash to Hash
+ variables = variables.to_h
+ end
+
+ # create request and trigger builds
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ if trigger_request
+ present trigger_request, with: Entities::TriggerRequest
+ else
+ errors = 'No builds created'
+ render_api_error!(errors, 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/assets/.gitkeep b/lib/ci/assets/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
new file mode 100644
index 0000000000..915a4f526a
--- /dev/null
+++ b/lib/ci/charts.rb
@@ -0,0 +1,71 @@
+module Ci
+ module Charts
+ class Chart
+ attr_reader :labels, :total, :success, :project, :build_times
+
+ def initialize(project)
+ @labels = []
+ @total = []
+ @success = []
+ @build_times = []
+ @project = project
+
+ collect
+ end
+
+
+ def push(from, to, format)
+ @labels << from.strftime(format)
+ @total << project.builds.
+ where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from).
+ count(:all)
+ @success << project.builds.
+ where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from).
+ success.count(:all)
+ end
+ end
+
+ class YearChart < Chart
+ def collect
+ 13.times do |i|
+ start_month = (Date.today.years_ago(1) + i.month).beginning_of_month
+ end_month = start_month.end_of_month
+
+ push(start_month, end_month, "%d %B %Y")
+ end
+ end
+ end
+
+ class MonthChart < Chart
+ def collect
+ 30.times do |i|
+ start_day = Date.today - 30.days + i.days
+ end_day = Date.today - 30.days + i.day + 1.day
+
+ push(start_day, end_day, "%d %B")
+ end
+ end
+ end
+
+ class WeekChart < Chart
+ def collect
+ 7.times do |i|
+ start_day = Date.today - 7.days + i.days
+ end_day = Date.today - 7.days + i.day + 1.day
+
+ push(start_day, end_day, "%d %B")
+ end
+ end
+ end
+
+ class BuildTime < Chart
+ def collect
+ commits = project.commits.joins(:builds).where("#{Ci::Build.table_name}.finished_at is NOT NULL AND #{Ci::Build.table_name}.started_at is NOT NULL").last(30)
+ commits.each do |commit|
+ @labels << commit.short_sha
+ @build_times << (commit.duration / 60)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/current_settings.rb b/lib/ci/current_settings.rb
new file mode 100644
index 0000000000..fd78b02497
--- /dev/null
+++ b/lib/ci/current_settings.rb
@@ -0,0 +1,22 @@
+module Ci
+ module CurrentSettings
+ def current_application_settings
+ key = :ci_current_application_settings
+
+ RequestStore.store[key] ||= begin
+ if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('ci_application_settings')
+ Ci::ApplicationSetting.current || Ci::ApplicationSetting.create_from_defaults
+ else
+ fake_application_settings
+ end
+ end
+ end
+
+ def fake_application_settings
+ OpenStruct.new(
+ all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'],
+ add_pusher: Ci::Settings.gitlab_ci['add_pusher'],
+ )
+ end
+ end
+end
diff --git a/lib/ci/git.rb b/lib/ci/git.rb
new file mode 100644
index 0000000000..7acc3f38ed
--- /dev/null
+++ b/lib/ci/git.rb
@@ -0,0 +1,5 @@
+module Ci
+ module Git
+ BLANK_SHA = '0' * 40
+ end
+end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
new file mode 100644
index 0000000000..e625e790df
--- /dev/null
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -0,0 +1,198 @@
+module Ci
+ class GitlabCiYamlProcessor
+ class ValidationError < StandardError;end
+
+ DEFAULT_STAGES = %w(build test deploy)
+ DEFAULT_STAGE = 'test'
+ ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage]
+
+ attr_reader :before_script, :image, :services, :variables
+
+ def initialize(config)
+ @config = YAML.load(config)
+
+ unless @config.is_a? Hash
+ raise ValidationError, "YAML should be a hash"
+ end
+
+ @config = @config.deep_symbolize_keys
+
+ initial_parsing
+
+ validate!
+ end
+
+ def builds_for_stage_and_ref(stage, ref, tag = false)
+ builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag)}
+ end
+
+ def builds
+ @jobs.map do |name, job|
+ build_job(name, job)
+ end
+ end
+
+ def stages
+ @stages || DEFAULT_STAGES
+ end
+
+ private
+
+ def initial_parsing
+ @before_script = @config[:before_script] || []
+ @image = @config[:image]
+ @services = @config[:services]
+ @stages = @config[:stages] || @config[:types]
+ @variables = @config[:variables] || {}
+ @config.except!(*ALLOWED_YAML_KEYS)
+
+ # anything that doesn't have script is considered as unknown
+ @config.each do |name, param|
+ raise ValidationError, "Unknown parameter: #{name}" unless param.is_a?(Hash) && param.has_key?(:script)
+ end
+
+ unless @config.values.any?{|job| job.is_a?(Hash)}
+ raise ValidationError, "Please define at least one job"
+ end
+
+ @jobs = {}
+ @config.each do |key, job|
+ stage = job[:stage] || job[:type] || DEFAULT_STAGE
+ @jobs[key] = { stage: stage }.merge(job)
+ end
+ end
+
+ def process?(only_params, except_params, ref, tag)
+ return true if only_params.nil? && except_params.nil?
+
+ if only_params
+ return true if tag && only_params.include?("tags")
+ return true if !tag && only_params.include?("branches")
+
+ only_params.find do |pattern|
+ match_ref?(pattern, ref)
+ end
+ else
+ return false if tag && except_params.include?("tags")
+ return false if !tag && except_params.include?("branches")
+
+ except_params.each do |pattern|
+ return false if match_ref?(pattern, ref)
+ end
+ end
+ end
+
+ def build_job(name, job)
+ {
+ stage: job[:stage],
+ script: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
+ tags: job[:tags] || [],
+ name: name,
+ only: job[:only],
+ except: job[:except],
+ allow_failure: job[:allow_failure] || false,
+ options: {
+ image: job[:image] || @image,
+ services: job[:services] || @services
+ }.compact
+ }
+ end
+
+ def match_ref?(pattern, ref)
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ ref
+ else
+ pattern == ref
+ end
+ end
+
+ def normalize_script(script)
+ if script.is_a? Array
+ script.join("\n")
+ else
+ script
+ end
+ end
+
+ def validate!
+ unless validate_array_of_strings(@before_script)
+ raise ValidationError, "before_script should be an array of strings"
+ end
+
+ unless @image.nil? || @image.is_a?(String)
+ raise ValidationError, "image should be a string"
+ end
+
+ unless @services.nil? || validate_array_of_strings(@services)
+ raise ValidationError, "services should be an array of strings"
+ end
+
+ unless @stages.nil? || validate_array_of_strings(@stages)
+ raise ValidationError, "stages should be an array of strings"
+ end
+
+ unless @variables.nil? || validate_variables(@variables)
+ raise ValidationError, "variables should be a map of key-valued strings"
+ end
+
+ @jobs.each do |name, job|
+ validate_job!("#{name} job", job)
+ end
+
+ true
+ end
+
+ def validate_job!(name, job)
+ job.keys.each do |key|
+ unless ALLOWED_JOB_KEYS.include? key
+ raise ValidationError, "#{name}: unknown parameter #{key}"
+ end
+ end
+
+ if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name}: script should be a string or an array of a strings"
+ end
+
+ if job[:stage]
+ unless job[:stage].is_a?(String) && job[:stage].in?(stages)
+ raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}"
+ end
+ end
+
+ if job[:image] && !job[:image].is_a?(String)
+ raise ValidationError, "#{name}: image should be a string"
+ end
+
+ if job[:services] && !validate_array_of_strings(job[:services])
+ raise ValidationError, "#{name}: services should be an array of strings"
+ end
+
+ if job[:tags] && !validate_array_of_strings(job[:tags])
+ raise ValidationError, "#{name}: tags parameter should be an array of strings"
+ end
+
+ if job[:only] && !validate_array_of_strings(job[:only])
+ raise ValidationError, "#{name}: only parameter should be an array of strings"
+ end
+
+ if job[:except] && !validate_array_of_strings(job[:except])
+ raise ValidationError, "#{name}: except parameter should be an array of strings"
+ end
+
+ if job[:allow_failure] && !job[:allow_failure].in?([true, false])
+ raise ValidationError, "#{name}: allow_failure parameter should be an boolean"
+ end
+ end
+
+ private
+
+ def validate_array_of_strings(values)
+ values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)}
+ end
+
+ def validate_variables(variables)
+ variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)}
+ end
+ end
+end
diff --git a/lib/ci/migrate/database.rb b/lib/ci/migrate/database.rb
new file mode 100644
index 0000000000..74f592dcae
--- /dev/null
+++ b/lib/ci/migrate/database.rb
@@ -0,0 +1,67 @@
+require 'yaml'
+
+module Ci
+ module Migrate
+ class Database
+ attr_reader :config
+
+ def initialize
+ @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
+ end
+
+ def restore(ci_dump)
+ puts 'Deleting all CI related data ... '
+ truncate_ci_tables
+
+ puts 'Restoring CI data ... '
+ case config["adapter"]
+ when /^mysql/ then
+ print "Restoring MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ system('mysql', *mysql_args, config['database'], in: ci_dump)
+ when "postgresql" then
+ puts "Restoring PostgreSQL database #{config['database']} ... "
+ pg_env
+ system('psql', config['database'], '-f', ci_dump)
+ end
+ end
+
+ protected
+
+ def truncate_ci_tables
+ c = ActiveRecord::Base.connection
+ c.tables.select { |t| t.start_with?('ci_') }.each do |table|
+ puts "Deleting data from #{table}..."
+ c.execute("DELETE FROM #{table}")
+ end
+ end
+
+ def mysql_args
+ args = {
+ 'host' => '--host',
+ 'port' => '--port',
+ 'socket' => '--socket',
+ 'username' => '--user',
+ 'encoding' => '--default-character-set'
+ }
+ args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
+ end
+
+ def pg_env
+ ENV['PGUSER'] = config["username"] if config["username"]
+ ENV['PGHOST'] = config["host"] if config["host"]
+ ENV['PGPORT'] = config["port"].to_s if config["port"]
+ ENV['PGPASSWORD'] = config["password"].to_s if config["password"]
+ end
+
+ def report_success(success)
+ if success
+ puts '[DONE]'.green
+ else
+ puts '[FAILED]'.red
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/migrate/tags.rb b/lib/ci/migrate/tags.rb
new file mode 100644
index 0000000000..125a535e9a
--- /dev/null
+++ b/lib/ci/migrate/tags.rb
@@ -0,0 +1,49 @@
+require 'yaml'
+
+module Ci
+ module Migrate
+ class Tags
+ def restore
+ puts 'Migrating tags for Runners... '
+ list_objects('Runner').each do |id|
+ putc '.'
+ runner = Ci::Runner.find_by_id(id)
+ if runner
+ tags = list_tags('Runner', id)
+ runner.update_attributes(tag_list: tags)
+ end
+ end
+ puts ''
+
+ puts 'Migrating tags for Builds... '
+ list_objects('Build').each do |id|
+ putc '.'
+ build = Ci::Build.find_by_id(id)
+ if build
+ tags = list_tags('Build', id)
+ build.update_attributes(tag_list: tags)
+ end
+ end
+ puts ''
+ end
+
+ protected
+
+ def list_objects(type)
+ ids = ActiveRecord::Base.connection.select_all(
+ "select distinct taggable_id from ci_taggings where taggable_type = #{ActiveRecord::Base::sanitize(type)}"
+ )
+ ids.map { |id| id['taggable_id'] }
+ end
+
+ def list_tags(type, id)
+ tags = ActiveRecord::Base.connection.select_all(
+ 'select ci_tags.name from ci_tags ' +
+ 'join ci_taggings on ci_tags.id = ci_taggings.tag_id ' +
+ "where taggable_type = #{ActiveRecord::Base::sanitize(type)} and taggable_id = #{ActiveRecord::Base::sanitize(id)} and context = 'tags'"
+ )
+ tags.map { |tag| tag['name'] }
+ end
+ end
+ end
+end
diff --git a/lib/ci/model.rb b/lib/ci/model.rb
new file mode 100644
index 0000000000..c42a0ad36d
--- /dev/null
+++ b/lib/ci/model.rb
@@ -0,0 +1,11 @@
+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/ci/scheduler.rb b/lib/ci/scheduler.rb
new file mode 100644
index 0000000000..ee0958f4be
--- /dev/null
+++ b/lib/ci/scheduler.rb
@@ -0,0 +1,16 @@
+module Ci
+ class Scheduler
+ def perform
+ projects = Ci::Project.where(always_build: true).all
+ projects.each do |project|
+ last_commit = project.commits.last
+ next unless last_commit && last_commit.last_build
+
+ interval = project.polling_interval
+ if (last_commit.last_build.created_at + interval.hours) < Time.now
+ last_commit.retry
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb
new file mode 100644
index 0000000000..bb2bdbed49
--- /dev/null
+++ b/lib/ci/static_model.rb
@@ -0,0 +1,49 @@
+# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database.
+module Ci
+ module StaticModel
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Used by ActiveRecord's polymorphic association to set object_id
+ def primary_key
+ 'id'
+ end
+
+ # Used by ActiveRecord's polymorphic association to set object_type
+ def base_class
+ self
+ end
+ end
+
+ # Used by AR for fetching attributes
+ #
+ # Pass it along if we respond to it.
+ def [](key)
+ send(key) if respond_to?(key)
+ end
+
+ def to_param
+ id
+ end
+
+ def new_record?
+ false
+ end
+
+ def persisted?
+ false
+ end
+
+ def destroyed?
+ false
+ end
+
+ def ==(other)
+ if other.is_a? ::Ci::StaticModel
+ id == other.id
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb
new file mode 100644
index 0000000000..2a87c91db5
--- /dev/null
+++ b/lib/ci/version_info.rb
@@ -0,0 +1,52 @@
+class VersionInfo
+ include Comparable
+
+ attr_reader :major, :minor, :patch
+
+ def self.parse(str)
+ if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/)
+ VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i)
+ else
+ VersionInfo.new
+ end
+ end
+
+ def initialize(major = 0, minor = 0, patch = 0)
+ @major = major
+ @minor = minor
+ @patch = patch
+ end
+
+ def <=>(other)
+ return unless other.is_a? VersionInfo
+ return unless valid? && other.valid?
+
+ if other.major < @major
+ 1
+ elsif @major < other.major
+ -1
+ elsif other.minor < @minor
+ 1
+ elsif @minor < other.minor
+ -1
+ elsif other.patch < @patch
+ 1
+ elsif @patch < other.patch
+ -1
+ else
+ 0
+ end
+ end
+
+ def to_s
+ if valid?
+ "%d.%d.%d" % [@major, @minor, @patch]
+ else
+ "Unknown"
+ end
+ end
+
+ def valid?
+ @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
+ end
+end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
new file mode 100644
index 0000000000..55deeeacd9
--- /dev/null
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -0,0 +1,35 @@
+# Class to parse and transform the info provided by omniauth
+#
+module Gitlab
+ module LDAP
+ class AuthHash < Gitlab::OAuth::AuthHash
+ private
+
+ def get_info(key)
+ attributes = ldap_config.attributes[key]
+ return super unless attributes
+
+ attributes = Array(attributes)
+
+ value = nil
+ attributes.each do |attribute|
+ value = get_raw(attribute)
+ break if value.present?
+ end
+
+ return super unless value
+
+ Gitlab::Utils.force_utf8(value)
+ value
+ end
+
+ def get_raw(key)
+ auth_hash.extra[:raw_info][key]
+ end
+
+ def ldap_config
+ @ldap_config ||= Gitlab::LDAP::Config.new(self.provider)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index d2ffa2e1fe..101a3285f4 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -84,6 +84,10 @@ module Gitlab
options['block_auto_created_users']
end
+ def attributes
+ options['attributes']
+ end
+
protected
def base_config
Gitlab.config.ldap
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 04a2223747..cb66fd500f 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -71,6 +71,10 @@ module Gitlab
def ldap_config
Gitlab::LDAP::Config.new(auth_hash.provider)
end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::LDAP::AuthHash.new(auth_hash)
+ end
end
end
end
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
index 8613150894..bb496135d9 100644
--- a/lib/gitlab/markdown/commit_range_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -73,7 +73,7 @@ module Gitlab
end
def url_for_commit_range(project, range)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_compare_url(project.namespace, project,
range.to_param.merge(only_path: context[:only_path]))
end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
index 5696b4fa58..fcbb2e936a 100644
--- a/lib/gitlab/markdown/commit_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -69,7 +69,7 @@ module Gitlab
end
def url_for_commit(project, commit)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
only_path: context[:only_path])
end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb
index 3d7445a27f..1e5cb12071 100644
--- a/lib/gitlab/markdown/label_reference_filter.rb
+++ b/lib/gitlab/markdown/label_reference_filter.rb
@@ -56,7 +56,7 @@ module Gitlab
end
def url_for_label(project, label)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_issues_path(project.namespace, project,
label_name: label.name,
only_path: context[:only_path])
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
index 48248f5219..ecbd263d0e 100644
--- a/lib/gitlab/markdown/merge_request_reference_filter.rb
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -63,7 +63,7 @@ module Gitlab
end
def url_for_merge_request(mr, project)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_merge_request_url(project.namespace, project, mr,
only_path: context[:only_path])
end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
index 9e1aab936c..e2cf89cb1d 100644
--- a/lib/gitlab/markdown/snippet_reference_filter.rb
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -63,7 +63,7 @@ module Gitlab
end
def url_for_snippet(snippet, project)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_snippet_url(project.namespace, project, snippet,
only_path: context[:only_path])
end
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
index 1871e52df0..6f436ea716 100644
--- a/lib/gitlab/markdown/user_reference_filter.rb
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -51,7 +51,7 @@ module Gitlab
private
def urls
- Rails.application.routes.url_helpers
+ Gitlab::Application.routes.url_helpers
end
def link_class
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index 9b8e783d16..d94b104bbf 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -16,16 +16,6 @@ module Gitlab
@provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s)
end
- def info
- auth_hash.info
- end
-
- def get_info(key)
- value = info.try(key)
- Gitlab::Utils.force_utf8(value) if value
- value
- end
-
def name
@name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}"
end
@@ -44,9 +34,19 @@ module Gitlab
private
+ def info
+ auth_hash.info
+ end
+
+ def get_info(key)
+ value = info[key]
+ Gitlab::Utils.force_utf8(value) if value
+ value
+ end
+
def username_and_email
@username_and_email ||= begin
- username = get_info(:nickname) || get_info(:username)
+ username = get_info(:username) || get_info(:nickname)
email = get_info(:email)
username ||= generate_username(email) if email
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 779819bc2b..6f0d02cafd 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -1,6 +1,6 @@
module Gitlab
class UrlBuilder
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
include GitlabRoutingHelper
def initialize(type)
diff --git a/lib/support/nginx/gitlab_ci b/lib/support/nginx/gitlab_ci
new file mode 100644
index 0000000000..ce179d6f59
--- /dev/null
+++ b/lib/support/nginx/gitlab_ci
@@ -0,0 +1,41 @@
+# GITLAB CI
+server {
+ listen 80 default_server; # e.g., listen 192.168.1.1:80;
+ server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com;
+
+ access_log /var/log/nginx/gitlab_ci_access.log;
+ error_log /var/log/nginx/gitlab_ci_error.log;
+
+ # expose API to fix runners
+ location /api {
+ proxy_read_timeout 300;
+ proxy_connect_timeout 300;
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN
+ resolver 8.8.8.8 8.8.4.4;
+ proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # expose build endpoint to allow trigger builds
+ location ~ ^/projects/\d+/build$ {
+ proxy_read_timeout 300;
+ proxy_connect_timeout 300;
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN
+ resolver 8.8.8.8 8.8.4.4;
+ proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # redirect all other CI requests
+ location / {
+ return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # adjust this to match the largest build log your runners might submit,
+ # set to 0 to disable limit
+ client_max_body_size 10m;
+}
\ No newline at end of file
diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lib/tasks/ci/cleanup.rake b/lib/tasks/ci/cleanup.rake
new file mode 100644
index 0000000000..2f4d11bd94
--- /dev/null
+++ b/lib/tasks/ci/cleanup.rake
@@ -0,0 +1,8 @@
+namespace :ci do
+ namespace :cleanup do
+ desc "GitLab CI | Clean running builds"
+ task builds: :environment do
+ Ci::Build.running.update_all(status: 'canceled')
+ end
+ end
+end
diff --git a/lib/tasks/ci/migrate.rake b/lib/tasks/ci/migrate.rake
new file mode 100644
index 0000000000..e7d41874a1
--- /dev/null
+++ b/lib/tasks/ci/migrate.rake
@@ -0,0 +1,63 @@
+namespace :ci do
+ desc 'GitLab | Import and migrate CI database'
+ task migrate: :environment do
+ unless ENV['force'] == 'yes'
+ puts "This will truncate all CI tables and restore it from provided backup."
+ puts "You will lose any previous CI data stored in the database."
+ ask_to_continue
+ puts ""
+ end
+
+ Rake::Task["ci:migrate:db"].invoke
+ Rake::Task["ci:migrate:autoincrements"].invoke
+ Rake::Task["ci:migrate:tags"].invoke
+ Rake::Task["ci:migrate:services"].invoke
+ end
+
+ namespace :migrate do
+ desc 'GitLab | Import CI database'
+ task db: :environment do
+ if ENV["CI_DUMP"].nil?
+ puts "No CI SQL dump specified:"
+ puts "rake gitlab:backup:restore CI_DUMP=ci_dump.sql"
+ exit 1
+ end
+
+ ci_dump = ENV["CI_DUMP"]
+ unless File.exists?(ci_dump)
+ puts "The specified sql dump doesn't exist!"
+ exit 1
+ end
+
+ ::Ci::Migrate::Database.new.restore(ci_dump)
+ end
+
+ desc 'GitLab | Migrate CI tags'
+ task tags: :environment do
+ ::Ci::Migrate::Tags.new.restore
+ end
+
+ desc 'GitLab | Migrate CI auto-increments'
+ task autoincrements: :environment do
+ c = ActiveRecord::Base.connection
+ c.tables.select { |t| t.start_with?('ci_') }.each do |table|
+ result = c.select_one("SELECT id FROM #{table} ORDER BY id DESC LIMIT 1")
+ if result
+ ai_val = result['id'].to_i + 1
+ puts "Resetting auto increment ID for #{table} to #{ai_val}"
+ if c.adapter_name == 'PostgreSQL'
+ c.execute("ALTER SEQUENCE #{table}_id_seq RESTART WITH #{ai_val}")
+ else
+ c.execute("ALTER TABLE #{table} AUTO_INCREMENT = #{ai_val}")
+ end
+ end
+ end
+ end
+
+ desc 'GitLab | Migrate CI services'
+ task services: :environment do
+ c = ActiveRecord::Base.connection
+ c.execute("UPDATE ci_services SET type=CONCAT('Ci::', type) WHERE type NOT LIKE 'Ci::%'")
+ end
+ end
+end
diff --git a/lib/tasks/ci/schedule_builds.rake b/lib/tasks/ci/schedule_builds.rake
new file mode 100644
index 0000000000..49435504c6
--- /dev/null
+++ b/lib/tasks/ci/schedule_builds.rake
@@ -0,0 +1,6 @@
+namespace :ci do
+ desc "GitLab CI | Clean running builds"
+ task schedule_builds: :environment do
+ Ci::Scheduler.new.perform
+ end
+end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 4c73f90bbf..f20c7f71ba 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -11,6 +11,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:db:create"].invoke
Rake::Task["gitlab:backup:repo:create"].invoke
Rake::Task["gitlab:backup:uploads:create"].invoke
+ Rake::Task["gitlab:backup:builds:create"].invoke
backup = Backup::Manager.new
backup.pack
@@ -30,6 +31,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db")
Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
+ Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
Rake::Task["gitlab:shell:setup"].invoke
backup.cleanup
@@ -73,6 +75,25 @@ namespace :gitlab do
end
end
+ namespace :builds do
+ task create: :environment do
+ $progress.puts "Dumping builds ... ".blue
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("builds")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Builds.new.dump
+ $progress.puts "done".green
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring builds ... ".blue
+ Backup::Builds.new.restore
+ $progress.puts "done".green
+ end
+ end
+
namespace :uploads do
task create: :environment do
$progress.puts "Dumping uploads ... ".blue
diff --git a/public/ci/build-canceled.svg b/public/ci/build-canceled.svg
new file mode 100644
index 0000000000..922e28bf69
--- /dev/null
+++ b/public/ci/build-canceled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/ci/build-failed.svg b/public/ci/build-failed.svg
new file mode 100644
index 0000000000..1aefd3f176
--- /dev/null
+++ b/public/ci/build-failed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/ci/build-pending.svg b/public/ci/build-pending.svg
new file mode 100644
index 0000000000..536931af84
--- /dev/null
+++ b/public/ci/build-pending.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/ci/build-running.svg b/public/ci/build-running.svg
new file mode 100644
index 0000000000..0d71eef3c3
--- /dev/null
+++ b/public/ci/build-running.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/ci/build-skipped.svg b/public/ci/build-skipped.svg
new file mode 100644
index 0000000000..f15507188e
--- /dev/null
+++ b/public/ci/build-skipped.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/ci/build-success.svg b/public/ci/build-success.svg
new file mode 100644
index 0000000000..43b67e45f4
--- /dev/null
+++ b/public/ci/build-success.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/ci/build-unknown.svg b/public/ci/build-unknown.svg
new file mode 100644
index 0000000000..c72a2f5a7f
--- /dev/null
+++ b/public/ci/build-unknown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/ci/favicon.ico b/public/ci/favicon.ico
new file mode 100644
index 0000000000..9663d4d00b
Binary files /dev/null and b/public/ci/favicon.ico differ
diff --git a/scripts/ci/prepare_build.sh b/scripts/ci/prepare_build.sh
new file mode 100755
index 0000000000..864a683a1b
--- /dev/null
+++ b/scripts/ci/prepare_build.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+if [ -f /.dockerinit ]; then
+ export FLAGS=(--deployment --path /cache)
+
+ apt-get update -qq
+ apt-get install -y -qq nodejs
+
+ wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb
+ dpkg -i phantomjs_1.9.0-1+b1_amd64.deb
+
+ cp config/database.yml.mysql config/database.yml
+ sed -i "s/username:.*/username: root/g" config/database.yml
+ sed -i "s/password:.*/password:/g" config/database.yml
+ sed -i "s/# socket:.*/host: mysql/g" config/database.yml
+else
+ export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
+
+ cp config/database.yml.mysql config/database.yml
+ sed -i "s/username\:.*$/username\: runner/" config/database.yml
+ sed -i "s/password\:.*$/password\: 'password'/" config/database.yml
+ sed -i "s/gitlab_ci_test/gitlab_ci_test_$((RANDOM/5000))/" config/database.yml
+fi
diff --git a/spec/controllers/ci/commits_controller_spec.rb b/spec/controllers/ci/commits_controller_spec.rb
new file mode 100644
index 0000000000..b71e750573
--- /dev/null
+++ b/spec/controllers/ci/commits_controller_spec.rb
@@ -0,0 +1,27 @@
+require "spec_helper"
+
+describe Ci::CommitsController do
+ before do
+ @project = FactoryGirl.create :ci_project
+ end
+
+ describe "GET /status" do
+ it "returns status of commit" do
+ commit = FactoryGirl.create :ci_commit, project: @project
+ get :status, id: commit.sha, ref_id: commit.ref, project_id: @project.id
+
+ expect(response).to be_success
+ expect(response.code).to eq('200')
+ JSON.parse(response.body)["status"] == "pending"
+ end
+
+ it "returns not_found status" do
+ commit = FactoryGirl.create :ci_commit, project: @project
+ get :status, id: commit.sha, ref_id: "deploy", project_id: @project.id
+
+ expect(response).to be_success
+ expect(response.code).to eq('200')
+ JSON.parse(response.body)["status"] == "not_found"
+ end
+ end
+end
diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb
new file mode 100644
index 0000000000..015788a05e
--- /dev/null
+++ b/spec/controllers/ci/projects_controller_spec.rb
@@ -0,0 +1,93 @@
+require "spec_helper"
+
+describe Ci::ProjectsController do
+ before do
+ @project = FactoryGirl.create :ci_project
+ end
+
+ describe "POST #build" do
+ it 'should respond 200 if params is ok' do
+ post :build, {
+ id: @project.id,
+ ref: 'master',
+ before: '2aa371379db71ac89ae20843fcff3b3477cf1a1d',
+ after: '1c8a9df454ef68c22c2a33cca8232bb50849e5c5',
+ token: @project.token,
+ ci_yaml_file: gitlab_ci_yaml,
+ commits: [ { message: "Message" } ]
+ }
+
+ expect(response).to be_success
+ expect(response.code).to eq('201')
+ end
+
+ it 'should respond 400 if push about removed branch' do
+ post :build, {
+ id: @project.id,
+ ref: 'master',
+ before: '2aa371379db71ac89ae20843fcff3b3477cf1a1d',
+ after: '0000000000000000000000000000000000000000',
+ token: @project.token,
+ ci_yaml_file: gitlab_ci_yaml
+ }
+
+ expect(response).not_to be_success
+ expect(response.code).to eq('400')
+ end
+
+ it 'should respond 400 if some params missed' do
+ post :build, id: @project.id, token: @project.token, ci_yaml_file: gitlab_ci_yaml
+ expect(response).not_to be_success
+ expect(response.code).to eq('400')
+ end
+
+ it 'should respond 403 if token is wrong' do
+ post :build, id: @project.id, token: 'invalid-token'
+ expect(response).not_to be_success
+ expect(response.code).to eq('403')
+ end
+ end
+
+ describe "POST /projects" do
+ let(:project_dump) { OpenStruct.new({ id: @project.gitlab_id }) }
+
+ let(:user) do
+ create(:user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ it "creates project" do
+ post :create, { project: JSON.dump(project_dump.to_h) }.with_indifferent_access
+
+ expect(response.code).to eq('302')
+ expect(assigns(:project)).not_to be_a_new(Ci::Project)
+ end
+
+ it "shows error" do
+ post :create, { project: JSON.dump(project_dump.to_h) }.with_indifferent_access
+
+ expect(response.code).to eq('302')
+ expect(flash[:alert]).to include("You have to have at least master role to enable CI for this project")
+ end
+ end
+
+ describe "GET /gitlab" do
+ let(:user) do
+ create(:user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ it "searches projects" do
+ xhr :get, :gitlab, { search: "str", format: "js" }.with_indifferent_access
+
+ expect(response).to be_success
+ expect(response.code).to eq('200')
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
new file mode 100644
index 0000000000..99da5a1877
--- /dev/null
+++ b/spec/factories/ci/builds.rb
@@ -0,0 +1,47 @@
+# == Schema Information
+#
+# Table name: builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# commit_id :integer
+# coverage :float
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_build, class: Ci::Build do
+ started_at 'Di 29. Okt 09:51:28 CET 2013'
+ finished_at 'Di 29. Okt 09:53:28 CET 2013'
+ commands 'ls -a'
+ options do
+ {
+ image: "ruby:2.1",
+ services: ["postgres"]
+ }
+ end
+
+ commit factory: :ci_commit
+
+ factory :ci_not_started_build do
+ started_at nil
+ finished_at nil
+ end
+ end
+end
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb
new file mode 100644
index 0000000000..70930c789c
--- /dev/null
+++ b/spec/factories/ci/commits.rb
@@ -0,0 +1,75 @@
+# == Schema Information
+#
+# Table name: commits
+#
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+FactoryGirl.define do
+ factory :ci_commit, class: Ci::Commit do
+ ref 'master'
+ before_sha '76de212e80737a608d939f648d959671fb0a0142'
+ sha '97de212e80737a608d939f648d959671fb0a0142'
+ push_data do
+ {
+ ref: 'refs/heads/master',
+ before: '76de212e80737a608d939f648d959671fb0a0142',
+ after: '97de212e80737a608d939f648d959671fb0a0142',
+ user_name: 'Git User',
+ user_email: 'git@example.com',
+ repository: {
+ name: 'test-data',
+ url: 'ssh://git@gitlab.com/test/test-data.git',
+ description: '',
+ homepage: 'http://gitlab.com/test/test-data'
+ },
+ commits: [
+ {
+ id: '97de212e80737a608d939f648d959671fb0a0142',
+ message: 'Test commit message',
+ timestamp: '2014-09-23T13:12:25+02:00',
+ url: 'https://gitlab.com/test/test-data/commit/97de212e80737a608d939f648d959671fb0a0142',
+ author: {
+ name: 'Git User',
+ email: 'git@user.com'
+ }
+ }
+ ],
+ total_commits_count: 1,
+ ci_yaml_file: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ }
+ end
+
+ factory :ci_commit_without_jobs do
+ after(:create) do |commit, evaluator|
+ commit.push_data[:ci_yaml_file] = YAML.dump({})
+ commit.save
+ end
+ end
+
+ factory :ci_commit_with_one_job do
+ after(:create) do |commit, evaluator|
+ commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" } })
+ commit.save
+ end
+ end
+
+ factory :ci_commit_with_two_jobs do
+ after(:create) do |commit, evaluator|
+ commit.push_data[:ci_yaml_file] = YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } })
+ commit.save
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/events.rb b/spec/factories/ci/events.rb
new file mode 100644
index 0000000000..9638618a40
--- /dev/null
+++ b/spec/factories/ci/events.rb
@@ -0,0 +1,24 @@
+# == Schema Information
+#
+# Table name: events
+#
+# id :integer not null, primary key
+# project_id :integer
+# user_id :integer
+# is_admin :integer
+# description :text
+# created_at :datetime
+# updated_at :datetime
+#
+
+FactoryGirl.define do
+ factory :ci_event, class: Ci::Event do
+ sequence :description do |n|
+ "updated project settings#{n}"
+ end
+
+ factory :ci_admin_event do
+ is_admin true
+ end
+ end
+end
diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb
new file mode 100644
index 0000000000..e6bd0685f8
--- /dev/null
+++ b/spec/factories/ci/projects.rb
@@ -0,0 +1,56 @@
+# == Schema Information
+#
+# Table name: projects
+#
+# id :integer not null, primary key
+# name :string(255) not null
+# timeout :integer default(3600), not null
+# created_at :datetime
+# updated_at :datetime
+# token :string(255)
+# default_ref :string(255)
+# path :string(255)
+# always_build :boolean default(FALSE), not null
+# polling_interval :integer
+# public :boolean default(FALSE), not null
+# ssh_url_to_repo :string(255)
+# gitlab_id :integer
+# allow_git_fetch :boolean default(TRUE), not null
+# email_recipients :string(255) default(""), not null
+# email_add_pusher :boolean default(TRUE), not null
+# email_only_broken_builds :boolean default(TRUE), not null
+# skip_refs :string(255)
+# coverage_regex :string(255)
+# shared_runners_enabled :boolean default(FALSE)
+# generated_yaml_config :text
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_project_without_token, class: Ci::Project do
+ sequence :name do |n|
+ "GitLab / gitlab-shell#{n}"
+ end
+
+ default_ref 'master'
+
+ sequence :path do |n|
+ "gitlab/gitlab-shell#{n}"
+ end
+
+ sequence :ssh_url_to_repo do |n|
+ "git@demo.gitlab.com:gitlab/gitlab-shell#{n}.git"
+ end
+
+ gl_project factory: :project
+
+ factory :ci_project do
+ token 'iPWx6WM4lhHNedGfBpPJNP'
+ end
+
+ factory :ci_public_project do
+ public true
+ end
+ end
+end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
new file mode 100644
index 0000000000..3aa14ca434
--- /dev/null
+++ b/spec/factories/ci/runner_projects.rb
@@ -0,0 +1,19 @@
+# == Schema Information
+#
+# Table name: runner_projects
+#
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_runner_project, class: Ci::RunnerProject do
+ runner_id 1
+ project_id 1
+ end
+end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
new file mode 100644
index 0000000000..db759eca9a
--- /dev/null
+++ b/spec/factories/ci/runners.rb
@@ -0,0 +1,38 @@
+# == Schema Information
+#
+# Table name: runners
+#
+# id :integer not null, primary key
+# token :string(255)
+# created_at :datetime
+# updated_at :datetime
+# description :string(255)
+# contacted_at :datetime
+# active :boolean default(TRUE), not null
+# is_shared :boolean default(FALSE)
+# name :string(255)
+# version :string(255)
+# revision :string(255)
+# platform :string(255)
+# architecture :string(255)
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_runner, class: Ci::Runner do
+ sequence :description do |n|
+ "My runner#{n}"
+ end
+
+ platform "darwin"
+
+ factory :ci_shared_runner do
+ is_shared true
+ end
+
+ factory :ci_specific_runner do
+ is_shared false
+ end
+ end
+end
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
new file mode 100644
index 0000000000..db053c610c
--- /dev/null
+++ b/spec/factories/ci/trigger_requests.rb
@@ -0,0 +1,13 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_trigger_request, class: Ci::TriggerRequest do
+ factory :ci_trigger_request_with_variables do
+ variables do
+ {
+ TRIGGER_KEY: 'TRIGGER_VALUE'
+ }
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb
new file mode 100644
index 0000000000..fd3afdb1ec
--- /dev/null
+++ b/spec/factories/ci/triggers.rb
@@ -0,0 +1,9 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_trigger_without_token, class: Ci::Trigger do
+ factory :ci_trigger do
+ token 'token'
+ end
+ end
+end
diff --git a/spec/factories/ci/web_hook.rb b/spec/factories/ci/web_hook.rb
new file mode 100644
index 0000000000..40d878ecb3
--- /dev/null
+++ b/spec/factories/ci/web_hook.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :ci_web_hook, class: Ci::WebHook do
+ sequence(:url) { FFaker::Internet.uri('http') }
+ project factory: :ci_project
+ end
+end
diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb
new file mode 100644
index 0000000000..88ef9c144a
--- /dev/null
+++ b/spec/features/ci/admin/builds_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe "Admin Builds" do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+
+ before do
+ skip_ci_admin_auth
+ login_as :user
+ end
+
+ describe "GET /admin/builds" do
+ before do
+ build
+ visit ci_admin_builds_path
+ end
+
+ it { expect(page).to have_content "All builds" }
+ it { expect(page).to have_content build.short_sha }
+ end
+
+ describe "Tabs" do
+ it "shows all builds" do
+ build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
+ build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
+ build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+
+ visit ci_admin_builds_path
+
+ expect(page.all(".build-link").size).to eq(4)
+ end
+
+ it "shows pending builds" do
+ build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
+ build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
+ build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+
+ visit ci_admin_builds_path
+
+ within ".nav.nav-tabs" do
+ click_on "Pending"
+ end
+
+ expect(page.find(".build-link")).to have_content(build.id)
+ expect(page.find(".build-link")).not_to have_content(build1.id)
+ expect(page.find(".build-link")).not_to have_content(build2.id)
+ expect(page.find(".build-link")).not_to have_content(build3.id)
+ end
+
+ it "shows running builds" do
+ build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
+ build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
+ build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+
+ visit ci_admin_builds_path
+
+ within ".nav.nav-tabs" do
+ click_on "Running"
+ end
+
+ expect(page.find(".build-link")).to have_content(build1.id)
+ expect(page.find(".build-link")).not_to have_content(build.id)
+ expect(page.find(".build-link")).not_to have_content(build2.id)
+ expect(page.find(".build-link")).not_to have_content(build3.id)
+ end
+ end
+end
diff --git a/spec/features/ci/admin/events_spec.rb b/spec/features/ci/admin/events_spec.rb
new file mode 100644
index 0000000000..a7e75cc4f6
--- /dev/null
+++ b/spec/features/ci/admin/events_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe "Admin Events" do
+ let(:event) { FactoryGirl.create :ci_admin_event }
+
+ before do
+ skip_ci_admin_auth
+ login_as :user
+ end
+
+ describe "GET /admin/events" do
+ before do
+ event
+ visit ci_admin_events_path
+ end
+
+ it { expect(page).to have_content "Events" }
+ it { expect(page).to have_content event.description }
+ end
+end
diff --git a/spec/features/ci/admin/projects_spec.rb b/spec/features/ci/admin/projects_spec.rb
new file mode 100644
index 0000000000..b88f55a680
--- /dev/null
+++ b/spec/features/ci/admin/projects_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe "Admin Projects" do
+ let(:project) { FactoryGirl.create :ci_project }
+
+ before do
+ skip_ci_admin_auth
+ login_as :user
+ end
+
+ describe "GET /admin/projects" do
+ before do
+ project
+ visit ci_admin_projects_path
+ end
+
+ it { expect(page).to have_content "Projects" }
+ end
+end
diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb
new file mode 100644
index 0000000000..b25121f080
--- /dev/null
+++ b/spec/features/ci/admin/runners_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe "Admin Runners" do
+ before do
+ skip_ci_admin_auth
+ login_as :user
+ end
+
+ describe "Runners page" do
+ before do
+ runner = FactoryGirl.create(:ci_runner)
+ commit = FactoryGirl.create(:ci_commit)
+ FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id)
+ visit ci_admin_runners_path
+ end
+
+ it { page.has_text? "Manage Runners" }
+ it { page.has_text? "To register a new runner" }
+ it { page.has_text? "Runners with last contact less than a minute ago: 1" }
+
+ describe 'search' do
+ before do
+ FactoryGirl.create :ci_runner, description: 'foo'
+ FactoryGirl.create :ci_runner, description: 'bar'
+
+ search_form = find('#runners-search')
+ search_form.fill_in 'search', with: 'foo'
+ search_form.click_button 'Search'
+ end
+
+ it { expect(page).to have_content("foo") }
+ it { expect(page).not_to have_content("bar") }
+ end
+ end
+
+ describe "Runner show page" do
+ let(:runner) { FactoryGirl.create :ci_runner }
+
+ before do
+ FactoryGirl.create(:ci_project, name: "foo")
+ FactoryGirl.create(:ci_project, name: "bar")
+ visit ci_admin_runner_path(runner)
+ end
+
+ describe 'runner info' do
+ it { expect(find_field('runner_token').value).to eq runner.token }
+ end
+
+ describe 'projects' do
+ it { expect(page).to have_content("foo") }
+ it { expect(page).to have_content("bar") }
+ end
+
+ describe 'search' do
+ before do
+ search_form = find('#runner-projects-search')
+ search_form.fill_in 'search', with: 'foo'
+ search_form.click_button 'Search'
+ end
+
+ it { expect(page).to have_content("foo") }
+ it { expect(page).not_to have_content("bar") }
+ end
+ end
+end
diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb
new file mode 100644
index 0000000000..2f020e524e
--- /dev/null
+++ b/spec/features/ci/builds_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe "Builds" do
+ context :private_project do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @commit = FactoryGirl.create :ci_commit, project: @project
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ login_as :user
+ @project.gl_project.team << [@user, :master]
+ end
+
+ describe "GET /:project/builds/:id" do
+ before do
+ visit ci_project_build_path(@project, @build)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
+ end
+
+ describe "GET /:project/builds/:id/cancel" do
+ before do
+ @build.run!
+ visit cancel_ci_project_build_path(@project, @build)
+ end
+
+ it { expect(page).to have_content 'canceled' }
+ it { expect(page).to have_content 'Retry' }
+ end
+
+ describe "POST /:project/builds/:id/retry" do
+ before do
+ @build.cancel!
+ visit ci_project_build_path(@project, @build)
+ click_link 'Retry'
+ end
+
+ it { expect(page).to have_content 'pending' }
+ it { expect(page).to have_content 'Cancel' }
+ end
+ end
+
+ context :public_project do
+ describe "Show page public accessible" do
+ before do
+ @project = FactoryGirl.create :ci_public_project
+ @commit = FactoryGirl.create :ci_commit, project: @project
+ @runner = FactoryGirl.create :ci_specific_runner
+ @build = FactoryGirl.create :ci_build, commit: @commit, runner: @runner
+
+ stub_gitlab_calls
+ visit ci_project_build_path(@project, @build)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ end
+ end
+end
diff --git a/spec/features/ci/commits_spec.rb b/spec/features/ci/commits_spec.rb
new file mode 100644
index 0000000000..40a62ca457
--- /dev/null
+++ b/spec/features/ci/commits_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe "Commits" do
+ include Ci::CommitsHelper
+
+ context "Authenticated user" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @commit = FactoryGirl.create :ci_commit, project: @project
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ login_as :user
+ @project.gl_project.team << [@user, :master]
+ end
+
+ describe "GET /:project/commits/:sha" do
+ before do
+ visit ci_commit_path(@commit)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
+ end
+
+ describe "Cancel commit" do
+ it "cancels commit" do
+ visit ci_commit_path(@commit)
+ click_on "Cancel"
+
+ expect(page).to have_content "canceled"
+ end
+ end
+
+ describe ".gitlab-ci.yml not found warning" do
+ it "does not show warning" do
+ visit ci_commit_path(@commit)
+
+ expect(page).not_to have_content ".gitlab-ci.yml not found in this commit"
+ end
+
+ it "shows warning" do
+ @commit.push_data[:ci_yaml_file] = nil
+ @commit.save
+
+ visit ci_commit_path(@commit)
+
+ expect(page).to have_content ".gitlab-ci.yml not found in this commit"
+ end
+ end
+ end
+
+ context "Public pages" do
+ before do
+ @project = FactoryGirl.create :ci_public_project
+ @commit = FactoryGirl.create :ci_commit, project: @project
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ end
+
+ describe "GET /:project/commits/:sha" do
+ before do
+ visit ci_commit_path(@commit)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
+ end
+ end
+end
diff --git a/spec/features/ci/events_spec.rb b/spec/features/ci/events_spec.rb
new file mode 100644
index 0000000000..5b9fd40415
--- /dev/null
+++ b/spec/features/ci/events_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe "Events" do
+ let(:user) { create(:user) }
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:event) { FactoryGirl.create :ci_admin_event, project: project }
+
+ before do
+ login_as(user)
+ project.gl_project.team << [user, :master]
+ end
+
+ describe "GET /ci/project/:id/events" do
+ before do
+ event
+ visit ci_project_events_path(project)
+ end
+
+ it { expect(page).to have_content "Events" }
+ it { expect(page).to have_content event.description }
+ end
+end
diff --git a/spec/features/ci/lint_spec.rb b/spec/features/ci/lint_spec.rb
new file mode 100644
index 0000000000..5d8f56e2cf
--- /dev/null
+++ b/spec/features/ci/lint_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe "Lint" do
+ before do
+ login_as :user
+ end
+
+ it "Yaml parsing", js: true do
+ content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ visit ci_lint_path
+ fill_in "content", with: content
+ click_on "Validate"
+ within "table" do
+ expect(page).to have_content("Job - rspec")
+ expect(page).to have_content("Job - spinach")
+ expect(page).to have_content("Deploy Job - staging")
+ expect(page).to have_content("Deploy Job - production")
+ end
+ end
+
+ it "Yaml parsing with error", js: true do
+ visit ci_lint_path
+ fill_in "content", with: ""
+ click_on "Validate"
+ expect(page).to have_content("Status: syntax is incorrect")
+ expect(page).to have_content("Error: Please provide content of .gitlab-ci.yml")
+ end
+end
diff --git a/spec/features/ci/projects_spec.rb b/spec/features/ci/projects_spec.rb
new file mode 100644
index 0000000000..ff17aeca44
--- /dev/null
+++ b/spec/features/ci/projects_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe "Projects" do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ @project = FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell"
+ @project.gl_project.team << [user, :master]
+ end
+
+ describe "GET /ci/projects", js: true do
+ before do
+ stub_js_gitlab_calls
+ visit ci_projects_path
+ end
+
+ it { expect(page).to have_content "GitLab / gitlab-shell" }
+ it { expect(page).to have_selector ".search input#search" }
+ end
+
+ describe "GET /ci/projects/:id" do
+ before do
+ visit ci_project_path(@project)
+ end
+
+ it { expect(page).to have_content @project.name }
+ it { expect(page).to have_content 'All commits' }
+ end
+
+ describe "GET /ci/projects/:id/edit" do
+ before do
+ visit edit_ci_project_path(@project)
+ end
+
+ it { expect(page).to have_content @project.name }
+ it { expect(page).to have_content 'Build Schedule' }
+
+ it "updates configuration" do
+ fill_in 'Timeout', with: '70'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'was successfully updated'
+
+ expect(find_field('Timeout').value).to eq '70'
+ end
+ end
+
+ describe "GET /ci/projects/:id/charts" do
+ before do
+ visit ci_project_charts_path(@project)
+ end
+
+ it { expect(page).to have_content 'Overall' }
+ it { expect(page).to have_content 'Builds chart for last week' }
+ it { expect(page).to have_content 'Builds chart for last month' }
+ it { expect(page).to have_content 'Builds chart for last year' }
+ it { expect(page).to have_content 'Commit duration in minutes for last 30 commits' }
+ end
+end
diff --git a/spec/features/ci/runners_spec.rb b/spec/features/ci/runners_spec.rb
new file mode 100644
index 0000000000..15147f15eb
--- /dev/null
+++ b/spec/features/ci/runners_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe "Runners" do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ end
+
+ describe "specific runners" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+
+ @project2 = FactoryGirl.create :ci_project
+ @project2.gl_project.team << [user, :master]
+
+ @shared_runner = FactoryGirl.create :ci_shared_runner
+ @specific_runner = FactoryGirl.create :ci_specific_runner
+ @specific_runner2 = FactoryGirl.create :ci_specific_runner
+ @project.runners << @specific_runner
+ @project2.runners << @specific_runner2
+ end
+
+ it "places runners in right places" do
+ visit ci_project_runners_path(@project)
+ expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name)
+ expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name)
+ expect(page.find(".available-shared-runners")).to have_content(@shared_runner.display_name)
+ end
+
+ it "enables specific runner for project" do
+ visit ci_project_runners_path(@project)
+
+ within ".available-specific-runners" do
+ click_on "Enable for this project"
+ end
+
+ expect(page.find(".activated-specific-runners")).to have_content(@specific_runner2.display_name)
+ end
+
+ it "disables specific runner for project" do
+ @project2.runners << @specific_runner
+
+ visit ci_project_runners_path(@project)
+
+ within ".activated-specific-runners" do
+ click_on "Disable for this project"
+ end
+
+ expect(page.find(".available-specific-runners")).to have_content(@specific_runner.display_name)
+ end
+
+ it "removes specific runner for project if this is last project for that runners" do
+ visit ci_project_runners_path(@project)
+
+ within ".activated-specific-runners" do
+ click_on "Remove runner"
+ end
+
+ expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey
+ end
+ end
+
+ describe "shared runners" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+ end
+
+ it "enables shared runners" do
+ visit ci_project_runners_path(@project)
+
+ click_on "Enable shared runners"
+
+ expect(@project.reload.shared_runners_enabled).to be_truthy
+ end
+ end
+
+ describe "show page" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+ @specific_runner = FactoryGirl.create :ci_specific_runner
+ @project.runners << @specific_runner
+ end
+
+ it "shows runner information" do
+ visit ci_project_runners_path(@project)
+
+ click_on @specific_runner.short_sha
+
+ expect(page).to have_content(@specific_runner.platform)
+ end
+ end
+end
diff --git a/spec/features/ci/triggers_spec.rb b/spec/features/ci/triggers_spec.rb
new file mode 100644
index 0000000000..c6afeb7462
--- /dev/null
+++ b/spec/features/ci/triggers_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'Triggers' do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+ visit ci_project_triggers_path(@project)
+ end
+
+ context 'create a trigger' do
+ before do
+ click_on 'Add Trigger'
+ expect(@project.triggers.count).to eq(1)
+ end
+
+ it 'contains trigger token' do
+ expect(page).to have_content(@project.triggers.first.token)
+ end
+
+ it 'revokes the trigger' do
+ click_on 'Revoke'
+ expect(@project.triggers.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/features/ci/variables_spec.rb b/spec/features/ci/variables_spec.rb
new file mode 100644
index 0000000000..e387b3be55
--- /dev/null
+++ b/spec/features/ci/variables_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe "Variables" do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ end
+
+ describe "specific runners" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+ end
+
+ it "creates variable", js: true do
+ visit ci_project_variables_path(@project)
+ click_on "Add a variable"
+ fill_in "Key", with: "SECRET_KEY"
+ fill_in "Value", with: "SECRET_VALUE"
+ click_on "Save changes"
+
+ expect(page).to have_content("Variables were successfully updated.")
+ expect(@project.variables.count).to eq(1)
+ end
+
+ end
+end
diff --git a/spec/helpers/ci/application_helper_spec.rb b/spec/helpers/ci/application_helper_spec.rb
new file mode 100644
index 0000000000..6a216715b7
--- /dev/null
+++ b/spec/helpers/ci/application_helper_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Ci::ApplicationHelper do
+ describe "#duration_in_words" do
+ it "returns minutes and seconds" do
+ intervals_in_words = {
+ 100 => "1 minute 40 seconds",
+ 121 => "2 minutes 1 second",
+ 3721 => "62 minutes 1 second",
+ 0 => "0 seconds"
+ }
+
+ intervals_in_words.each do |interval, expectation|
+ expect(duration_in_words(Time.now + interval, Time.now)).to eq(expectation)
+ end
+ end
+
+ it "calculates interval from now if there is no finished_at" do
+ expect(duration_in_words(nil, Time.now - 5)).to eq("5 seconds")
+ end
+ end
+
+ describe "#time_interval_in_words" do
+ it "returns minutes and seconds" do
+ intervals_in_words = {
+ 100 => "1 minute 40 seconds",
+ 121 => "2 minutes 1 second",
+ 3721 => "62 minutes 1 second",
+ 0 => "0 seconds"
+ }
+
+ intervals_in_words.each do |interval, expectation|
+ expect(time_interval_in_words(interval)).to eq(expectation)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
new file mode 100644
index 0000000000..6d0e2d3d1e
--- /dev/null
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Ci::RunnersHelper do
+ it "returns - not contacted yet" do
+ runner = FactoryGirl.build :ci_runner
+ expect(runner_status_icon(runner)).to include("not connected yet")
+ end
+
+ it "returns offline text" do
+ runner = FactoryGirl.build(:ci_runner, contacted_at: 1.day.ago, active: true)
+ expect(runner_status_icon(runner)).to include("Runner is offline")
+ end
+
+ it "returns online text" do
+ runner = FactoryGirl.build(:ci_runner, contacted_at: 1.hour.ago, active: true)
+ expect(runner_status_icon(runner)).to include("Runner is online")
+ end
+end
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
new file mode 100644
index 0000000000..75c023bbc4
--- /dev/null
+++ b/spec/lib/ci/ansi2html_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe Ci::Ansi2html do
+ subject { Ci::Ansi2html }
+
+ it "prints non-ansi as-is" do
+ expect(subject.convert("Hello")).to eq('Hello')
+ end
+
+ it "strips non-color-changing controll sequences" do
+ expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world')
+ end
+
+ it "prints simply red" do
+ expect(subject.convert("\e[31mHello\e[0m")).to eq('Hello')
+ end
+
+ it "prints simply red without trailing reset" do
+ expect(subject.convert("\e[31mHello")).to eq('Hello')
+ end
+
+ it "prints simply yellow" do
+ expect(subject.convert("\e[33mHello\e[0m")).to eq('Hello')
+ end
+
+ it "prints default on blue" do
+ expect(subject.convert("\e[39;44mHello")).to eq('Hello')
+ end
+
+ it "prints red on blue" do
+ expect(subject.convert("\e[31;44mHello")).to eq('Hello')
+ end
+
+ it "resets colors after red on blue" do
+ expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('Hello world')
+ end
+
+ it "performs color change from red/blue to yellow/blue" do
+ expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('Hello world')
+ end
+
+ it "performs color change from red/blue to yellow/green" do
+ expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('Hello world')
+ end
+
+ it "performs color change from red/blue to reset to yellow/green" do
+ expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('Helloworld')
+ end
+
+ it "ignores unsupported codes" do
+ expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello')
+ end
+
+ it "prints light red" do
+ expect(subject.convert("\e[91mHello\e[0m")).to eq('Hello')
+ end
+
+ it "prints default on light red" do
+ expect(subject.convert("\e[101mHello\e[0m")).to eq('Hello')
+ end
+
+ it "performs color change from red/blue to default/blue" do
+ expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('Hello world')
+ end
+
+ it "performs color change from light red/blue to default/blue" do
+ expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('Hello world')
+ end
+
+ it "prints bold text" do
+ expect(subject.convert("\e[1mHello")).to eq('Hello')
+ end
+
+ it "resets bold text" do
+ expect(subject.convert("\e[1mHello\e[21m world")).to eq('Hello world')
+ expect(subject.convert("\e[1mHello\e[22m world")).to eq('Hello world')
+ end
+
+ it "prints italic text" do
+ expect(subject.convert("\e[3mHello")).to eq('Hello')
+ end
+
+ it "resets italic text" do
+ expect(subject.convert("\e[3mHello\e[23m world")).to eq('Hello world')
+ end
+
+ it "prints underlined text" do
+ expect(subject.convert("\e[4mHello")).to eq('Hello')
+ end
+
+ it "resets underlined text" do
+ expect(subject.convert("\e[4mHello\e[24m world")).to eq('Hello world')
+ end
+
+ it "prints concealed text" do
+ expect(subject.convert("\e[8mHello")).to eq('Hello')
+ end
+
+ it "resets concealed text" do
+ expect(subject.convert("\e[8mHello\e[28m world")).to eq('Hello world')
+ end
+
+ it "prints crossed-out text" do
+ expect(subject.convert("\e[9mHello")).to eq('Hello')
+ end
+
+ it "resets crossed-out text" do
+ expect(subject.convert("\e[9mHello\e[29m world")).to eq('Hello world')
+ end
+
+ it "can print 256 xterm fg colors" do
+ expect(subject.convert("\e[38;5;16mHello")).to eq('Hello')
+ end
+
+ it "can print 256 xterm fg colors on normal magenta background" do
+ expect(subject.convert("\e[38;5;16;45mHello")).to eq('Hello')
+ end
+
+ it "can print 256 xterm bg colors" do
+ expect(subject.convert("\e[48;5;240mHello")).to eq('Hello')
+ end
+
+ it "can print 256 xterm bg colors on normal magenta foreground" do
+ expect(subject.convert("\e[48;5;16;35mHello")).to eq('Hello')
+ end
+
+ it "prints bold colored text vividly" do
+ expect(subject.convert("\e[1;31mHello\e[0m")).to eq('Hello')
+ end
+
+ it "prints bold light colored text correctly" do
+ expect(subject.convert("\e[1;91mHello\e[0m")).to eq('Hello')
+ end
+end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
new file mode 100644
index 0000000000..24894e8198
--- /dev/null
+++ b/spec/lib/ci/charts_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe "Charts" do
+
+ context "build_times" do
+ before do
+ @project = FactoryGirl.create(:ci_project)
+ @commit = FactoryGirl.create(:ci_commit, project: @project)
+ FactoryGirl.create(:ci_build, commit: @commit)
+ end
+
+ it 'should return build times in minutes' do
+ chart = Ci::Charts::BuildTime.new(@project)
+ expect(chart.build_times).to eq([2])
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
new file mode 100644
index 0000000000..49482ac2b1
--- /dev/null
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -0,0 +1,313 @@
+require 'spec_helper'
+
+module Ci
+ describe GitlabCiYamlProcessor do
+
+ describe "#builds_for_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)
+
+ 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",
+ except: nil,
+ name: :rspec,
+ only: nil,
+ script: "pwd\nrspec",
+ tags: [],
+ options: {},
+ allow_failure: false
+ })
+ end
+
+ 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)
+
+ 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)
+
+ 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)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "does not build tags" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["tags"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "0-1", true).size).to eq(0)
+ 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: ["master", "deploy"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "returns build only for specified type" do
+
+ config = YAML.dump({
+ before_script: ["pwd"],
+ build: { script: "build", type: "build", only: ["master", "deploy"] },
+ rspec: { script: "rspec", type: type, only: ["master", "deploy"] },
+ staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ production: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0)
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ end
+ end
+
+ describe "Image and service handling" do
+ it "returns image and service when defined" do
+ config = YAML.dump({
+ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ 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).to eq({
+ except: nil,
+ stage: "test",
+ name: :rspec,
+ only: nil,
+ script: "pwd\nrspec",
+ tags: [],
+ options: {
+ image: "ruby:2.1",
+ services: ["mysql"]
+ },
+ allow_failure: false
+ })
+ 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"], 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({
+ except: nil,
+ stage: "test",
+ name: :rspec,
+ only: nil,
+ script: "pwd\nrspec",
+ tags: [],
+ options: {
+ image: "ruby:2.5",
+ services: ["postgresql"]
+ },
+ allow_failure: false
+ })
+ end
+ end
+
+ describe "Variables" do
+ it "returns variables when defined" do
+ variables = {
+ var1: "value1",
+ var2: "value2",
+ }
+ config = YAML.dump({
+ variables: variables,
+ before_script: ["pwd"],
+ rspec: { script: "rspec" }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.variables).to eq(variables)
+ end
+ end
+
+ describe "Error handling" do
+ it "indicates that object is invalid" do
+ expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string")
+ end
+
+ it "returns errors if job image parameter is invalid" do
+ config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
+ 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
+ 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ end
+
+ it "returns errors if there are unknown parameters" do
+ config = YAML.dump({ extra: "bundle update" })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ end
+
+ it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do
+ config = YAML.dump({ extra: { services: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ end
+
+ it "returns errors if there is no any jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"] })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean")
+ end
+
+ it "returns errors if job stage is not a string" do
+ config = YAML.dump({ rspec: { script: "test", type: 1, allow_failure: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ 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 pre-defined stage" do
+ config = YAML.dump({ rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ 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: ["build", "test"], rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ 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({ types: "test", rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings")
+ end
+
+ it "returns errors if stages is not an array of strings" do
+ config = YAML.dump({ types: [true, "test"], rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages 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)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
+ end
+
+ it "returns errors if variables is not a map of key-valued strings" do
+ config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
+ end
+ end
+ end
+end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 9c115bbfc6..48bc60eed1 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ExtractsPath do
include ExtractsPath
include RepoHelpers
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
let(:project) { double('project') }
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
new file mode 100644
index 0000000000..18c7924fea
--- /dev/null
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::LDAP::AuthHash do
+ let(:auth_hash) do
+ Gitlab::LDAP::AuthHash.new(
+ OmniAuth::AuthHash.new(
+ uid: '123456',
+ provider: 'ldapmain',
+ info: info,
+ extra: {
+ raw_info: raw_info
+ }
+ )
+ )
+ end
+
+ let(:info) do
+ {
+ name: 'Smith, J.',
+ email: 'johnsmith@example.com',
+ nickname: '123456'
+ }
+ end
+
+ let(:raw_info) do
+ {
+ uid: '123456',
+ email: 'johnsmith@example.com',
+ cn: 'Smith, J.',
+ fullName: 'John Smith'
+ }
+ end
+
+ context "without overridden attributes" do
+
+ it "has the correct username" do
+ expect(auth_hash.username).to eq("123456")
+ end
+
+ it "has the correct name" do
+ expect(auth_hash.name).to eq("Smith, J.")
+ end
+ end
+
+ context "with overridden attributes" do
+ let(:attributes) do
+ {
+ username: ['mail', 'email'],
+ name: 'fullName'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::LDAP::Config).to receive(:attributes).and_return(attributes)
+ end
+
+ it "has the correct username" do
+ expect(auth_hash.username).to eq("johnsmith@example.com")
+ end
+
+ it "has the correct name" do
+ expect(auth_hash.name).to eq("John Smith")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 84d9fb54b6..fd2e5f6d0e 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::LDAP::User do
}
end
let(:auth_hash) do
- double(uid: 'my-uid', provider: 'ldapmain', info: double(info))
+ OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info)
end
describe :changed? do
diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
index e4a6cd954c..5632f2306e 100644
--- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
@@ -3,11 +3,11 @@ require 'spec_helper'
describe Gitlab::OAuth::AuthHash do
let(:auth_hash) do
Gitlab::OAuth::AuthHash.new(
- double({
+ OmniAuth::AuthHash.new(
provider: provider_ascii,
uid: uid_ascii,
- info: double(info_hash)
- })
+ info: info_hash
+ )
)
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index c6cca98a03..c0083fc85b 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::OAuth::User do
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
let(:provider) { 'my-provider' }
- let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
let(:info_hash) do
{
nickname: '-john+gitlab-ETC%.git@gmail.com',
diff --git a/spec/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb
new file mode 100644
index 0000000000..20d8ddcd13
--- /dev/null
+++ b/spec/mailers/ci/notify_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Ci::Notify do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
+
+ before do
+ @project = FactoryGirl.create :ci_project
+ @commit = FactoryGirl.create :ci_commit, project: @project
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ end
+
+ describe 'build success' do
+ subject { Ci::Notify.build_success_email(@build.id, 'wow@example.com') }
+
+ it 'has the correct subject' do
+ should have_subject /Build success for/
+ end
+
+ it 'contains name of project' do
+ should have_body_text /build successful/
+ end
+ end
+
+ describe 'build fail' do
+ subject { Ci::Notify.build_fail_email(@build.id, 'wow@example.com') }
+
+ it 'has the correct subject' do
+ should have_subject /Build failed for/
+ end
+
+ it 'contains name of project' do
+ should have_body_text /build failed/
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
new file mode 100644
index 0000000000..ce80115204
--- /dev/null
+++ b/spec/models/ci/build_spec.rb
@@ -0,0 +1,350 @@
+# == Schema Information
+#
+# Table name: builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# commit_id :integer
+# coverage :float
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+#
+
+require 'spec_helper'
+
+describe Ci::Build do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+
+ it { is_expected.to belong_to(:commit) }
+ it { is_expected.to validate_presence_of :status }
+
+ it { is_expected.to respond_to :success? }
+ it { is_expected.to respond_to :failed? }
+ it { is_expected.to respond_to :running? }
+ it { is_expected.to respond_to :pending? }
+ it { is_expected.to respond_to :trace_html }
+
+ describe :first_pending do
+ let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday }
+ let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' }
+ before { first; second }
+ subject { Ci::Build.first_pending }
+
+ it { is_expected.to be_a(Ci::Build) }
+ it('returns with the first pending build') { is_expected.to eq(first) }
+ end
+
+ describe :create_from do
+ before do
+ build.status = 'success'
+ build.save
+ end
+ let(:create_from_build) { Ci::Build.create_from build }
+
+ it 'there should be a pending task' do
+ expect(Ci::Build.pending.count(:all)).to eq 0
+ create_from_build
+ expect(Ci::Build.pending.count(:all)).to be > 0
+ end
+ end
+
+ describe :started? do
+ subject { build.started? }
+
+ context 'without started_at' do
+ before { build.started_at = nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ %w(running success failed).each do |status|
+ context "if build status is #{status}" do
+ before { build.status = status }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(pending canceled).each do |status|
+ context "if build status is #{status}" do
+ before { build.status = status }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :active? do
+ subject { build.active? }
+
+ %w(pending running).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(success failed canceled).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :complete? do
+ subject { build.complete? }
+
+ %w(success failed canceled).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(pending running).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :ignored? do
+ subject { build.ignored? }
+
+ context 'if build is not allowed to fail' do
+ before { build.allow_failure = false }
+
+ context 'and build.status is success' do
+ before { build.status = 'success' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before { build.status = 'failed' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'if build is allowed to fail' do
+ before { build.allow_failure = true }
+
+ context 'and build.status is success' do
+ before { build.status = 'success' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before { build.status = 'failed' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe :trace do
+ subject { build.trace_html }
+
+ it { is_expected.to be_empty }
+
+ context 'if build.trace contains text' do
+ let(:text) { 'example output' }
+ before { build.trace = text }
+
+ it { is_expected.to include(text) }
+ it { expect(subject.length).to be >= text.length }
+ end
+ end
+
+ describe :timeout do
+ subject { build.timeout }
+
+ it { is_expected.to eq(commit.project.timeout) }
+ end
+
+ describe :duration do
+ subject { build.duration }
+
+ it { is_expected.to eq(120.0) }
+
+ context 'if the building process has not started yet' do
+ before do
+ build.started_at = nil
+ build.finished_at = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'if the building process has started' do
+ before do
+ build.started_at = Time.now - 1.minute
+ build.finished_at = nil
+ end
+
+ it { is_expected.to be_a(Float) }
+ it { is_expected.to be > 0.0 }
+ end
+ end
+
+ describe :options do
+ let(:options) do
+ {
+ image: "ruby:2.1",
+ services: [
+ "postgres"
+ ]
+ }
+ end
+
+ subject { build.options }
+ it { is_expected.to eq(options) }
+ end
+
+ describe :ref do
+ subject { build.ref }
+
+ it { is_expected.to eq(commit.ref) }
+ end
+
+ describe :sha do
+ subject { build.sha }
+
+ it { is_expected.to eq(commit.sha) }
+ end
+
+ describe :short_sha do
+ subject { build.short_sha }
+
+ it { is_expected.to eq(commit.short_sha) }
+ end
+
+ describe :before_sha do
+ subject { build.before_sha }
+
+ it { is_expected.to eq(commit.before_sha) }
+ end
+
+ describe :allow_git_fetch do
+ subject { build.allow_git_fetch }
+
+ it { is_expected.to eq(project.allow_git_fetch) }
+ end
+
+ describe :project do
+ subject { build.project }
+
+ it { is_expected.to eq(commit.project) }
+ end
+
+ describe :project_id do
+ subject { build.project_id }
+
+ it { is_expected.to eq(commit.project_id) }
+ end
+
+ describe :project_name do
+ subject { build.project_name }
+
+ it { is_expected.to eq(project.name) }
+ end
+
+ describe :repo_url do
+ subject { build.repo_url }
+
+ it { is_expected.to eq(project.repo_url_with_auth) }
+ end
+
+ describe :extract_coverage do
+ context 'valid content & regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+
+ context 'valid content & bad regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'no coverage content & regex' do
+ subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'multiple results in content & regex' do
+ subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+ end
+
+ describe :variables do
+ context 'returns variables' do
+ subject { build.variables }
+
+ let(:variables) do
+ [
+ { key: :DB_NAME, value: 'postgres', public: true }
+ ]
+ end
+
+ it { is_expected.to eq(variables) }
+
+ context 'and secure variables' do
+ let(:secure_variables) do
+ [
+ { key: 'SECRET_KEY', value: 'secret_value', public: false }
+ ]
+ end
+
+ before do
+ build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ end
+
+ it { is_expected.to eq(variables + secure_variables) }
+
+ context 'and trigger variables' do
+ let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
+ let(:trigger_request) { FactoryGirl.create :ci_trigger_request_with_variables, commit: commit, trigger: trigger }
+ let(:trigger_variables) do
+ [
+ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false }
+ ]
+ end
+
+ before do
+ build.trigger_request = trigger_request
+ end
+
+ it { is_expected.to eq(variables + secure_variables + trigger_variables) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
new file mode 100644
index 0000000000..586c9dc23a
--- /dev/null
+++ b/spec/models/ci/commit_spec.rb
@@ -0,0 +1,268 @@
+# == Schema Information
+#
+# Table name: commits
+#
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+#
+
+require 'spec_helper'
+
+describe Ci::Commit do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:commit_with_project) { FactoryGirl.create :ci_commit, project: project }
+ let(:config_processor) { Ci::GitlabCiYamlProcessor.new(gitlab_ci_yaml) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:builds) }
+ it { is_expected.to validate_presence_of :before_sha }
+ it { is_expected.to validate_presence_of :sha }
+ it { is_expected.to validate_presence_of :ref }
+ it { is_expected.to validate_presence_of :push_data }
+
+ it { is_expected.to respond_to :git_author_name }
+ it { is_expected.to respond_to :git_author_email }
+ it { is_expected.to respond_to :short_sha }
+
+ describe :last_build do
+ subject { commit.last_build }
+ before do
+ @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
+ @second = FactoryGirl.create :ci_build, commit: commit
+ end
+
+ it { is_expected.to be_a(Ci::Build) }
+ it('returns with the most recently created build') { is_expected.to eq(@second) }
+ end
+
+ describe :retry do
+ before do
+ @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
+ @second = FactoryGirl.create :ci_build, commit: commit
+ end
+
+ it "creates new build" do
+ expect(commit.builds.count(:all)).to eq 2
+ commit.retry
+ expect(commit.builds.count(:all)).to eq 3
+ end
+ end
+
+ describe :project_recipients do
+
+ context 'always sending notification' do
+ it 'should return commit_pusher_email as only recipient when no additional recipients are given' do
+ project = FactoryGirl.create :ci_project,
+ email_add_pusher: true,
+ email_recipients: ''
+ commit = FactoryGirl.create :ci_commit, project: project
+ expected = 'commit_pusher_email'
+ allow(commit).to receive(:push_data) { { user_email: expected } }
+ expect(commit.project_recipients).to eq([expected])
+ end
+
+ it 'should return commit_pusher_email and additional recipients' do
+ project = FactoryGirl.create :ci_project,
+ email_add_pusher: true,
+ email_recipients: 'rec1 rec2'
+ commit = FactoryGirl.create :ci_commit, project: project
+ expected = 'commit_pusher_email'
+ allow(commit).to receive(:push_data) { { user_email: expected } }
+ expect(commit.project_recipients).to eq(['rec1', 'rec2', expected])
+ end
+
+ it 'should return recipients' do
+ project = FactoryGirl.create :ci_project,
+ email_add_pusher: false,
+ email_recipients: 'rec1 rec2'
+ commit = FactoryGirl.create :ci_commit, project: project
+ expect(commit.project_recipients).to eq(['rec1', 'rec2'])
+ end
+
+ it 'should return unique recipients only' do
+ project = FactoryGirl.create :ci_project,
+ email_add_pusher: true,
+ email_recipients: 'rec1 rec1 rec2'
+ commit = FactoryGirl.create :ci_commit, project: project
+ expected = 'rec2'
+ allow(commit).to receive(:push_data) { { user_email: expected } }
+ expect(commit.project_recipients).to eq(['rec1', 'rec2'])
+ end
+ end
+ end
+
+ describe :valid_commit_sha do
+ context 'commit.sha can not start with 00000000' do
+ before do
+ commit.sha = '0' * 40
+ commit.valid_commit_sha
+ end
+
+ it('commit errors should not be empty') { expect(commit.errors).not_to be_empty }
+ end
+ end
+
+ describe :compare? do
+ subject { commit_with_project.compare? }
+
+ context 'if commit.before_sha are not nil' do
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :short_sha do
+ subject { commit.short_before_sha }
+
+ it 'has 8 items' do
+ expect(subject.size).to eq(8)
+ end
+ it { expect(commit.before_sha).to start_with(subject) }
+ end
+
+ describe :short_sha do
+ subject { commit.short_sha }
+
+ it 'has 8 items' do
+ expect(subject.size).to eq(8)
+ end
+ it { expect(commit.sha).to start_with(subject) }
+ end
+
+ describe :create_next_builds do
+ before do
+ allow(commit).to receive(:config_processor).and_return(config_processor)
+ end
+
+ it "creates builds for next type" do
+ expect(commit.create_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+
+ expect(commit.create_next_builds(nil)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(4)
+
+ expect(commit.create_next_builds(nil)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(5)
+
+ expect(commit.create_next_builds(nil)).to be_falsey
+ end
+ end
+
+ describe :create_builds do
+ before do
+ allow(commit).to receive(:config_processor).and_return(config_processor)
+ end
+
+ it 'creates builds' do
+ expect(commit.create_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+ end
+
+ context 'for build triggers' do
+ let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
+ let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger }
+
+ it 'creates builds' do
+ expect(commit.create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+ end
+
+ it 'rebuilds commit' do
+ expect(commit.create_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+
+ expect(commit.create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(4)
+ end
+
+ it 'creates next builds' do
+ expect(commit.create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+
+ expect(commit.create_next_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(4)
+ end
+
+ context 'for [ci skip]' do
+ before do
+ commit.push_data[:commits][0][:message] = 'skip this commit [ci skip]'
+ commit.save
+ end
+
+ it 'rebuilds commit' do
+ expect(commit.status).to eq('skipped')
+ expect(commit.create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+ expect(commit.status).to eq('pending')
+ end
+ end
+ end
+ end
+
+ describe "#finished_at" do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+
+ it "returns finished_at of latest build" do
+ build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60
+ build1 = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120
+
+ expect(commit.finished_at.to_i).to eq(build.finished_at.to_i)
+ end
+
+ it "returns nil if there is no finished build" do
+ build = FactoryGirl.create :ci_not_started_build, commit: commit
+
+ expect(commit.finished_at).to be_nil
+ end
+ end
+
+ describe "coverage" do
+ let(:project) { FactoryGirl.create :ci_project, coverage_regex: "/.*/" }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+
+ it "calculates average when there are two builds with coverage" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
+ expect(commit.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one with nil" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
+ FactoryGirl.create :ci_build, commit: commit
+ expect(commit.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one is retried" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
+ expect(commit.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there is one build without coverage" do
+ FactoryGirl.create :ci_build, commit: commit
+ expect(commit.coverage).to be_nil
+ end
+ end
+end
diff --git a/spec/models/ci/mail_service_spec.rb b/spec/models/ci/mail_service_spec.rb
new file mode 100644
index 0000000000..b5f37b349d
--- /dev/null
+++ b/spec/models/ci/mail_service_spec.rb
@@ -0,0 +1,184 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+require 'spec_helper'
+
+describe Ci::MailService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Validations" do
+ context "active" do
+ before do
+ subject.active = true
+ end
+ end
+ end
+
+ describe 'Sends email for' do
+ let(:mail) { Ci::MailService.new }
+
+ describe 'failed build' do
+ let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) }
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email("git@example.com")
+ mail.execute(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).to receive(:build_fail_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
+ end
+ end
+
+ describe 'successfull build' do
+ let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) }
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email("git@example.com")
+ mail.execute(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+
+ describe 'successfull build and project has email_recipients' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: false,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email("git@example.com")
+ should_email("jeroen@example.com")
+ mail.execute(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+
+ describe 'successful build and notify only broken builds' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: true,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email(commit.git_author_email)
+ should_email("jeroen@example.com")
+ mail.execute(build) if mail.can_execute?(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+
+ describe 'successful build and can test service' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: false,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ build
+ end
+
+ it do
+ expect(mail.can_test?).to eq(true)
+ end
+ end
+
+ describe 'retried build should not receive email' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: true,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ Ci::Build.retry(build)
+ should_email(commit.git_author_email)
+ should_email("jeroen@example.com")
+ mail.execute(build) if mail.can_execute?(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb
new file mode 100644
index 0000000000..49ac086025
--- /dev/null
+++ b/spec/models/ci/project_services/hip_chat_message_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Ci::HipChatMessage do
+ subject { Ci::HipChatMessage.new(build) }
+
+ let(:project) { FactoryGirl.create(:ci_project) }
+
+ context "One build" do
+ let(:commit) { FactoryGirl.create(:ci_commit_with_one_job, project: project) }
+
+ let(:build) do
+ commit.create_builds
+ commit.builds.first
+ end
+
+ context 'when build succeeds' do
+ it 'returns a successful message' do
+ build.update(status: "success")
+
+ expect( subject.status_color ).to eq 'green'
+ expect( subject.notify? ).to be_falsey
+ expect( subject.to_s ).to match(/Build '[^']+' #\d+/)
+ expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./)
+ end
+ end
+
+ context 'when build fails' do
+ it 'returns a failure message' do
+ build.update(status: "failed")
+
+ expect( subject.status_color ).to eq 'red'
+ expect( subject.notify? ).to be_truthy
+ expect( subject.to_s ).to match(/Build '[^']+' #\d+/)
+ expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./)
+ end
+ end
+ end
+
+ context "Several builds" do
+ let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs, project: project) }
+
+ let(:build) do
+ commit.builds.first
+ end
+
+ context 'when all matrix builds succeed' do
+ it 'returns a successful message' do
+ commit.create_builds
+ commit.builds.update_all(status: "success")
+ commit.reload
+
+ expect( subject.status_color ).to eq 'green'
+ expect( subject.notify? ).to be_falsey
+ expect( subject.to_s ).to match(/Commit #\d+/)
+ expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./)
+ end
+ end
+
+ context 'when at least one matrix build fails' do
+ it 'returns a failure message' do
+ commit.create_builds
+ first_build = commit.builds.first
+ second_build = commit.builds.last
+ first_build.update(status: "success")
+ second_build.update(status: "failed")
+
+ expect( subject.status_color ).to eq 'red'
+ expect( subject.notify? ).to be_truthy
+ expect( subject.to_s ).to match(/Commit #\d+/)
+ expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb
new file mode 100644
index 0000000000..063d46b84d
--- /dev/null
+++ b/spec/models/ci/project_services/hip_chat_service_spec.rb
@@ -0,0 +1,74 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+
+require 'spec_helper'
+
+describe Ci::HipChatService do
+
+ describe "Validations" do
+
+ context "active" do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :hipchat_room }
+ it { is_expected.to validate_presence_of :hipchat_token }
+
+ end
+ end
+
+ describe "Execute" do
+
+ let(:service) { Ci::HipChatService.new }
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
+ let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' }
+
+ before do
+ allow(service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ notify_only_broken_builds: false,
+ hipchat_room: 123,
+ hipchat_token: 'a1b2c3d4e5f6'
+ )
+
+ WebMock.stub_request(:post, api_url)
+ end
+
+
+ it "should call the HipChat API" do
+ service.execute(build)
+ Ci::HipChatNotifierWorker.drain
+
+ expect( WebMock ).to have_requested(:post, api_url).once
+ end
+
+ it "calls the worker with expected arguments" do
+ expect( Ci::HipChatNotifierWorker ).to receive(:perform_async) \
+ .with(an_instance_of(String), hash_including(
+ token: 'a1b2c3d4e5f6',
+ room: 123,
+ server: 'https://api.hipchat.com',
+ color: 'red',
+ notify: true
+ ))
+
+ service.execute(build)
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb
new file mode 100644
index 0000000000..f533590372
--- /dev/null
+++ b/spec/models/ci/project_services/slack_message_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe Ci::SlackMessage do
+ subject { Ci::SlackMessage.new(commit) }
+
+ let(:project) { FactoryGirl.create :ci_project }
+
+ context "One build" do
+ let(:commit) { FactoryGirl.create(:ci_commit_with_one_job, project: project) }
+
+ let(:build) do
+ commit.create_builds
+ commit.builds.first
+ end
+
+ context 'when build succeeded' do
+ let(:color) { 'good' }
+
+ it 'returns a message with succeeded build' do
+ build.update(status: "success")
+
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Build')
+ expect(subject.fallback).to include("\##{build.id}")
+ expect(subject.fallback).to include('succeeded')
+ expect(subject.attachments.first[:fields]).to be_empty
+ end
+ end
+
+ context 'when build failed' do
+ let(:color) { 'danger' }
+
+ it 'returns a message with failed build' do
+ build.update(status: "failed")
+
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Build')
+ expect(subject.fallback).to include("\##{build.id}")
+ expect(subject.fallback).to include('failed')
+ expect(subject.attachments.first[:fields]).to be_empty
+ end
+ end
+ end
+
+ context "Several builds" do
+ let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs, project: project) }
+
+ context 'when all matrix builds succeeded' do
+ let(:color) { 'good' }
+
+ it 'returns a message with success' do
+ commit.create_builds
+ commit.builds.update_all(status: "success")
+ commit.reload
+
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Commit')
+ expect(subject.fallback).to include("\##{commit.id}")
+ expect(subject.fallback).to include('succeeded')
+ expect(subject.attachments.first[:fields]).to be_empty
+ end
+ end
+
+ context 'when one of matrix builds failed' do
+ let(:color) { 'danger' }
+
+ it 'returns a message with information about failed build' do
+ commit.create_builds
+ first_build = commit.builds.first
+ second_build = commit.builds.last
+ first_build.update(status: "success")
+ second_build.update(status: "failed")
+
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Commit')
+ expect(subject.fallback).to include("\##{commit.id}")
+ expect(subject.fallback).to include('failed')
+ expect(subject.attachments.first[:fields].size).to eq(1)
+ expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name)
+ expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}")
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb
new file mode 100644
index 0000000000..0524f47243
--- /dev/null
+++ b/spec/models/ci/project_services/slack_service_spec.rb
@@ -0,0 +1,58 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+require 'spec_helper'
+
+describe Ci::SlackService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Validations" do
+ context "active" do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :webhook }
+ end
+ end
+
+ describe "Execute" do
+ let(:slack) { Ci::SlackService.new }
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
+ let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
+ let(:notify_only_broken_builds) { false }
+
+ before do
+ allow(slack).to receive_messages(
+ project: project,
+ project_id: project.id,
+ webhook: webhook_url,
+ notify_only_broken_builds: notify_only_broken_builds
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it "should call Slack API" do
+ slack.execute(build)
+ Ci::SlackNotifierWorker.drain
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+end
diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb
new file mode 100644
index 0000000000..261ea69f5b
--- /dev/null
+++ b/spec/models/ci/project_spec.rb
@@ -0,0 +1,199 @@
+# == Schema Information
+#
+# Table name: projects
+#
+# id :integer not null, primary key
+# name :string(255) not null
+# timeout :integer default(3600), not null
+# created_at :datetime
+# updated_at :datetime
+# token :string(255)
+# default_ref :string(255)
+# path :string(255)
+# always_build :boolean default(FALSE), not null
+# polling_interval :integer
+# public :boolean default(FALSE), not null
+# ssh_url_to_repo :string(255)
+# gitlab_id :integer
+# allow_git_fetch :boolean default(TRUE), not null
+# email_recipients :string(255) default(""), not null
+# email_add_pusher :boolean default(TRUE), not null
+# email_only_broken_builds :boolean default(TRUE), not null
+# skip_refs :string(255)
+# coverage_regex :string(255)
+# shared_runners_enabled :boolean default(FALSE)
+# generated_yaml_config :text
+#
+
+require 'spec_helper'
+
+describe Ci::Project do
+ subject { FactoryGirl.build :ci_project }
+
+ it { is_expected.to have_many(:commits) }
+
+ it { is_expected.to validate_presence_of :name }
+ it { is_expected.to validate_presence_of :timeout }
+ it { is_expected.to validate_presence_of :default_ref }
+
+ describe 'before_validation' do
+ it 'should set an random token if none provided' do
+ project = FactoryGirl.create :ci_project_without_token
+ expect(project.token).not_to eq("")
+ end
+
+ it 'should not set an random toke if one provided' do
+ project = FactoryGirl.create :ci_project
+ expect(project.token).to eq("iPWx6WM4lhHNedGfBpPJNP")
+ end
+ end
+
+ describe "ordered_by_last_commit_date" do
+ it "returns ordered projects" do
+ newest_project = FactoryGirl.create :ci_project
+ oldest_project = FactoryGirl.create :ci_project
+ project_without_commits = FactoryGirl.create :ci_project
+
+ FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: newest_project
+ FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: oldest_project
+
+ expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_project, oldest_project, project_without_commits])
+ end
+ end
+
+ describe 'ordered commits' do
+ let(:project) { FactoryGirl.create :ci_project }
+
+ it 'returns ordered list of commits' do
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project
+ expect(project.commits).to eq([commit2, commit1])
+ end
+
+ it 'returns commits ordered by committed_at and id, with nulls last' do
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
+ commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project
+ commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
+ expect(project.commits).to eq([commit2, commit4, commit3, commit1])
+ end
+ end
+
+ context :valid_project do
+ let(:project) { FactoryGirl.create :ci_project }
+
+ context :project_with_commit_and_builds do
+ before do
+ commit = FactoryGirl.create(:ci_commit, project: project)
+ FactoryGirl.create(:ci_build, commit: commit)
+ end
+
+ it { expect(project.status).to eq('pending') }
+ it { expect(project.last_commit).to be_kind_of(Ci::Commit) }
+ it { expect(project.human_status).to eq('pending') }
+ end
+ end
+
+ describe '#email_notification?' do
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ expect(project.email_notification?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: 'test tesft'
+ expect(project.email_notification?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: ''
+ expect(project.email_notification?).to eq(false)
+ end
+ end
+
+ describe '#broken_or_success?' do
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(true)
+ allow(project).to receive(:success?).and_return(true)
+ expect(project.broken_or_success?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(true)
+ allow(project).to receive(:success?).and_return(false)
+ expect(project.broken_or_success?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(false)
+ allow(project).to receive(:success?).and_return(true)
+ expect(project.broken_or_success?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(false)
+ allow(project).to receive(:success?).and_return(false)
+ expect(project.broken_or_success?).to eq(false)
+ end
+ end
+
+ describe 'Project.parse' do
+ let(:project) { FactoryGirl.create :project }
+
+ subject { Ci::Project.parse(project) }
+
+ it { is_expected.to be_valid }
+ it { is_expected.to be_kind_of(Ci::Project) }
+ it { expect(subject.name).to eq(project.name_with_namespace) }
+ it { expect(subject.gitlab_id).to eq(project.id) }
+ it { expect(subject.gitlab_url).to eq(project.web_url) }
+ end
+
+ describe :repo_url_with_auth do
+ let(:project) { FactoryGirl.create :ci_project }
+ subject { project.repo_url_with_auth }
+
+ it { is_expected.to be_a(String) }
+ it { is_expected.to end_with(".git") }
+ it { is_expected.to start_with(project.gitlab_url[0..6]) }
+ it { is_expected.to include(project.token) }
+ it { is_expected.to include('gitlab-ci-token') }
+ it { is_expected.to include(project.gitlab_url[7..-1]) }
+ end
+
+ describe :search do
+ let!(:project) { FactoryGirl.create(:ci_project, name: "foo") }
+
+ it { expect(Ci::Project.search('fo')).to include(project) }
+ it { expect(Ci::Project.search('bar')).to be_empty }
+ end
+
+ describe :any_runners do
+ it "there are no runners available" do
+ project = FactoryGirl.create(:ci_project)
+ expect(project.any_runners?).to be_falsey
+ end
+
+ it "there is a specific runner" do
+ project = FactoryGirl.create(:ci_project)
+ project.runners << FactoryGirl.create(:ci_specific_runner)
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it "there is a shared runner" do
+ project = FactoryGirl.create(:ci_project, shared_runners_enabled: true)
+ FactoryGirl.create(:ci_shared_runner)
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it "there is a shared runner, but they are prohibited to use" do
+ project = FactoryGirl.create(:ci_project)
+ FactoryGirl.create(:ci_shared_runner)
+ expect(project.any_runners?).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
new file mode 100644
index 0000000000..0218d48413
--- /dev/null
+++ b/spec/models/ci/runner_project_spec.rb
@@ -0,0 +1,16 @@
+# == Schema Information
+#
+# Table name: runner_projects
+#
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'spec_helper'
+
+describe Ci::RunnerProject do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
new file mode 100644
index 0000000000..757593a7ab
--- /dev/null
+++ b/spec/models/ci/runner_spec.rb
@@ -0,0 +1,70 @@
+# == Schema Information
+#
+# Table name: runners
+#
+# id :integer not null, primary key
+# token :string(255)
+# created_at :datetime
+# updated_at :datetime
+# description :string(255)
+# contacted_at :datetime
+# active :boolean default(TRUE), not null
+# is_shared :boolean default(FALSE)
+# name :string(255)
+# version :string(255)
+# revision :string(255)
+# platform :string(255)
+# architecture :string(255)
+#
+
+require 'spec_helper'
+
+describe Ci::Runner do
+ describe '#display_name' do
+ it 'should return the description if it has a value' do
+ runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
+ expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
+ end
+
+ it 'should return the token if it does not have a description' do
+ runner = FactoryGirl.create(:ci_runner)
+ expect(runner.display_name).to eq runner.description
+ end
+
+ it 'should return the token if the description is an empty string' do
+ runner = FactoryGirl.build(:ci_runner, description: '')
+ expect(runner.display_name).to eq runner.token
+ end
+ end
+
+ describe :assign_to do
+ let!(:project) { FactoryGirl.create :ci_project }
+ let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) }
+
+ before { shared_runner.assign_to(project) }
+
+ it { expect(shared_runner).to be_specific }
+ it { expect(shared_runner.projects).to eq([project]) }
+ it { expect(shared_runner.only_for?(project)).to be_truthy }
+ end
+
+ describe "belongs_to_one_project?" do
+ it "returns false if there are two projects runner assigned to" do
+ runner = FactoryGirl.create(:ci_specific_runner)
+ project = FactoryGirl.create(:ci_project)
+ project1 = FactoryGirl.create(:ci_project)
+ project.runners << runner
+ project1.runners << runner
+
+ expect(runner.belongs_to_one_project?).to be_falsey
+ end
+
+ it "returns true" do
+ runner = FactoryGirl.create(:ci_specific_runner)
+ project = FactoryGirl.create(:ci_project)
+ project.runners << runner
+
+ expect(runner.belongs_to_one_project?).to be_truthy
+ end
+ end
+end
diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb
new file mode 100644
index 0000000000..2c575056b0
--- /dev/null
+++ b/spec/models/ci/service_spec.rb
@@ -0,0 +1,49 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+require 'spec_helper'
+
+describe Ci::Service do
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Mass assignment" do
+ end
+
+ describe "Test Button" do
+ before do
+ @service = Ci::Service.new
+ end
+
+ describe "Testable" do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+
+ before do
+ allow(@service).to receive_messages(
+ project: project
+ )
+ build
+ @testable = @service.can_test?
+ end
+
+ describe :can_test do
+ it { expect(@testable).to eq(true) }
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
new file mode 100644
index 0000000000..19c14ef2da
--- /dev/null
+++ b/spec/models/ci/trigger_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Ci::Trigger do
+ let(:project) { FactoryGirl.create :ci_project }
+
+ describe 'before_validation' do
+ it 'should set an random token if none provided' do
+ trigger = FactoryGirl.create :ci_trigger_without_token, project: project
+ expect(trigger.token).not_to be_nil
+ end
+
+ it 'should not set an random token if one provided' do
+ trigger = FactoryGirl.create :ci_trigger, project: project
+ expect(trigger.token).to eq('token')
+ end
+ end
+end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
new file mode 100644
index 0000000000..97a3d0081f
--- /dev/null
+++ b/spec/models/ci/variable_spec.rb
@@ -0,0 +1,44 @@
+# == Schema Information
+#
+# Table name: variables
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# key :string(255)
+# value :text
+# encrypted_value :text
+# encrypted_value_salt :string(255)
+# encrypted_value_iv :string(255)
+#
+
+require 'spec_helper'
+
+describe Ci::Variable do
+ subject { Ci::Variable.new }
+
+ let(:secret_value) { 'secret' }
+
+ before :each do
+ subject.value = secret_value
+ end
+
+ describe :value do
+ it 'stores the encrypted value' do
+ expect(subject.encrypted_value).not_to be_nil
+ end
+
+ it 'stores an iv for value' do
+ expect(subject.encrypted_value_iv).not_to be_nil
+ end
+
+ it 'stores a salt for value' do
+ expect(subject.encrypted_value_salt).not_to be_nil
+ end
+
+ it 'fails to decrypt if iv is incorrect' do
+ subject.encrypted_value_iv = nil
+ subject.instance_variable_set(:@value, nil)
+ expect { subject.value }.to raise_error
+ end
+ end
+end
diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb
new file mode 100644
index 0000000000..c4c0b007c1
--- /dev/null
+++ b/spec/models/ci/web_hook_spec.rb
@@ -0,0 +1,62 @@
+# == Schema Information
+#
+# Table name: web_hooks
+#
+# id :integer not null, primary key
+# url :string(255) not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'spec_helper'
+
+describe Ci::WebHook do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Validations" do
+ it { is_expected.to validate_presence_of(:url) }
+
+ context "url format" do
+ it { is_expected.to allow_value("http://example.com").for(:url) }
+ it { is_expected.to allow_value("https://excample.com").for(:url) }
+ it { is_expected.to allow_value("http://test.com/api").for(:url) }
+ it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
+ it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
+
+ it { is_expected.not_to allow_value("example.com").for(:url) }
+ it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
+ it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
+ end
+ end
+
+ describe "execute" do
+ before(:each) do
+ @web_hook = FactoryGirl.create(:ci_web_hook)
+ @project = @web_hook.project
+ @data = { before: 'oldrev', after: 'newrev', ref: 'ref' }
+
+ WebMock.stub_request(:post, @web_hook.url)
+ end
+
+ it "POSTs to the web hook URL" do
+ @web_hook.execute(@data)
+ expect(WebMock).to have_requested(:post, @web_hook.url).once
+ end
+
+ it "POSTs the data as JSON" do
+ json = @data.to_json
+
+ @web_hook.execute(@data)
+ expect(WebMock).to have_requested(:post, @web_hook.url).with(body: json).once
+ end
+
+ it "catches exceptions" do
+ expect(Ci::WebHook).to receive(:post).and_raise("Some HTTP Post error")
+
+ expect{ @web_hook.execute(@data) }.to raise_error
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 942768fa25..35b3d3e296 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -2,11 +2,12 @@ require "spec_helper"
describe API::API, api: true do
include ApiHelpers
+ let(:base_time) { Time.now }
let(:user) { create(:user) }
let!(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test") }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.seconds) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
@@ -74,8 +75,8 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
- expect(json_response.first['id']).to eq(@mr_later.id)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
end
it "should return an array of merge_requests in descending order" do
@@ -83,8 +84,8 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.first['id']).to eq(@mr_later.id)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
end
it "should return an array of merge_requests ordered by updated_at" do
@@ -92,17 +93,17 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
- expect(json_response.first['id']).to eq(@mr_later.id)
+ response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
end
it "should return an array of merge_requests ordered by created_at" do
- get api("/projects/#{project.id}/merge_requests?sort=created_at", user)
+ get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
- expect(json_response.first['id']).to eq(@mr_later.id)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
new file mode 100644
index 0000000000..c25d182330
--- /dev/null
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+
+ let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
+ let(:project) { FactoryGirl.create(:ci_project) }
+
+ describe "Builds API for runners" do
+ let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
+ let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") }
+
+ before do
+ FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id
+ end
+
+ describe "POST /builds/register" do
+ it "should start a build" do
+ commit = FactoryGirl.create(:ci_commit, project: project)
+ commit.create_builds
+ build = commit.builds.first
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response['sha']).to eq(build.sha)
+ expect(runner.reload.platform).to eq("darwin")
+ end
+
+ it "should return 404 error if no pending build found" do
+ post ci_api("/builds/register"), token: runner.token
+
+ expect(response.status).to eq(404)
+ end
+
+ it "should return 404 error if no builds for specific runner" do
+ commit = FactoryGirl.create(:ci_commit, project: shared_project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+
+ post ci_api("/builds/register"), token: runner.token
+
+ expect(response.status).to eq(404)
+ end
+
+ it "should return 404 error if no builds for shared runner" do
+ commit = FactoryGirl.create(:ci_commit, project: project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+
+ post ci_api("/builds/register"), token: shared_runner.token
+
+ expect(response.status).to eq(404)
+ end
+
+ it "returns options" do
+ commit = FactoryGirl.create(:ci_commit, project: project)
+ commit.create_builds
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ end
+
+ it "returns variables" do
+ commit = FactoryGirl.create(:ci_commit, project: project)
+ commit.create_builds
+ project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response["variables"]).to eq([
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ ])
+ end
+
+ it "returns variables for triggers" do
+ trigger = FactoryGirl.create(:ci_trigger, project: project)
+ commit = FactoryGirl.create(:ci_commit, project: project)
+
+ trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
+ commit.create_builds(trigger_request)
+ project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response["variables"]).to eq([
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false },
+ ])
+ end
+ end
+
+ describe "PUT /builds/:id" do
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project)}
+ let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
+
+ it "should update a running build" do
+ build.run!
+ put ci_api("/builds/#{build.id}"), token: runner.token
+ expect(response.status).to eq(200)
+ end
+
+ it 'Should not override trace information when no trace is given' do
+ build.run!
+ build.update!(trace: 'hello_world')
+ put ci_api("/builds/#{build.id}"), token: runner.token
+ expect(build.reload.trace).to eq 'hello_world'
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb
new file mode 100644
index 0000000000..e89b665149
--- /dev/null
+++ b/spec/requests/ci/api/commits_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Ci::API::API, 'Commits' do
+ include ApiHelpers
+
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+
+ let(:options) do
+ {
+ project_token: project.token,
+ project_id: project.id
+ }
+ end
+
+ describe "GET /commits" do
+ before { commit }
+
+ it "should return commits per project" do
+ get ci_api("/commits"), options
+
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first["project_id"]).to eq(project.id)
+ expect(json_response.first["sha"]).to eq(commit.sha)
+ end
+ end
+
+ describe "POST /commits" do
+ let(:data) do
+ {
+ "before" => "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ "after" => "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "ref" => "refs/heads/master",
+ "commits" => [
+ {
+ "id" => "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "message" => "Update Catalan translation to e38cb41.",
+ "timestamp" => "2011-12-12T14:27:31+02:00",
+ "url" => "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "author" => {
+ "name" => "Jordi Mallach",
+ "email" => "jordi@softcatala.org",
+ }
+ }
+ ],
+ ci_yaml_file: gitlab_ci_yaml
+ }
+ end
+
+ it "should create a build" do
+ post ci_api("/commits"), options.merge(data: data)
+
+ expect(response.status).to eq(201)
+ expect(json_response['sha']).to eq("da1560886d4f094c3e6c9ef40349f7d38b5d27d7")
+ end
+
+ it "should return 400 error if no data passed" do
+ post ci_api("/commits"), options
+
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq("400 (Bad request) \"data\" not given")
+ end
+ end
+end
diff --git a/spec/requests/ci/api/forks_spec.rb b/spec/requests/ci/api/forks_spec.rb
new file mode 100644
index 0000000000..37fa1e82f2
--- /dev/null
+++ b/spec/requests/ci/api/forks_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:private_token) { create(:user).private_token }
+
+ let(:options) do
+ {
+ private_token: private_token,
+ url: GitlabCi.config.gitlab_ci.url
+ }
+ end
+
+ before do
+ stub_gitlab_calls
+ end
+
+
+ describe "POST /forks" do
+ let(:project_info) do
+ {
+ project_id: project.gitlab_id,
+ project_token: project.token,
+ data: {
+ id: create(:empty_project).id,
+ name_with_namespace: "Gitlab.org / Underscore",
+ path_with_namespace: "gitlab-org/underscore",
+ default_branch: "master",
+ ssh_url_to_repo: "git@example.com:gitlab-org/underscore"
+ }
+ }
+ end
+
+ context "with valid info" do
+ before do
+ options.merge!(project_info)
+ end
+
+ it "should create a project with valid data" do
+ post ci_api("/forks"), options
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq("Gitlab.org / Underscore")
+ end
+ end
+
+ context "with invalid project info" do
+ before do
+ options.merge!({})
+ end
+
+ it "should error with invalid data" do
+ post ci_api("/forks"), options
+ expect(response.status).to eq(400)
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb
new file mode 100644
index 0000000000..2adae52e79
--- /dev/null
+++ b/spec/requests/ci/api/projects_spec.rb
@@ -0,0 +1,267 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+
+ let(:gitlab_url) { GitlabCi.config.gitlab_ci.url }
+ let(:user) { create(:user) }
+ let(:private_token) { user.private_token }
+
+ let(:options) do
+ {
+ private_token: private_token,
+ url: gitlab_url
+ }
+ end
+
+ before do
+ stub_gitlab_calls
+ end
+
+ context "requests for scoped projects" do
+ # NOTE: These ids are tied to the actual projects on demo.gitlab.com
+ describe "GET /projects" do
+ let!(:project1) { FactoryGirl.create(:ci_project) }
+ let!(:project2) { FactoryGirl.create(:ci_project) }
+
+ before do
+ project1.gl_project.team << [user, :developer]
+ project2.gl_project.team << [user, :developer]
+ end
+
+ it "should return all projects on the CI instance" do
+ get ci_api("/projects"), options
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(2)
+ expect(json_response.first["id"]).to eq(project1.id)
+ expect(json_response.last["id"]).to eq(project2.id)
+ end
+ end
+
+ describe "GET /projects/owned" do
+ let!(:gl_project1) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
+ let!(:gl_project2) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
+ let!(:project1) { FactoryGirl.create(:ci_project, gl_project: gl_project1) }
+ let!(:project2) { FactoryGirl.create(:ci_project, gl_project: gl_project2) }
+
+ before do
+ project1.gl_project.team << [user, :developer]
+ project2.gl_project.team << [user, :developer]
+ end
+
+ it "should return all projects on the CI instance" do
+ get ci_api("/projects/owned"), options
+
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(2)
+ end
+ end
+ end
+
+ describe "POST /projects/:project_id/webhooks" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+
+ context "Valid Webhook URL" do
+ let!(:webhook) { { web_hook: "http://example.com/sth/1/ala_ma_kota" } }
+
+ before do
+ options.merge!(webhook)
+ end
+
+ it "should create webhook for specified project" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(201)
+ expect(json_response["url"]).to eq(webhook[:web_hook])
+ end
+
+ it "fails to create webhook for non existsing project" do
+ post ci_api("/projects/non-existant-id/webhooks"), options
+ expect(response.status).to eq(404)
+ end
+
+ it "non-manager is not authorized" do
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "Invalid Webhook URL" do
+ let!(:webhook) { { web_hook: "ala_ma_kota" } }
+
+ before do
+ options.merge!(webhook)
+ end
+
+ it "fails to create webhook for not valid url" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context "Missed web_hook parameter" do
+ it "fails to create webhook for not provided url" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(400)
+ end
+ end
+ end
+
+ describe "GET /projects/:id" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+
+ before do
+ project.gl_project.team << [user, :developer]
+ end
+
+ context "with an existing project" do
+ it "should retrieve the project info" do
+ get ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(200)
+ expect(json_response['id']).to eq(project.id)
+ end
+ end
+
+ context "with a non-existing project" do
+ it "should return 404 error if project not found" do
+ get ci_api("/projects/non_existent_id"), options
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ describe "PUT /projects/:id" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+ let!(:project_info) { { name: "An updated name!" } }
+
+ before do
+ options.merge!(project_info)
+ end
+
+ it "should update a specific project's information" do
+ project.gl_project.team << [user, :master]
+ put ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(200)
+ expect(json_response["name"]).to eq(project_info[:name])
+ end
+
+ it "fails to update a non-existing project" do
+ put ci_api("/projects/non-existant-id"), options
+ expect(response.status).to eq(404)
+ end
+
+ it "non-manager is not authorized" do
+ put ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ describe "DELETE /projects/:id" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+
+ it "should delete a specific project" do
+ project.gl_project.team << [user, :master]
+ delete ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(200)
+ expect { project.reload }.to raise_error
+ end
+
+ it "non-manager is not authorized" do
+ delete ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(401)
+ end
+
+ it "is getting not found error" do
+ delete ci_api("/projects/not-existing_id"), options
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe "POST /projects" do
+ let(:project_info) do
+ {
+ name: "My project",
+ gitlab_id: 1,
+ path: "testing/testing",
+ ssh_url_to_repo: "ssh://example.com/testing/testing.git"
+ }
+ end
+
+ let(:invalid_project_info) { {} }
+
+ context "with valid project info" do
+ before do
+ options.merge!(project_info)
+ end
+
+ it "should create a project with valid data" do
+ post ci_api("/projects"), options
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq(project_info[:name])
+ end
+ end
+
+ context "with invalid project info" do
+ before do
+ options.merge!(invalid_project_info)
+ end
+
+ it "should error with invalid data" do
+ post ci_api("/projects"), options
+ expect(response.status).to eq(400)
+ end
+ end
+
+ describe "POST /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:runner) { FactoryGirl.create(:ci_runner) }
+
+ it "should add the project to the runner" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(201)
+
+ project.reload
+ expect(project.runners.first.id).to eq(runner.id)
+ end
+
+ it "should fail if it tries to link a non-existing project or runner" do
+ post ci_api("/projects/#{project.id}/runners/non-existing"), options
+ expect(response.status).to eq(404)
+
+ post ci_api("/projects/non-existing/runners/#{runner.id}"), options
+ expect(response.status).to eq(404)
+ end
+
+ it "non-manager is not authorized" do
+ allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false)
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ describe "DELETE /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:runner) { FactoryGirl.create(:ci_runner) }
+
+ it "should remove the project from the runner" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+
+ expect(project.runners).to be_present
+ delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(200)
+
+ project.reload
+ expect(project.runners).to be_empty
+ end
+
+ it "non-manager is not authorized" do
+ delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
new file mode 100644
index 0000000000..11dc089e1f
--- /dev/null
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+ include StubGitlabCalls
+
+ before do
+ stub_gitlab_calls
+ end
+
+ describe "GET /runners" do
+ let(:gitlab_url) { GitlabCi.config.gitlab_ci.url }
+ let(:private_token) { create(:user).private_token }
+ let(:options) do
+ {
+ private_token: private_token,
+ url: gitlab_url
+ }
+ end
+
+ before do
+ 5.times { FactoryGirl.create(:ci_runner) }
+ end
+
+ it "should retrieve a list of all runners" do
+ get ci_api("/runners", nil), options
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(5)
+ expect(json_response.last).to have_key("id")
+ expect(json_response.last).to have_key("token")
+ end
+ end
+
+ describe "POST /runners/register" do
+ describe "should create a runner if token provided" do
+ before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN }
+
+ it { expect(response.status).to eq(201) }
+ end
+
+ describe "should create a runner with description" do
+ before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, description: "server.hostname" }
+
+ it { expect(response.status).to eq(201) }
+ it { expect(Ci::Runner.first.description).to eq("server.hostname") }
+ end
+
+ describe "should create a runner with tags" do
+ before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, tag_list: "tag1, tag2" }
+
+ it { expect(response.status).to eq(201) }
+ it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) }
+ end
+
+ describe "should create a runner if project token provided" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ before { post ci_api("/runners/register"), token: project.token }
+
+ it { expect(response.status).to eq(201) }
+ it { expect(project.runners.size).to eq(1) }
+ end
+
+ it "should return 403 error if token is invalid" do
+ post ci_api("/runners/register"), token: 'invalid'
+
+ expect(response.status).to eq(403)
+ end
+
+ it "should return 400 error if no token" do
+ post ci_api("/runners/register")
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ describe "DELETE /runners/delete" do
+ let!(:runner) { FactoryGirl.create(:ci_runner) }
+ before { delete ci_api("/runners/delete"), token: runner.token }
+
+ it { expect(response.status).to eq(200) }
+ it { expect(Ci::Runner.count).to eq(0) }
+ end
+end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
new file mode 100644
index 0000000000..ff6fdbdd6f
--- /dev/null
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+
+ describe 'POST /projects/:project_id/refs/:ref/trigger' do
+ let!(:trigger_token) { 'secure token' }
+ let!(:project) { FactoryGirl.create(:ci_project) }
+ let!(:project2) { FactoryGirl.create(:ci_project) }
+ let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
+ let(:options) do
+ {
+ token: trigger_token
+ }
+ end
+
+ context 'Handles errors' do
+ it 'should return bad request if token is missing' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger")
+ expect(response.status).to eq(400)
+ end
+
+ it 'should return not found if project is not found' do
+ post ci_api('/projects/0/refs/master/trigger'), options
+ expect(response.status).to eq(404)
+ end
+
+ it 'should return unauthorized if token is for different project' do
+ post ci_api("/projects/#{project2.id}/refs/master/trigger"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'Have a commit' do
+ before do
+ @commit = FactoryGirl.create(:ci_commit, project: project)
+ end
+
+ it 'should create builds' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options
+ expect(response.status).to eq(201)
+ @commit.builds.reload
+ expect(@commit.builds.size).to eq(2)
+ end
+
+ it 'should return bad request with no builds created if there\'s no commit for that ref' do
+ post ci_api("/projects/#{project.id}/refs/other-branch/trigger"), options
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('No builds created')
+ end
+
+ context 'Validates variables' do
+ let(:variables) do
+ { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
+ end
+
+ it 'should validate variables to be a hash' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: 'value')
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('variables needs to be a hash')
+ end
+
+ it 'should validate variables needs to be a map of key-valued strings' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
+ end
+
+ it 'create trigger request with variables' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: variables)
+ expect(response.status).to eq(201)
+ @commit.builds.reload
+ expect(@commit.builds.first.trigger_request.variables).to eq(variables)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb
new file mode 100644
index 0000000000..998c386ead
--- /dev/null
+++ b/spec/requests/ci/builds_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe "Builds" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @commit = FactoryGirl.create :ci_commit, project: @project
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ end
+
+ describe "GET /:project/builds/:id/status.json" do
+ before do
+ get status_ci_project_build_path(@project, @build), format: :json
+ end
+
+ it { expect(response.status).to eq(200) }
+ it { expect(response.body).to include(@build.sha) }
+ end
+end
diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb
new file mode 100644
index 0000000000..fb31767033
--- /dev/null
+++ b/spec/requests/ci/commits_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe "Commits" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @commit = FactoryGirl.create :ci_commit, project: @project
+ end
+
+ describe "GET /:project/refs/:ref_name/commits/:id/status.json" do
+ before do
+ get status_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), format: :json
+ end
+
+ it { expect(response.status).to eq(200) }
+ it { expect(response.body).to include(@commit.sha) }
+ end
+end
diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb
new file mode 100644
index 0000000000..38d9943765
--- /dev/null
+++ b/spec/services/ci/create_commit_service_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+module Ci
+ describe CreateCommitService do
+ let(:service) { CreateCommitService.new }
+ let(:project) { FactoryGirl.create(:ci_project) }
+
+ describe :execute do
+ context 'valid params' do
+ let(:commit) do
+ service.execute(project,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ ci_yaml_file: gitlab_ci_yaml,
+ commits: [ { message: "Message" } ]
+ )
+ end
+
+ it { expect(commit).to be_kind_of(Commit) }
+ it { expect(commit).to be_valid }
+ it { expect(commit).to be_persisted }
+ it { expect(commit).to eq(project.commits.last) }
+ it { expect(commit.builds.first).to be_kind_of(Build) }
+ end
+
+ context "skip tag if there is no build for it" do
+ it "creates commit if there is appropriate job" do
+ result = service.execute(project,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ ci_yaml_file: gitlab_ci_yaml,
+ commits: [ { message: "Message" } ]
+ )
+ expect(result).to be_persisted
+ end
+
+ it "creates commit if there is no appropriate job but deploy job has right ref setting" do
+ config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
+
+ result = service.execute(project,
+ ref: 'refs/heads/0_1',
+ before: '00000000',
+ after: '31das312',
+ ci_yaml_file: config,
+ commits: [ { message: "Message" } ]
+ )
+ expect(result).to be_persisted
+ end
+ end
+
+ describe :ci_skip? do
+ it "skips builds creation if there is [ci skip] tag in commit message" do
+ commits = [{ message: "some message[ci skip]" }]
+ commit = service.execute(project,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits,
+ ci_yaml_file: gitlab_ci_yaml
+ )
+ expect(commit.builds.any?).to be false
+ expect(commit.status).to eq("skipped")
+ end
+
+ it "does not skips builds creation if there is no [ci skip] tag in commit message" do
+ commits = [{ message: "some message" }]
+
+ commit = service.execute(project,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits,
+ ci_yaml_file: gitlab_ci_yaml
+ )
+
+ expect(commit.builds.first.name).to eq("staging")
+ end
+
+ it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
+ commits = [{ message: "some message[ci skip]" }]
+ commit = service.execute(project,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits,
+ ci_yaml_file: "invalid: file"
+ )
+ expect(commit.builds.any?).to be false
+ expect(commit.status).to eq("skipped")
+ end
+ end
+
+ it "skips build creation if there are already builds" do
+ commits = [{ message: "message" }]
+ commit = service.execute(project,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits,
+ ci_yaml_file: gitlab_ci_yaml
+ )
+ expect(commit.builds.count(:all)).to eq(2)
+
+ commit = service.execute(project,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits,
+ ci_yaml_file: gitlab_ci_yaml
+ )
+ expect(commit.builds.count(:all)).to eq(2)
+ end
+
+ it "creates commit with failed status if yaml is invalid" do
+ commits = [{ message: "some message" }]
+
+ commit = service.execute(project,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits,
+ ci_yaml_file: "invalid: file"
+ )
+
+ expect(commit.status).to eq("failed")
+ expect(commit.builds.any?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_project_service_spec.rb b/spec/services/ci/create_project_service_spec.rb
new file mode 100644
index 0000000000..64041b8d5a
--- /dev/null
+++ b/spec/services/ci/create_project_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Ci::CreateProjectService do
+ let(:service) { Ci::CreateProjectService.new }
+ let(:current_user) { double.as_null_object }
+ let(:project) { FactoryGirl.create :project }
+
+ describe :execute do
+ context 'valid params' do
+ subject { service.execute(current_user, project, 'http://localhost/projects/:project_id') }
+
+ it { is_expected.to be_kind_of(Ci::Project) }
+ it { is_expected.to be_persisted }
+ end
+
+ context 'without project dump' do
+ it 'should raise exception' do
+ expect { service.execute(current_user, '', '') }.to raise_error
+ end
+ end
+
+ context "forking" do
+ let(:ci_origin_project) do
+ FactoryGirl.create(:ci_project, shared_runners_enabled: true, public: true, allow_git_fetch: true)
+ end
+
+ subject { service.execute(current_user, project, 'http://localhost/projects/:project_id', ci_origin_project) }
+
+ it "uses project as a template for settings and jobs" do
+ expect(subject.shared_runners_enabled).to be_truthy
+ expect(subject.public).to be_truthy
+ expect(subject.allow_git_fetch).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
new file mode 100644
index 0000000000..d12cd9773d
--- /dev/null
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Ci::CreateTriggerRequestService do
+ let(:service) { Ci::CreateTriggerRequestService.new }
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
+
+ describe :execute do
+ context 'valid params' do
+ subject { service.execute(project, trigger, 'master') }
+
+ before do
+ @commit = FactoryGirl.create :ci_commit, project: project
+ end
+
+ it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.commit).to eq(@commit) }
+ end
+
+ context 'no commit for ref' do
+ subject { service.execute(project, trigger, 'other-branch') }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'no builds created' do
+ subject { service.execute(project, trigger, 'master') }
+
+ before do
+ FactoryGirl.create :ci_commit_without_jobs, project: project
+ end
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'for multiple commits' do
+ subject { service.execute(project, trigger, 'master') }
+
+ before do
+ @commit1 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, project: project
+ @commit2 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
+ @commit3 = FactoryGirl.create :ci_commit, committed_at: 3.hour.ago, project: project
+ end
+
+ context 'retries latest one' do
+ it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject).to be_persisted }
+ it { expect(subject.commit).to eq(@commit2) }
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb
new file mode 100644
index 0000000000..9b330a90ae
--- /dev/null
+++ b/spec/services/ci/event_service_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Ci::EventService do
+ let(:project) { FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell" }
+ let(:user) { double(username: "root", id: 1) }
+
+ before do
+ Event.destroy_all
+ end
+
+ describe :remove_project do
+ it "creates event" do
+ Ci::EventService.new.remove_project(user, project)
+
+ expect(Ci::Event.admin.last.description).to eq("Project \"GitLab / gitlab-shell\" has been removed by root")
+ end
+ end
+
+ describe :create_project do
+ it "creates event" do
+ Ci::EventService.new.create_project(user, project)
+
+ expect(Ci::Event.admin.last.description).to eq("Project \"GitLab / gitlab-shell\" has been created by root")
+ end
+ end
+
+ describe :change_project_settings do
+ it "creates event" do
+ Ci::EventService.new.change_project_settings(user, project)
+
+ expect(Ci::Event.last.description).to eq("User \"root\" updated projects settings")
+ end
+ end
+end
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
new file mode 100644
index 0000000000..7565eb8f03
--- /dev/null
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+module Ci
+ describe ImageForBuildService do
+ let(:service) { ImageForBuildService.new }
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project, ref: 'master') }
+ let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
+
+ describe :execute do
+ before { build }
+
+ context 'branch name' do
+ before { build.run! }
+ let(:image) { service.execute(project, ref: 'master') }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
+ it { expect(image.name).to eq('build-running.svg') }
+ end
+
+ context 'unknown branch name' do
+ let(:image) { service.execute(project, ref: 'feature') }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
+ it { expect(image.name).to eq('build-unknown.svg') }
+ end
+
+ context 'commit sha' do
+ before { build.run! }
+ let(:image) { service.execute(project, sha: build.sha) }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
+ it { expect(image.name).to eq('build-running.svg') }
+ end
+
+ context 'unknown commit sha' do
+ let(:image) { service.execute(project, sha: '0000000') }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
+ it { expect(image.name).to eq('build-unknown.svg') }
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
new file mode 100644
index 0000000000..7b5af6c3dd
--- /dev/null
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -0,0 +1,91 @@
+require 'spec_helper'
+
+module Ci
+ describe RegisterBuildService do
+ let!(:service) { RegisterBuildService.new }
+ let!(:project) { FactoryGirl.create :ci_project }
+ let!(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let!(:pending_build) { FactoryGirl.create :ci_build, project: project, commit: commit }
+ let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
+ let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
+
+ before do
+ specific_runner.assign_to(project)
+ end
+
+ describe :execute do
+ context 'runner follow tag list' do
+ it "picks build with the same tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ specific_runner.tag_list = ["linux"]
+ expect(service.execute(specific_runner)).to eq(pending_build)
+ end
+
+ it "does not pick build with different tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ specific_runner.tag_list = ["win32"]
+ expect(service.execute(specific_runner)).to be_falsey
+ end
+
+ it "picks build without tag" do
+ expect(service.execute(specific_runner)).to eq(pending_build)
+ end
+
+ it "does not pick build with tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ expect(service.execute(specific_runner)).to be_falsey
+ end
+
+ it "pick build without tag" do
+ specific_runner.tag_list = ["win32"]
+ expect(service.execute(specific_runner)).to eq(pending_build)
+ end
+ end
+
+ context 'allow shared runners' do
+ before do
+ project.shared_runners_enabled = true
+ project.save
+ end
+
+ context 'shared runner' do
+ let(:build) { service.execute(shared_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(shared_runner) }
+ end
+
+ context 'specific runner' do
+ let(:build) { service.execute(specific_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(specific_runner) }
+ end
+ end
+
+ context 'disallow shared runners' do
+ context 'shared runner' do
+ let(:build) { service.execute(shared_runner) }
+
+ it { expect(build).to be_nil }
+ end
+
+ context 'specific runner' do
+ let(:build) { service.execute(specific_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(specific_runner) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb
new file mode 100644
index 0000000000..cebdd145e4
--- /dev/null
+++ b/spec/services/ci/web_hook_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Ci::WebHookService do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+ let(:hook) { FactoryGirl.create :ci_web_hook, project: project }
+
+ describe :execute do
+ it "should execute successfully" do
+ stub_request(:post, hook.url).to_return(status: 200)
+ expect(Ci::WebHookService.new.build_end(build)).to be_truthy
+ end
+ end
+
+ context 'build_data' do
+ it "contains all needed fields" do
+ expect(build_data(build)).to include(
+ :build_id,
+ :project_id,
+ :ref,
+ :build_status,
+ :build_started_at,
+ :build_finished_at,
+ :before_sha,
+ :project_name,
+ :gitlab_url,
+ :build_name
+ )
+ end
+ end
+
+ def build_data(build)
+ Ci::WebHookService.new.send :build_data, build
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 0780c4f320..dfe855926c 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -30,6 +30,9 @@ RSpec.configure do |config|
config.include StubConfiguration
config.include RelativeUrl, type: feature
config.include TestEnv
+ config.include StubGitlabCalls
+ config.include StubGitlabData
+
config.infer_spec_type_from_file_location!
config.raise_errors_for_deprecations!
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index f63322776d..1b3cafb497 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -28,6 +28,17 @@ module ApiHelpers
"&private_token=#{user.private_token}" : "")
end
+ def ci_api(path, user = nil)
+ "/ci/api/v1/#{path}" +
+
+ # Normalize query string
+ (path.index('?') ? '' : '?') +
+
+ # Append private_token if given a User object
+ (user.respond_to?(:private_token) ?
+ "&private_token=#{user.private_token}" : "")
+ end
+
def json_response
@_json_response ||= JSON.parse(response.body)
end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index 755964e9a3..203117aee7 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -72,6 +72,6 @@ module FilterSpecHelper
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module
def urls
- Rails.application.routes.url_helpers
+ Gitlab::Application.routes.url_helpers
end
end
diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml
new file mode 100644
index 0000000000..3482145404
--- /dev/null
+++ b/spec/support/gitlab_stubs/gitlab_ci.yml
@@ -0,0 +1,63 @@
+image: ruby:2.1
+services:
+ - postgres
+
+before_script:
+ - gem install bundler
+ - bundle install
+ - bundle exec rake db:create
+
+variables:
+ DB_NAME: postgres
+
+types:
+ - test
+ - deploy
+ - notify
+
+rspec:
+ script: "rake spec"
+ tags:
+ - ruby
+ - postgres
+ only:
+ - branches
+
+spinach:
+ script: "rake spinach"
+ allow_failure: true
+ tags:
+ - ruby
+ - mysql
+ except:
+ - tags
+
+staging:
+ script: "cap deploy stating"
+ type: deploy
+ tags:
+ - capistrano
+ - debian
+ except:
+ - stable
+
+production:
+ type: deploy
+ script:
+ - cap deploy production
+ - cap notify
+ tags:
+ - capistrano
+ - debian
+ only:
+ - master
+ - /^deploy-.*$/
+
+dockerhub:
+ type: notify
+ script: "curl http://dockerhub/URL"
+ tags:
+ - ruby
+ - postgres
+ only:
+ - branches
diff --git a/spec/support/gitlab_stubs/project_8.json b/spec/support/gitlab_stubs/project_8.json
new file mode 100644
index 0000000000..f0a9fce859
--- /dev/null
+++ b/spec/support/gitlab_stubs/project_8.json
@@ -0,0 +1,45 @@
+{
+ "id":8,
+ "description":"ssh access and repository management app for GitLab",
+ "default_branch":"master",
+ "public":false,
+ "visibility_level":0,
+ "ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git",
+ "web_url":"http://demo.gitlab.com/gitlab/gitlab-shell",
+ "owner": {
+ "id":4,
+ "name":"GitLab",
+ "created_at":"2012-12-21T13:03:05Z"
+ },
+ "name":"gitlab-shell",
+ "name_with_namespace":"GitLab / gitlab-shell",
+ "path":"gitlab-shell",
+ "path_with_namespace":"gitlab/gitlab-shell",
+ "issues_enabled":true,
+ "merge_requests_enabled":true,
+ "wall_enabled":false,
+ "wiki_enabled":true,
+ "snippets_enabled":false,
+ "created_at":"2013-03-20T13:28:53Z",
+ "last_activity_at":"2013-11-30T00:11:17Z",
+ "namespace":{
+ "created_at":"2012-12-21T13:03:05Z",
+ "description":"Self hosted Git management software",
+ "id":4,
+ "name":"GitLab",
+ "owner_id":1,
+ "path":"gitlab",
+ "updated_at":"2013-03-20T13:29:13Z"
+ },
+ "permissions":{
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ }
+}
\ No newline at end of file
diff --git a/spec/support/gitlab_stubs/project_8_hooks.json b/spec/support/gitlab_stubs/project_8_hooks.json
new file mode 100644
index 0000000000..93d51406d6
--- /dev/null
+++ b/spec/support/gitlab_stubs/project_8_hooks.json
@@ -0,0 +1 @@
+[{}]
diff --git a/spec/support/gitlab_stubs/projects.json b/spec/support/gitlab_stubs/projects.json
new file mode 100644
index 0000000000..ca42c14c5d
--- /dev/null
+++ b/spec/support/gitlab_stubs/projects.json
@@ -0,0 +1 @@
+[{"id":3,"description":"GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlabhq.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlabhq.git","web_url":"http://demo.gitlab.com/gitlab/gitlabhq","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlabhq","name_with_namespace":"GitLab / gitlabhq","path":"gitlabhq","path_with_namespace":"gitlab/gitlabhq","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:34Z","last_activity_at":"2013-12-02T19:10:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":4,"description":"Component of GitLab CI. Web application","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-ci.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-ci.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-ci","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-ci","name_with_namespace":"GitLab / gitlab-ci","path":"gitlab-ci","path_with_namespace":"gitlab/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:50Z","last_activity_at":"2013-11-28T19:26:54Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":5,"description":"","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-recipes.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-recipes.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-recipes","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-recipes","name_with_namespace":"GitLab / gitlab-recipes","path":"gitlab-recipes","path_with_namespace":"gitlab/gitlab-recipes","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:07:02Z","last_activity_at":"2013-12-02T13:54:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":8,"description":"ssh access and repository management app for GitLab","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-shell","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-shell","name_with_namespace":"GitLab / gitlab-shell","path":"gitlab-shell","path_with_namespace":"gitlab/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-03-20T13:28:53Z","last_activity_at":"2013-11-30T00:11:17Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":9,"description":null,"default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab_git.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab_git.git","web_url":"http://demo.gitlab.com/gitlab/gitlab_git","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab_git","name_with_namespace":"GitLab / gitlab_git","path":"gitlab_git","path_with_namespace":"gitlab/gitlab_git","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-04-28T19:15:08Z","last_activity_at":"2013-12-02T13:07:13Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":10,"description":"ultra lite authorization library http://randx.github.com/six/\\r\\n ","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/six.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/six.git","web_url":"http://demo.gitlab.com/sandbox/six","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Six","name_with_namespace":"Sandbox / Six","path":"six","path_with_namespace":"sandbox/six","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:45:02Z","last_activity_at":"2013-11-29T11:30:56Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":11,"description":"Simple HTML5 Charts using the