summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@gitlab.com>2015-09-16 14:42:49 +0200
committerDouwe Maan <douwe@gitlab.com>2015-09-16 14:42:49 +0200
commit624a2cf27c719dbb384ec9251120f62c0a04fdc4 (patch)
treec67851357e8344a386f820a816b936ca96f2c65d
parent7bb22831a13b021c7b2d1cc8542808243a659d38 (diff)
parentcccd269da3f5d82c5d14289980d9b52c9cad08db (diff)
downloadgitlab-ce-624a2cf27c719dbb384ec9251120f62c0a04fdc4.tar.gz
Merge branch 'master' into update-mailroom
-rw-r--r--.gitignore4
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG1
-rw-r--r--CHANGELOG-CI298
-rw-r--r--Gemfile197
-rw-r--r--Gemfile.lock511
-rw-r--r--Procfile2
-rw-r--r--app/assets/images/ci/arch.jpgbin0 -> 25222 bytes
-rw-r--r--app/assets/images/ci/favicon.icobin0 -> 5430 bytes
-rw-r--r--app/assets/images/ci/loader.gifbin0 -> 4405 bytes
-rw-r--r--app/assets/images/ci/no_avatar.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/ci/rails.pngbin0 -> 6646 bytes
-rw-r--r--app/assets/images/ci/service_sample.pngbin0 -> 76024 bytes
-rw-r--r--app/assets/javascripts/ci/Chart.min.js39
-rw-r--r--app/assets/javascripts/ci/application.js.coffee40
-rw-r--r--app/assets/javascripts/ci/build.coffee41
-rw-r--r--app/assets/javascripts/ci/pager.js.coffee42
-rw-r--r--app/assets/javascripts/ci/projects.js.coffee6
-rw-r--r--app/assets/stylesheets/application.scss6
-rw-r--r--app/assets/stylesheets/ci/builds.scss70
-rw-r--r--app/assets/stylesheets/ci/lint.scss10
-rw-r--r--app/assets/stylesheets/ci/projects.scss56
-rw-r--r--app/assets/stylesheets/ci/runners.scss36
-rw-r--r--app/assets/stylesheets/ci/xterm.scss906
-rw-r--r--app/controllers/application_controller.rb3
-rw-r--r--app/controllers/ci/admin/application_controller.rb10
-rw-r--r--app/controllers/ci/admin/application_settings_controller.rb31
-rw-r--r--app/controllers/ci/admin/builds_controller.rb18
-rw-r--r--app/controllers/ci/admin/events_controller.rb9
-rw-r--r--app/controllers/ci/admin/projects_controller.rb19
-rw-r--r--app/controllers/ci/admin/runner_projects_controller.rb34
-rw-r--r--app/controllers/ci/admin/runners_controller.rb69
-rw-r--r--app/controllers/ci/application_controller.rb76
-rw-r--r--app/controllers/ci/builds_controller.rb78
-rw-r--r--app/controllers/ci/charts_controller.rb24
-rw-r--r--app/controllers/ci/commits_controller.rb38
-rw-r--r--app/controllers/ci/events_controller.rb21
-rw-r--r--app/controllers/ci/helps_controller.rb16
-rw-r--r--app/controllers/ci/lints_controller.rb26
-rw-r--r--app/controllers/ci/projects_controller.rb137
-rw-r--r--app/controllers/ci/runner_projects_controller.rb34
-rw-r--r--app/controllers/ci/runners_controller.rb73
-rw-r--r--app/controllers/ci/services_controller.rb59
-rw-r--r--app/controllers/ci/triggers_controller.rb43
-rw-r--r--app/controllers/ci/variables_controller.rb33
-rw-r--r--app/controllers/ci/web_hooks_controller.rb53
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/helpers/ci/application_helper.rb140
-rw-r--r--app/helpers/ci/builds_helper.rb41
-rw-r--r--app/helpers/ci/commits_helper.rb39
-rw-r--r--app/helpers/ci/gitlab_helper.rb36
-rw-r--r--app/helpers/ci/icons_helper.rb11
-rw-r--r--app/helpers/ci/projects_helper.rb36
-rw-r--r--app/helpers/ci/routes_helper.rb29
-rw-r--r--app/helpers/ci/runners_helper.rb22
-rw-r--r--app/helpers/ci/triggers_helper.rb7
-rw-r--r--app/helpers/ci/user_helper.rb15
-rw-r--r--app/mailers/ci/emails/builds.rb17
-rw-r--r--app/mailers/ci/notify.rb47
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/ability.rb1
-rw-r--r--app/models/ci/application_setting.rb27
-rw-r--r--app/models/ci/build.rb285
-rw-r--r--app/models/ci/commit.rb267
-rw-r--r--app/models/ci/event.rb27
-rw-r--r--app/models/ci/project.rb225
-rw-r--r--app/models/ci/project_status.rb47
-rw-r--r--app/models/ci/runner.rb80
-rw-r--r--app/models/ci/runner_project.rb21
-rw-r--r--app/models/ci/service.rb105
-rw-r--r--app/models/ci/trigger.rb39
-rw-r--r--app/models/ci/trigger_request.rb23
-rw-r--r--app/models/ci/variable.rb25
-rw-r--r--app/models/ci/web_hook.rb44
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_services/ci/hip_chat_message.rb78
-rw-r--r--app/models/project_services/ci/hip_chat_service.rb93
-rw-r--r--app/models/project_services/ci/mail_service.rb84
-rw-r--r--app/models/project_services/ci/slack_message.rb97
-rw-r--r--app/models/project_services/ci/slack_service.rb81
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/user.rb9
-rw-r--r--app/services/ci/create_commit_service.rb50
-rw-r--r--app/services/ci/create_project_service.rb35
-rw-r--r--app/services/ci/create_trigger_request_service.rb17
-rw-r--r--app/services/ci/event_service.rb31
-rw-r--r--app/services/ci/image_for_build_service.rb31
-rw-r--r--app/services/ci/register_build_service.rb40
-rw-r--r--app/services/ci/test_hook_service.rb7
-rw-r--r--app/services/ci/web_hook_service.rb36
-rw-r--r--app/views/ci/admin/application_settings/_form.html.haml24
-rw-r--r--app/views/ci/admin/application_settings/show.html.haml3
-rw-r--r--app/views/ci/admin/builds/_build.html.haml32
-rw-r--r--app/views/ci/admin/builds/index.html.haml28
-rw-r--r--app/views/ci/admin/events/index.html.haml17
-rw-r--r--app/views/ci/admin/projects/_project.html.haml28
-rw-r--r--app/views/ci/admin/projects/index.html.haml15
-rw-r--r--app/views/ci/admin/runner_projects/index.html.haml57
-rw-r--r--app/views/ci/admin/runners/_runner.html.haml48
-rw-r--r--app/views/ci/admin/runners/index.html.haml52
-rw-r--r--app/views/ci/admin/runners/show.html.haml118
-rw-r--r--app/views/ci/admin/runners/update.js.haml2
-rw-r--r--app/views/ci/builds/_build.html.haml45
-rw-r--r--app/views/ci/builds/show.html.haml167
-rw-r--r--app/views/ci/charts/_build_times.haml21
-rw-r--r--app/views/ci/charts/_builds.haml41
-rw-r--r--app/views/ci/charts/_overall.haml21
-rw-r--r--app/views/ci/charts/show.html.haml4
-rw-r--r--app/views/ci/commits/_commit.html.haml32
-rw-r--r--app/views/ci/commits/show.html.haml88
-rw-r--r--app/views/ci/errors/show.haml2
-rw-r--r--app/views/ci/events/index.html.haml19
-rw-r--r--app/views/ci/helps/oauth2.html.haml20
-rw-r--r--app/views/ci/helps/show.html.haml40
-rw-r--r--app/views/ci/lints/_create.html.haml39
-rw-r--r--app/views/ci/lints/create.js.haml2
-rw-r--r--app/views/ci/lints/show.html.haml25
-rw-r--r--app/views/ci/notify/build_fail_email.html.haml19
-rw-r--r--app/views/ci/notify/build_fail_email.text.erb9
-rw-r--r--app/views/ci/notify/build_success_email.html.haml20
-rw-r--r--app/views/ci/notify/build_success_email.text.erb9
-rw-r--r--app/views/ci/projects/_form.html.haml101
-rw-r--r--app/views/ci/projects/_gl_projects.html.haml15
-rw-r--r--app/views/ci/projects/_info.html.haml2
-rw-r--r--app/views/ci/projects/_no_runners.html.haml8
-rw-r--r--app/views/ci/projects/_project.html.haml22
-rw-r--r--app/views/ci/projects/_public.html.haml21
-rw-r--r--app/views/ci/projects/_search.html.haml17
-rw-r--r--app/views/ci/projects/edit.html.haml21
-rw-r--r--app/views/ci/projects/gitlab.html.haml27
-rw-r--r--app/views/ci/projects/index.html.haml13
-rw-r--r--app/views/ci/projects/show.html.haml60
-rw-r--r--app/views/ci/runners/_runner.html.haml35
-rw-r--r--app/views/ci/runners/_shared_runners.html.haml23
-rw-r--r--app/views/ci/runners/_specific_runners.html.haml29
-rw-r--r--app/views/ci/runners/edit.html.haml27
-rw-r--r--app/views/ci/runners/index.html.haml25
-rw-r--r--app/views/ci/runners/show.html.haml64
-rw-r--r--app/views/ci/services/_form.html.haml57
-rw-r--r--app/views/ci/services/edit.html.haml1
-rw-r--r--app/views/ci/services/index.html.haml22
-rw-r--r--app/views/ci/shared/_guide.html.haml15
-rw-r--r--app/views/ci/shared/_no_runners.html.haml7
-rw-r--r--app/views/ci/triggers/_trigger.html.haml14
-rw-r--r--app/views/ci/triggers/index.html.haml67
-rw-r--r--app/views/ci/user_sessions/new.html.haml8
-rw-r--r--app/views/ci/variables/show.html.haml39
-rw-r--r--app/views/ci/web_hooks/index.html.haml92
-rw-r--r--app/views/layouts/ci/_info.html.haml2
-rw-r--r--app/views/layouts/ci/_nav_admin.html.haml33
-rw-r--r--app/views/layouts/ci/_nav_build.html.haml3
-rw-r--r--app/views/layouts/ci/_nav_commit.haml3
-rw-r--r--app/views/layouts/ci/_nav_dashboard.html.haml24
-rw-r--r--app/views/layouts/ci/_nav_project.html.haml53
-rw-r--r--app/views/layouts/ci/_page.html.haml26
-rw-r--r--app/views/layouts/ci/admin.html.haml11
-rw-r--r--app/views/layouts/ci/application.html.haml11
-rw-r--r--app/views/layouts/ci/build.html.haml11
-rw-r--r--app/views/layouts/ci/commit.html.haml11
-rw-r--r--app/views/layouts/ci/notify.html.haml19
-rw-r--r--app/views/layouts/ci/project.html.haml11
-rw-r--r--app/workers/ci/hip_chat_notifier_worker.rb19
-rw-r--r--app/workers/ci/slack_notifier_worker.rb10
-rw-r--r--app/workers/ci/web_hook_worker.rb9
-rwxr-xr-xbin/background_jobs2
-rw-r--r--bin/ci/upgrade.rb3
-rw-r--r--builds/.gitkeep0
-rw-r--r--config/environments/development.rb5
-rw-r--r--config/gitlab.yml.example22
-rw-r--r--config/initializers/1_settings.rb24
-rw-r--r--config/initializers/3_grit_ext.rb5
-rw-r--r--config/initializers/4_ci_app.rb10
-rw-r--r--config/initializers/connection_fix.rb32
-rw-r--r--config/initializers/cookies_serializer.rb3
-rw-r--r--config/initializers/default_url_options.rb (renamed from config/initializers/8_default_url_options.rb)2
-rw-r--r--config/initializers/omniauth.rb (renamed from config/initializers/7_omniauth.rb)0
-rw-r--r--config/initializers/rack_attack.rb.example14
-rw-r--r--config/initializers/rack_profiler.rb (renamed from config/initializers/6_rack_profiler.rb)2
-rw-r--r--config/initializers/secret_token.rb24
-rw-r--r--config/initializers/session_store.rb2
-rw-r--r--config/initializers/sidekiq.rb (renamed from config/initializers/4_sidekiq.rb)0
-rw-r--r--config/initializers/static_files.rb2
-rw-r--r--config/locales/devise.en.yml7
-rw-r--r--config/routes.rb99
-rw-r--r--config/schedule.rb8
-rw-r--r--config/secrets.yml.example12
-rw-r--r--config/sidekiq.yml.example2
-rw-r--r--db/migrate/20150826001931_add_ci_tables.rb190
-rw-r--r--db/migrate/20150914215247_add_ci_tags.rb23
-rw-r--r--db/migrate/limits_to_mysql.rb4
-rw-r--r--db/schema.rb209
-rw-r--r--doc/README.md23
-rw-r--r--doc/ci/README.md23
-rw-r--r--doc/ci/api/README.md87
-rw-r--r--doc/ci/api/builds.md41
-rw-r--r--doc/ci/api/commits.md101
-rw-r--r--doc/ci/api/forks.md23
-rw-r--r--doc/ci/api/projects.md154
-rw-r--r--doc/ci/api/runners.md77
-rw-r--r--doc/ci/deployment/README.md98
-rw-r--r--doc/ci/docker/README.md4
-rw-r--r--doc/ci/docker/using_docker_build.md112
-rw-r--r--doc/ci/docker/using_docker_images.md203
-rw-r--r--doc/ci/examples/README.md5
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md72
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md67
-rw-r--r--doc/ci/examples/test-clojure-application.md35
-rw-r--r--doc/ci/permissions/README.md24
-rw-r--r--doc/ci/quick_start/README.md119
-rw-r--r--doc/ci/quick_start/build_status.pngbin0 -> 62140 bytes
-rw-r--r--doc/ci/quick_start/commit_status.pngbin0 -> 33492 bytes
-rw-r--r--doc/ci/quick_start/new_commit.pngbin0 -> 47527 bytes
-rw-r--r--doc/ci/quick_start/projects.pngbin0 -> 37014 bytes
-rw-r--r--doc/ci/quick_start/runners.pngbin0 -> 123048 bytes
-rw-r--r--doc/ci/quick_start/runners_activated.pngbin0 -> 60769 bytes
-rw-r--r--doc/ci/runners/README.md145
-rw-r--r--doc/ci/runners/project_specific.pngbin0 -> 31408 bytes
-rw-r--r--doc/ci/runners/shared_runner.pngbin0 -> 18366 bytes
-rw-r--r--doc/ci/runners/shared_to_specific_admin.pngbin0 -> 5897 bytes
-rw-r--r--doc/ci/variables/README.md95
-rw-r--r--doc/ci/yaml/README.md204
-rw-r--r--doc/install/installation.md23
-rw-r--r--doc/migrate_ci_to_ce/README.md261
-rw-r--r--doc/release/monthly.md2
-rw-r--r--doc/reply_by_email/README.md85
-rw-r--r--doc/reply_by_email/postfix.md310
-rw-r--r--doc/update/7.14-to-8.0.md26
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/helpers.rb43
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/projects.rb36
-rw-r--r--lib/api/services.rb4
-rw-r--r--lib/backup/builds.rb34
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/ci/ansi2html.rb224
-rw-r--r--lib/ci/api/api.rb39
-rw-r--r--lib/ci/api/builds.rb53
-rw-r--r--lib/ci/api/commits.rb66
-rw-r--r--lib/ci/api/entities.rb56
-rw-r--r--lib/ci/api/forks.rb37
-rw-r--r--lib/ci/api/helpers.rb33
-rw-r--r--lib/ci/api/projects.rb210
-rw-r--r--lib/ci/api/runners.rb69
-rw-r--r--lib/ci/api/triggers.rb49
-rw-r--r--lib/ci/assets/.gitkeep0
-rw-r--r--lib/ci/charts.rb71
-rw-r--r--lib/ci/current_settings.rb22
-rw-r--r--lib/ci/git.rb5
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb198
-rw-r--r--lib/ci/migrate/database.rb67
-rw-r--r--lib/ci/migrate/tags.rb49
-rw-r--r--lib/ci/model.rb11
-rw-r--r--lib/ci/scheduler.rb16
-rw-r--r--lib/ci/static_model.rb49
-rw-r--r--lib/ci/version_info.rb52
-rw-r--r--lib/gitlab/markdown/commit_range_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/commit_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/label_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/merge_request_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/snippet_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/user_reference_filter.rb2
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/support/nginx/gitlab_ci29
-rw-r--r--lib/tasks/ci/.gitkeep0
-rw-r--r--lib/tasks/ci/cleanup.rake8
-rw-r--r--lib/tasks/ci/migrate.rake63
-rw-r--r--lib/tasks/ci/schedule_builds.rake6
-rw-r--r--lib/tasks/gitlab/backup.rake21
-rw-r--r--public/ci/build-canceled.svg1
-rw-r--r--public/ci/build-failed.svg1
-rw-r--r--public/ci/build-pending.svg1
-rw-r--r--public/ci/build-running.svg1
-rw-r--r--public/ci/build-success.svg1
-rw-r--r--public/ci/build-unknown.svg1
-rw-r--r--public/ci/favicon.icobin0 -> 5430 bytes
-rwxr-xr-xscripts/ci/prepare_build.sh22
-rw-r--r--spec/controllers/ci/commits_controller_spec.rb27
-rw-r--r--spec/controllers/ci/projects_controller_spec.rb93
-rw-r--r--spec/factories/ci/builds.rb47
-rw-r--r--spec/factories/ci/commits.rb75
-rw-r--r--spec/factories/ci/events.rb24
-rw-r--r--spec/factories/ci/projects.rb56
-rw-r--r--spec/factories/ci/runner_projects.rb19
-rw-r--r--spec/factories/ci/runners.rb38
-rw-r--r--spec/factories/ci/trigger_requests.rb13
-rw-r--r--spec/factories/ci/triggers.rb9
-rw-r--r--spec/factories/ci/web_hook.rb6
-rw-r--r--spec/features/ci/admin/builds_spec.rb71
-rw-r--r--spec/features/ci/admin/events_spec.rb20
-rw-r--r--spec/features/ci/admin/projects_spec.rb19
-rw-r--r--spec/features/ci/admin/runners_spec.rb65
-rw-r--r--spec/features/ci/builds_spec.rb60
-rw-r--r--spec/features/ci/commits_spec.rb69
-rw-r--r--spec/features/ci/events_spec.rb22
-rw-r--r--spec/features/ci/lint_spec.rb28
-rw-r--r--spec/features/ci/projects_spec.rb60
-rw-r--r--spec/features/ci/runners_spec.rb96
-rw-r--r--spec/features/ci/triggers_spec.rb28
-rw-r--r--spec/features/ci/variables_spec.rb28
-rw-r--r--spec/helpers/ci/application_helper_spec.rb37
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb18
-rw-r--r--spec/lib/ci/ansi2html_spec.rb134
-rw-r--r--spec/lib/ci/charts_spec.rb17
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb313
-rw-r--r--spec/lib/extracts_path_spec.rb2
-rw-r--r--spec/mailers/ci/notify_spec.rb36
-rw-r--r--spec/models/ci/build_spec.rb350
-rw-r--r--spec/models/ci/commit_spec.rb268
-rw-r--r--spec/models/ci/mail_service_spec.rb184
-rw-r--r--spec/models/ci/project_services/hip_chat_message_spec.rb74
-rw-r--r--spec/models/ci/project_services/hip_chat_service_spec.rb74
-rw-r--r--spec/models/ci/project_services/slack_message_spec.rb84
-rw-r--r--spec/models/ci/project_services/slack_service_spec.rb58
-rw-r--r--spec/models/ci/project_spec.rb181
-rw-r--r--spec/models/ci/runner_project_spec.rb16
-rw-r--r--spec/models/ci/runner_spec.rb70
-rw-r--r--spec/models/ci/service_spec.rb49
-rw-r--r--spec/models/ci/trigger_spec.rb17
-rw-r--r--spec/models/ci/variable_spec.rb44
-rw-r--r--spec/models/ci/web_hook_spec.rb62
-rw-r--r--spec/requests/api/merge_requests_spec.rb25
-rw-r--r--spec/requests/ci/api/builds_spec.rb115
-rw-r--r--spec/requests/ci/api/commits_spec.rb65
-rw-r--r--spec/requests/ci/api/forks_spec.rb59
-rw-r--r--spec/requests/ci/api/projects_spec.rb267
-rw-r--r--spec/requests/ci/api/runners_spec.rb83
-rw-r--r--spec/requests/ci/api/triggers_spec.rb78
-rw-r--r--spec/requests/ci/builds_spec.rb18
-rw-r--r--spec/requests/ci/commits_spec.rb17
-rw-r--r--spec/services/ci/create_commit_service_spec.rb132
-rw-r--r--spec/services/ci/create_project_service_spec.rb36
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb52
-rw-r--r--spec/services/ci/event_service_spec.rb34
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb48
-rw-r--r--spec/services/ci/register_build_service_spec.rb91
-rw-r--r--spec/services/ci/web_hook_service_spec.rb36
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/api_helpers.rb11
-rw-r--r--spec/support/filter_spec_helper.rb2
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci.yml63
-rw-r--r--spec/support/gitlab_stubs/project_8.json45
-rw-r--r--spec/support/gitlab_stubs/project_8_hooks.json1
-rw-r--r--spec/support/gitlab_stubs/projects.json1
-rw-r--r--spec/support/gitlab_stubs/session.json20
-rw-r--r--spec/support/gitlab_stubs/user.json20
-rw-r--r--spec/support/login_helpers.rb4
-rw-r--r--spec/support/setup_builds_storage.rb17
-rw-r--r--spec/support/stub_gitlab_calls.rb77
-rw-r--r--spec/support/stub_gitlab_data.rb5
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb14
351 files changed, 16498 insertions, 460 deletions
diff --git a/.gitignore b/.gitignore
index 8a68bb3e4f0..2a97eacad48 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,12 +20,13 @@ backups/*
config/aws.yml
config/database.yml
config/gitlab.yml
-config/initializers/omniauth.rb
+config/gitlab_ci.yml
config/initializers/rack_attack.rb
config/initializers/smtp_settings.rb
config/resque.yml
config/unicorn.rb
config/mail_room.yml
+config/secrets.yml
coverage/*
db/*.sqlite3
db/*.sqlite3-journal
@@ -41,3 +42,4 @@ rails_best_practices_output.html
/tags
tmp/
vendor/bundle/*
+builds/*
diff --git a/.rubocop.yml b/.rubocop.yml
index ea4d365761e..05b8ecc3b00 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -998,7 +998,9 @@ AllCops:
- 'tmp/**/*'
- 'bin/**/*'
- 'lib/backup/**/*'
+ - 'lib/ci/backup/**/*'
- 'lib/tasks/**/*'
+ - 'lib/ci/migrate/**/*'
- 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb'
diff --git a/CHANGELOG b/CHANGELOG
index eb4c59d6205..71238630d31 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.0.0 (unreleased)
+ - Fix broken sort in merge request API (Stan Hu)
- Bump rouge to 1.10.1 to remove warning noise and fix other syntax highlighting bugs (Stan Hu)
- Gracefully handle errors in syntax highlighting by leaving the block unformatted (Stan Hu)
- Add "replace" and "upload" functionalities to allow user replace existing file and upload new file into current repository
diff --git a/CHANGELOG-CI b/CHANGELOG-CI
new file mode 100644
index 00000000000..d1ad661d88b
--- /dev/null
+++ b/CHANGELOG-CI
@@ -0,0 +1,298 @@
+v7.14.0 (unreleased)
+ - Truncate commit messages after subject line in table
+ - Adjust CI config to support Docker executors
+ - Added Application Settings
+ - Randomize test database for CI tests
+ - Make YAML validation stricter
+ - Use avatars received from GitLab
+ - Refactor GitLab API usage to use either access_token or private_token depending on what was specified during login
+ - Allow to use access_token for API requests
+ - Fix project API listing returning empty list when first projects are not added to CI
+ - Allow to define variables from YAML
+ - Added support for CI skipped status
+ - Fix broken yaml error saving
+ - Add committed_at to commits to properly order last commit (the force push issue)
+ - Rename type(s) to stage(s)
+ - Fix navigation icons
+ - Add missing stage when doing retry
+ - Require variable keys to be not-empty and unique
+ - Fix variable saving issue
+ - Display variable saving errors in variables page not the project's
+ - Added Build Triggers API
+
+v7.13.1
+ - Fix: user could steal specific runner
+ - Fix: don't send notifications for jobs with allow_failure set
+ - Fix invalid link to doc.gitlab.com
+
+v7.13.0
+ - Fix inline edit runner-description
+ - Allow to specify image and services in yml that can be used with docker
+ - Fix: No runner notification can see managers only
+ - Fix service testing for slack
+ - Ability to cancel all builds in commit at once
+ - Disable colors in rake tasks automatically (if IO is not a TTY)
+ - Implemented "rake env:info". Rake task to receive system information
+ - Fix coverage calculation on commit page
+ - Enhance YAML validation
+ - Redirect back after authorization
+ - Change favicon
+ - Refactoring: Get rid of private_token usage in the frontend.
+ - Allow to specify allow_failure for job
+ - Build traces is stored in the file instead of database
+ - Make the builds path configurable
+ - Disable link to runner if it's not assigned to specific project
+ - Store all secrets in config/secrets.yml
+ - Encrypt variables
+ - Allow to specify flexible list of types in yaml
+
+v7.12.2
+ - Revert: Runner without tag should pick builds without tag only
+
+v7.12.1
+ - Runner without tag should pick builds without tag only
+ - Explicit error in the GitLab when commit not found.
+ - Fix: lint with relative subpath
+ - Update webhook example
+ - Improved Lint stability
+ - Add warning when .gitlab-ci.yml not found
+ - Improved validation for .gitlab-ci.yml
+ - Fix list of branches in only section
+ - Fix "Status Badge" button
+
+v7.12.0
+ - Endless scroll on the dashboard
+ - Add notification if there are no runners
+ - Fix pagination on dashboard
+ - Remove ID column from runners list in the admin area
+ - Increase default timeout for builds to 60 minutes
+ - Using .gitlab-ci.yml file instead of jobs
+ - Link to the runner from the build page for admin user
+ - Ability to set secret variables for runner
+ - Dont retry build when push same commit in same ref twice
+ - Admin area: show amount of runners with last contact less than a minute ago
+ - Fix re-adding project with the same name but different gitlab_id
+ - Implementation of Lint (.gitlab-ci.yml validation tool)
+ - Updated rails to 4.1.11
+ - API fix: project create call
+ - Link to web-editor with .gitlab-ci.yml
+ - Updated examples in the documentation
+
+v7.11.0
+ - Deploy Jobs API calls
+ - Projects search on dashboard page
+ - Improved runners page
+ - Running and Pending tabs on admin builds page
+ - Fix [ci skip] tag, so you can skip CI triggering now
+ - Add HipChat notifications
+ - Clean up project advanced settings.
+ - Add a GitLab project path parameter to the project API
+ - Remove projects IDs from dashboard
+ - UI fix: Remove page headers from the admin area
+ - Improve Email templates
+ - Add backup/restore utility
+ - Coordinator stores information(version, platform, revision, etc.) about runners.
+ - Fixed pagination on dashboard
+ - Public accessible build and commit pages of public projects
+ - Fix vulnerability in the API when MySQL is used
+
+v7.10.1
+ - Fix failing migration when update to 7.10 from 7.8 and older versions
+
+sidekiq_wirker_fix
+ - added sidekiq.yml
+ - integrated in script/background_jobs
+v7.10.0
+ - Projects sorting by last commit date
+ - Add project search at runner page
+ - Fix GitLab and CI projects collision
+ - Events for admin
+ - Events per projects
+ - Search for runners in admin area
+ - UI improvements: created separated admin section, removed useless project show page
+ - Runners sorting in admin area (by id)
+ - Remove protected_attributes gem
+ - Skip commit creation if there is no appropriate job
+
+v7.9.3
+ - Contains no changes
+ - Developers can cancel and retry jobs
+
+v7.9.2
+ - [Security] Already existing projects should not be served by shared runners
+ - Ability to run deploy job without test jobs (every push will trigger deploy job)
+
+v7.9.1
+ - [Security] Adding explicit is_shared parameter to runner
+ - [Security] By default new projects are not served by shared runners
+
+v7.9.0
+ - Reset user session if token is invalid
+ - Runner delete api endpoint
+ - Fix bug about showing edit button on commit page if user does not have permissions
+ - Allow to pass description and tag list during Runner's registration
+ - Added api for project jobs
+ - Implementation of deploy jobs after all parallel jobs(tests).
+ - Add scroll up/down buttons for better mobile experience with large build traces
+ - Add runner last contact (Kamil Trzciński)
+ - Allow to pause runners - when paused runner will not receive any new build (Kamil Trzciński)
+ - Add brakeman (security scanner for Ruby on Rails)
+ - Changed a color of the canceled builds
+ - Fix of show the same commits in different branches
+
+v7.8.2
+ - Fix the broken build failed email
+ - Notify only pusher instead of commiter
+
+v7.8.0
+ - Fix OAuth login with GitLab installed in relative URL
+ - GitLab CI has same version as GitLab since now
+ - Allow to pass description and tag list during Runner's registration (Kamil Trzciński)
+ - Update documentation (API, Install, Update)
+ - Skip refs field supports for wildcard branch name (ex. feature/*)
+ - Migrate E-mail notification to Services menu (Kamil Trzciński)
+ - Added Slack notifications (Kamil Trzciński)
+ - Disable turbolink on links pointing out to GitLab server
+ - Add test coverage parsing example for pytest-cov
+ - Upgrade raindrops gem
+
+v5.4.2
+ - Fix exposure of project token via build data
+
+v5.4.1
+ - Fix 500 if on builds page if build has no job
+ - Truncate project token from build trace
+ - Allow users with access to project see build trace
+
+v5.4.0 (Requires GitLab 7.7)
+ - Fixed 500 error for badge if build is pending
+ - Non-admin users can now register specific runners for their projects
+ - Project specific runners page which users can access
+ - Remove progress output from schedule_builds cron job
+ - Fix schedule_builds rake task
+ - Fix test webhook button
+ - Job can be branch specific or tag specific or both
+ - Shared runners builds projects which are not assigned to specific ones
+ - Job can be runner specific through tags
+ - Runner have tags
+ - Move job settings to separate page
+ - Add authorization level managing projects
+ - OAuth authentication via GitLab.
+
+v5.3
+ - Remove annoying 'Done' message from schedule_builds cron job
+ - Fix a style issue with the navbar
+ - Skip CSRF check on the project's build page
+ - Fix showing wrong build script on admin projects page
+ - Add branch and commit message to build result emails
+
+v5.2
+ - Improve performance by adding new indicies
+ - Separate Commit logic from Build logic in prep for Parallel Builds
+ - Parallel builds
+ - You can have multiple build scripts per project
+
+v5.1
+ - Registration token and runner token are named differently
+ - Redirect to previous page after sign-in
+ - Dont show archived projects
+ - Add support for skip branches from build
+ - Add coverage parsing feature
+ - Update rails to 4.0.10
+ - Look for a REVISION file before running `git log`
+ - All builds page for admin
+
+v5.0.1
+ - Update rails to 4.0.5
+
+v5.0.0
+ - Set build timeout in minutes
+ - Web Hooks for builds
+ - Nprogress bar
+ - Remove extra spaces in build script
+ - Requires runner v5
+ * All script commands executed as one file
+ * Cancel button works correctly now
+ * Runner stability increased
+ * Timeout applies to build now instead of line of script
+
+v4.3.0
+ - Refactor build js
+ - Redirect to build page with sha + bid if build id is not provided
+ - Update rails to 4.0.3
+ - Restyle project settings page
+ - Improve help page
+ - Replaced puma with unicorn
+ - Improved init.d script
+ - Add submodule init to default build script for new projects
+
+v4.2.0
+ - Build duration chart
+ - Bootstrap 3 with responsive UI
+ - Improved init.d script
+ - Refactoring
+ - Changed http codes for POST /projects/:id/build action
+ - Turbolinks
+
+v4.1.0
+ - Rails 4
+ - Click on build branch to see other builds for this branch
+ - Email notifications (Jeroen Knoops)
+
+v4.0.0
+ - Shared runners (no need to add runner to every project)
+ - Admin area (only available for GitLab admins)
+ - Hide all runners management into admin area
+ - Use http cloning for builds instead of deploy keys
+ - Allow choose between git clone and git fetch when get code for build
+ - Make build timeout actually works
+ - Requires GitLab 6.3 or higher
+ - GitLab CI settings go to GitLab project via api on creation
+
+v3.2.0
+ - Limit visibility of projects by gitlab authorized projects
+ - Use one page for both gitlab and gitlab-ci projects
+
+v3.1.0
+ - Login with both username, email or LDAP credentials (if GitLab 6.0+)
+ - Retry build button functionality
+ - UI fixes for resolution 1366px and lower
+ - Fix gravatar ssl warning
+
+v3.0.0
+ - Build running functionality extracted in gitlab-ci-runner
+ - Added API for runners and builds
+ - Redesigned application
+ - Added charts
+ - Use GitLab auth
+ - Add projects via UI with few clicks
+
+v2.2.0
+ - replaced unicorn with puma
+ - replaced grit with rugged
+ - Runner.rb more transactional safe now
+ - updated rails to 3.2.13
+ - updated devise to 2.2
+ - fixed issue when build left in running status if exception triggered
+ - rescue build timeout correctly
+ - badge helper with markdown & html
+ - increased test coverage to 85%
+
+v2.1.0
+ - Removed horizontal scroll for build trace
+ - new status badges
+ - better encode
+ - added several CI_* env variables
+
+v2.0.0
+ - Replace resque with sidekiq
+ - Run only one build at time per project
+ - Added whenever for schedule jobs
+
+v1.2.0
+ - Added Github web hook support
+ - Added build schedule
+
+v1.1.0
+ - Added JSON response for builds status
+ - Compatible with GitLab v4.0.0 \ No newline at end of file
diff --git a/Gemfile b/Gemfile
index f6b2a0a41da..1903d66e6ab 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,14 @@
source "https://rubygems.org"
-gem 'rails', '4.1.11'
+def darwin_only(require_as)
+ RUBY_PLATFORM.include?('darwin') && require_as
+end
+
+def linux_only(require_as)
+ RUBY_PLATFORM.include?('linux') && require_as
+end
+
+gem 'rails', '4.1.12'
# Specify a sprockets version due to security issue
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
@@ -10,29 +18,29 @@ gem 'sprockets', '~> 2.12.3'
gem "default_value_for", "~> 3.0.0"
# Supported DBs
-gem "mysql2", group: :mysql
-gem "pg", group: :postgres
+gem "mysql2", '~> 0.3.16', group: :mysql
+gem "pg", '~> 0.18.2', group: :postgres
# Authentication libraries
-gem "devise", '3.2.4'
-gem "devise-async", '0.9.0'
+gem "devise", '~> 3.2.4'
+gem "devise-async", '~> 0.9.0'
gem 'omniauth', "~> 1.2.2"
-gem 'omniauth-google-oauth2'
-gem 'omniauth-twitter'
-gem 'omniauth-github'
-gem 'omniauth-shibboleth'
-gem 'omniauth-kerberos', group: :kerberos
-gem 'omniauth-gitlab'
-gem 'omniauth-bitbucket'
+gem 'omniauth-google-oauth2', '~> 0.2.5'
+gem 'omniauth-twitter', '~> 1.0.1'
+gem 'omniauth-github', '~> 1.1.1'
+gem 'omniauth-shibboleth', '~> 1.1.1'
+gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos
+gem 'omniauth-gitlab', '~> 1.0.0'
+gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-saml', '~> 1.4.0'
+gem 'doorkeeper', '~> 2.1.3'
gem 'omniauth_crowd'
-gem 'doorkeeper', '2.1.3'
gem "rack-oauth2", "~> 1.0.5"
# Two-factor authentication
-gem 'devise-two-factor'
-gem 'rqrcode-rails3'
-gem 'attr_encrypted', '1.3.4'
+gem 'devise-two-factor', '~> 1.0.1'
+gem 'rqrcode-rails3', '~> 0.1.7'
+gem 'attr_encrypted', '~> 1.3.4'
# Browser detection
gem "browser", '~> 1.0.0'
@@ -44,7 +52,7 @@ gem "gitlab_git", '~> 7.2.15'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
-gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap"
+gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
# Git Wiki
gem 'gollum-lib', '~> 4.0.2'
@@ -59,47 +67,47 @@ gem "gitlab-linguist", "~> 3.0.1", require: "linguist"
# API
gem "grape", "~> 0.6.1"
gem "grape-entity", "~> 0.4.2"
-gem 'rack-cors', require: 'rack/cors'
+gem 'rack-cors', '~> 0.2.9', require: 'rack/cors'
# Format dates and times
# based on human-friendly examples
-gem "stamp"
+gem "stamp", '~> 0.5.0'
# Enumeration fields
-gem 'enumerize'
+gem 'enumerize', '~> 0.7.0'
# Pagination
gem "kaminari", "~> 0.15.1"
# HAML
-gem "haml-rails"
+gem "haml-rails", '~> 0.5.3'
# Files attachments
-gem "carrierwave"
+gem "carrierwave", '~> 0.9.0'
# Drag and Drop UI
-gem 'dropzonejs-rails'
+gem 'dropzonejs-rails', '~> 0.7.1'
# for aws storage
gem "fog", "~> 1.25.0"
-gem "unf"
+gem "unf", '~> 0.1.4'
# Authorization
-gem "six"
+gem "six", '~> 0.2.0'
# Seed data
-gem "seed-fu"
+gem "seed-fu", '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
-gem 'task_list', '1.0.2', require: 'task_list/railtie'
-gem 'github-markup'
+gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
+gem 'github-markup', '~> 1.3.1'
gem 'redcarpet', '~> 3.3.2'
-gem 'RedCloth'
+gem 'RedCloth', '~> 4.2.9'
gem 'rdoc', '~>3.6'
-gem 'org-ruby', '= 0.9.12'
+gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~>0.3.6'
-gem 'wikicloth', '=0.8.1'
+gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
# Diffs
@@ -107,37 +115,38 @@ gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
- gem "unicorn", '~> 4.6.3'
- gem 'unicorn-worker-killer'
+ gem "unicorn", '~> 4.8.2'
+ gem 'unicorn-worker-killer', '~> 0.4.2'
end
# State machine
-gem "state_machine"
+gem "state_machine", '~> 1.2.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
-gem 'slim'
-gem 'sinatra', require: nil
-gem 'sidekiq', '~> 3.3'
-gem 'sidetiq', '0.6.3'
+gem 'slim', '~> 2.0.2'
+gem 'sinatra', '~> 1.4.4', require: nil
+gem 'sidekiq', '3.3.0'
+gem 'sidetiq', '~> 0.6.3'
# HTTP requests
-gem "httparty"
+gem "httparty", '~> 0.13.3'
# Colored output to console
-gem "colored"
+gem "colored", '~> 1.2'
+gem "colorize", '~> 0.5.8'
# GitLab settings
-gem 'settingslogic'
+gem 'settingslogic', '~> 2.0.9'
# Misc
-gem "foreman"
-gem 'version_sorter'
+
+gem 'version_sorter', '~> 2.0.0'
# Cache
-gem "redis-rails"
+gem "redis-rails", '~> 4.0.0'
# Campfire integration
gem 'tinder', '~> 1.9.2'
@@ -176,69 +185,70 @@ gem "sanitize", '~> 2.0'
gem "rack-attack", '~> 4.3.0'
# Ace editor
-gem 'ace-rails-ap'
+gem 'ace-rails-ap', '~> 2.0.1'
# Keyboard shortcuts
-gem 'mousetrap-rails'
+gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
-gem 'charlock_holmes'
+gem 'charlock_holmes', '~> 0.6.9.4'
gem "sass-rails", '~> 4.0.5'
-gem "coffee-rails"
-gem "uglifier"
+gem "coffee-rails", '~> 4.1.0'
+gem "uglifier", '~> 2.3.2'
gem 'turbolinks', '~> 2.5.0'
-gem 'jquery-turbolinks'
+gem 'jquery-turbolinks', '~> 2.0.1'
-gem 'addressable'
+gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.1'
gem 'gon', '~> 5.0.0'
gem 'jquery-atwho-rails', '~> 1.0.0'
-gem 'jquery-rails', '3.1.3'
-gem 'jquery-scrollto-rails'
-gem 'jquery-ui-rails'
-gem 'nprogress-rails'
+gem 'jquery-rails', '~> 3.1.3'
+gem 'jquery-scrollto-rails', '~> 1.4.3'
+gem 'jquery-ui-rails', '~> 4.2.1'
+gem 'nprogress-rails', '~> 0.1.2.3'
gem 'raphael-rails', '~> 2.1.2'
-gem 'request_store'
+gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9'
-gem 'virtus'
+gem 'virtus', '~> 1.0.1'
group :development do
- gem 'brakeman', require: false
- gem "annotate", "~> 2.6.0.beta2"
- gem "letter_opener"
- gem 'quiet_assets', '~> 1.0.1'
- gem 'rack-mini-profiler', require: false
+ gem "foreman"
+ gem 'brakeman', '3.0.1', require: false
+
+ gem "annotate", "~> 2.6.0"
+ gem "letter_opener", '~> 1.1.2'
+ gem 'quiet_assets', '~> 1.0.2'
+ gem 'rack-mini-profiler', '~> 0.9.0', require: false
gem 'rerun', '~> 0.10.0'
# Better errors handler
- gem 'better_errors'
- gem 'binding_of_caller'
+ gem 'better_errors', '~> 1.0.1'
+ gem 'binding_of_caller', '~> 0.7.2'
# Docs generator
- gem "sdoc"
+ gem "sdoc", '~> 0.3.20'
# thin instead webrick
- gem 'thin'
+ gem 'thin', '~> 1.6.1'
end
group :development, :test do
- gem 'awesome_print'
gem 'byebug', platform: :mri
- gem 'fuubar', '~> 2.0.0'
gem 'pry-rails'
- gem 'coveralls', '~> 0.8.2', require: false
+ gem 'awesome_print', '~> 1.2.0'
+ gem 'fuubar', '~> 2.0.0'
+
gem 'database_cleaner', '~> 1.4.0'
- gem 'factory_girl_rails'
+ gem 'factory_girl_rails', '~> 4.3.0'
gem 'rspec-rails', '~> 3.3.0'
- gem 'rubocop', '0.28.0', require: false
- gem 'spinach-rails'
+ gem 'spinach-rails', '~> 0.2.1'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
- gem 'minitest', '~> 5.3.0'
+ gem 'minitest', '~> 5.7.0'
# Generate Fake data
gem 'ffaker', '~> 2.0.0'
@@ -248,20 +258,23 @@ group :development, :test do
gem 'poltergeist', '~> 1.6.0'
gem 'teaspoon', '~> 1.0.0'
- gem 'teaspoon-jasmine'
+ gem 'teaspoon-jasmine', '~> 2.2.0'
- gem 'spring', '~> 1.3.1'
- gem 'spring-commands-rspec', '~> 1.0.0'
+ gem 'spring', '~> 1.3.6'
+ gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
+
+ gem 'rubocop', '~> 0.28.0', require: false
+ gem 'coveralls', '~> 0.8.2', require: false
+ gem 'simplecov', '~> 0.10.0', require: false
end
group :test do
- gem 'simplecov', require: false
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'webmock', '~> 1.21.0'
- gem 'test_after_commit'
+ gem 'test_after_commit', '~> 0.2.2'
gem 'sham_rack'
end
@@ -269,10 +282,32 @@ group :production do
gem "gitlab_meta", '7.0'
end
-gem "newrelic_rpm"
+gem "newrelic_rpm", '~> 3.9.4.245'
-gem 'octokit', '3.7.0'
+gem 'octokit', '~> 3.7.0'
gem "mail_room", "~> 0.5.1"
-gem 'email_reply_parser'
+gem 'email_reply_parser', '~> 0.5.8'
+
+## CI
+gem 'activerecord-deprecated_finders', '~> 1.0.3'
+gem 'activerecord-session_store', '~> 0.1.0'
+gem "nested_form", '~> 0.3.2'
+
+# Scheduled
+gem 'whenever', '~> 0.8.4', require: false
+
+# OAuth
+gem 'oauth2', '~> 1.0.0'
+
+# Soft deletion
+gem "paranoia", "~> 2.0"
+
+group :development, :test do
+ gem 'guard-rspec', '~> 4.2.0'
+
+ gem 'rb-fsevent', require: darwin_only('rb-fsevent')
+ gem 'growl', require: darwin_only('growl')
+ gem 'rb-inotify', require: linux_only('rb-inotify')
+end
diff --git a/Gemfile.lock b/Gemfile.lock
index 6a82d99a14f..b033f6fab63 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,31 +4,36 @@ GEM
CFPropertyList (2.3.1)
RedCloth (4.2.9)
ace-rails-ap (2.0.1)
- actionmailer (4.1.11)
- actionpack (= 4.1.11)
- actionview (= 4.1.11)
+ actionmailer (4.1.12)
+ actionpack (= 4.1.12)
+ actionview (= 4.1.12)
mail (~> 2.5, >= 2.5.4)
- actionpack (4.1.11)
- actionview (= 4.1.11)
- activesupport (= 4.1.11)
+ actionpack (4.1.12)
+ actionview (= 4.1.12)
+ activesupport (= 4.1.12)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
- actionview (4.1.11)
- activesupport (= 4.1.11)
+ actionview (4.1.12)
+ activesupport (= 4.1.12)
builder (~> 3.1)
erubis (~> 2.7.0)
- activemodel (4.1.11)
- activesupport (= 4.1.11)
+ activemodel (4.1.12)
+ activesupport (= 4.1.12)
builder (~> 3.1)
- activerecord (4.1.11)
- activemodel (= 4.1.11)
- activesupport (= 4.1.11)
+ activerecord (4.1.12)
+ activemodel (= 4.1.12)
+ activesupport (= 4.1.12)
arel (~> 5.0.0)
+ activerecord-deprecated_finders (1.0.4)
+ activerecord-session_store (0.1.1)
+ actionpack (>= 4.0.0, < 5)
+ activerecord (>= 4.0.0, < 5)
+ railties (>= 4.0.0, < 5)
activeresource (4.0.0)
activemodel (~> 4.0)
activesupport (~> 4.0)
rails-observers (~> 0.1.1)
- activesupport (4.1.11)
+ activesupport (4.1.12)
i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
@@ -37,33 +42,34 @@ GEM
acts-as-taggable-on (3.5.0)
activerecord (>= 3.2, < 5)
addressable (2.3.8)
- annotate (2.6.0)
- activerecord (>= 2.3.0)
- rake (>= 0.8.7)
+ annotate (2.6.10)
+ activerecord (>= 3.2, <= 4.3)
+ rake (~> 10.4)
arel (5.0.1.20140414130214)
asana (0.0.6)
activeresource (>= 3.2.3)
asciidoctor (1.5.2)
- ast (2.0.0)
- astrolabe (1.3.0)
- parser (>= 2.2.0.pre.3, < 3.0)
+ ast (2.1.0)
+ astrolabe (1.3.1)
+ parser (~> 2.2)
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
- autoprefixer-rails (5.1.11)
+ autoprefixer-rails (5.2.1.2)
execjs
json
awesome_print (1.2.0)
- axiom-types (0.0.5)
- descendants_tracker (~> 0.0.1)
- ice_nine (~> 0.9)
- bcrypt (3.1.7)
+ axiom-types (0.1.1)
+ descendants_tracker (~> 0.0.4)
+ ice_nine (~> 0.11.0)
+ thread_safe (~> 0.3, >= 0.3.1)
+ bcrypt (3.1.10)
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (3.3.4.1)
+ bootstrap-sass (3.3.5)
autoprefixer-rails (>= 5.0.0.1)
sass (>= 3.2.19)
brakeman (3.0.1)
@@ -78,9 +84,7 @@ GEM
terminal-table (~> 1.4)
browser (1.0.0)
builder (3.2.2)
- byebug (3.2.0)
- columnize (~> 0.8)
- debugger-linecache (~> 1.2)
+ byebug (6.0.2)
cal-heatmap-rails (0.0.1)
capybara (2.4.4)
mime-types (>= 1.16)
@@ -88,7 +92,7 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- capybara-screenshot (1.0.9)
+ capybara-screenshot (1.0.11)
capybara (>= 1.0, < 3)
launchy
carrierwave (0.9.0)
@@ -98,6 +102,8 @@ GEM
celluloid (0.16.0)
timers (~> 4.0.0)
charlock_holmes (0.6.9.4)
+ chronic (0.10.2)
+ chunky_png (1.3.4)
cliver (0.3.2)
coderay (1.1.0)
coercible (1.0.0)
@@ -111,8 +117,7 @@ GEM
coffee-script-source (1.9.1.1)
colored (1.2)
colorize (0.5.8)
- columnize (0.9.0)
- connection_pool (2.1.0)
+ connection_pool (2.2.0)
coveralls (0.8.2)
json (~> 1.8)
rest-client (>= 1.6.8, < 2)
@@ -122,15 +127,15 @@ GEM
crack (0.4.2)
safe_yaml (~> 1.0.0)
creole (0.3.8)
- d3_rails (3.5.5)
+ d3_rails (3.5.6)
railties (>= 3.1.0)
- daemons (1.1.9)
+ daemons (1.2.3)
database_cleaner (1.4.1)
debug_inspector (0.0.2)
- debugger-linecache (1.2.0)
- default_value_for (3.0.0)
+ default_value_for (3.0.1)
activerecord (>= 3.2.0, < 5.0)
- descendants_tracker (0.0.3)
+ descendants_tracker (0.0.4)
+ thread_safe (~> 0.3, >= 0.3.1)
devise (3.2.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@@ -139,21 +144,20 @@ GEM
warden (~> 1.2.3)
devise-async (0.9.0)
devise (~> 3.2)
- devise-two-factor (1.0.1)
+ devise-two-factor (1.0.2)
activemodel
activesupport
attr_encrypted (~> 1.3.2)
- devise (~> 3.2.4)
- rails
- rotp (~> 1.6.1)
+ devise (>= 3.2.4, < 3.5)
+ railties
+ rotp (< 2)
diff-lcs (1.2.5)
- diffy (3.0.3)
+ diffy (3.0.7)
docile (1.1.5)
domain_name (0.5.24)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (2.1.3)
+ doorkeeper (2.1.4)
railties (>= 3.2)
- dotenv (0.9.0)
dropzonejs-rails (0.7.1)
rails (> 3.1)
email_reply_parser (0.5.8)
@@ -163,25 +167,25 @@ GEM
encryptor (1.3.0)
enumerize (0.7.0)
activesupport (>= 3.2)
- equalizer (0.0.8)
+ equalizer (0.0.11)
erubis (2.7.0)
escape_utils (0.2.4)
- eventmachine (1.0.4)
- excon (0.45.3)
- execjs (2.5.2)
+ eventmachine (1.0.8)
+ excon (0.45.4)
+ execjs (2.6.0)
expression_parser (0.9.0)
factory_girl (4.3.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.3.0)
factory_girl (~> 4.3.0)
railties (>= 3.0.0)
- faraday (0.8.9)
+ faraday (0.8.10)
multipart-post (~> 1.2.0)
- faraday_middleware (0.9.0)
- faraday (>= 0.7.4, < 0.9)
+ faraday_middleware (0.10.0)
+ faraday (>= 0.7.4, < 0.10)
fastercsv (1.5.5)
ffaker (2.0.0)
- ffi (1.9.8)
+ ffi (1.9.10)
fission (0.5.0)
CFPropertyList (~> 2.2)
flowdock (0.7.0)
@@ -202,11 +206,11 @@ GEM
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
opennebula
- fog-brightbox (0.7.1)
+ fog-brightbox (0.9.0)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
- fog-core (1.30.0)
+ fog-core (1.32.1)
builder
excon (~> 0.45)
formatador (~> 0.2)
@@ -216,7 +220,7 @@ GEM
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
- fog-profitbricks (0.0.3)
+ fog-profitbricks (0.0.5)
fog-core
fog-xml
nokogiri
@@ -227,7 +231,7 @@ GEM
fog-sakuracloud (1.0.1)
fog-core
fog-json
- fog-softlayer (0.4.6)
+ fog-softlayer (0.4.7)
fog-core
fog-json
fog-terremark (0.1.0)
@@ -242,11 +246,10 @@ GEM
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
- font-awesome-rails (4.2.0.0)
+ font-awesome-rails (4.4.0.0)
railties (>= 3.2, < 5.0)
- foreman (0.63.0)
- dotenv (>= 0.7)
- thor (>= 0.13.6)
+ foreman (0.78.0)
+ thor (~> 0.19.1)
formatador (0.2.5)
fuubar (2.0.0)
rspec (~> 3.0)
@@ -255,15 +258,14 @@ GEM
rugged (~> 0.21)
gemojione (2.0.1)
json
- gherkin-ruby (0.3.1)
- racc
- github-markup (1.3.1)
- posix-spawn (~> 0.3.8)
+ get_process_mem (0.2.0)
+ gherkin-ruby (0.3.2)
+ github-markup (1.3.3)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-grit (2.7.2)
+ gitlab-grit (2.7.3)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
mime-types (~> 1.15)
@@ -285,16 +287,16 @@ GEM
omniauth (~> 1.0)
pyu-ruby-sasl (~> 0.0.3.1)
rubyntlm (~> 0.3)
- gollum-grit_adapter (0.1.3)
+ gollum-grit_adapter (1.0.0)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.0.2)
- github-markup (~> 1.3.1)
- gollum-grit_adapter (~> 0.1, >= 0.1.1)
+ gollum-lib (4.0.3)
+ github-markup (~> 1.3.3)
+ gollum-grit_adapter (~> 1.0)
nokogiri (~> 1.6.4)
rouge (~> 1.10.1)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
- gon (5.0.1)
+ gon (5.0.4)
actionpack (>= 2.3.0)
json
grape (0.6.1)
@@ -307,9 +309,22 @@ GEM
rack-accept
rack-mount
virtus (>= 1.0.0)
- grape-entity (0.4.2)
+ grape-entity (0.4.8)
activesupport
multi_json (>= 1.3.2)
+ growl (1.0.3)
+ guard (2.13.0)
+ formatador (>= 0.2.4)
+ listen (>= 2.7, <= 4.0)
+ lumberjack (~> 1.0)
+ nenv (~> 0.1)
+ notiffany (~> 0.0)
+ pry (>= 0.9.12)
+ shellany (~> 0.0)
+ thor (>= 0.18.1)
+ guard-rspec (4.2.10)
+ guard (~> 2.1)
+ rspec (>= 2.14, < 4.0)
haml (4.0.7)
tilt
haml-rails (0.5.3)
@@ -320,24 +335,23 @@ GEM
hashie (2.1.2)
highline (1.6.21)
hike (1.2.3)
- hipchat (1.5.0)
+ hipchat (1.5.2)
httparty
mimemagic
- hitimes (1.2.2)
+ hitimes (1.2.3)
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
http-cookie (1.0.2)
domain_name (~> 0.5)
http_parser.rb (0.5.3)
- httparty (0.13.3)
+ httparty (0.13.5)
json (~> 1.8)
multi_xml (>= 0.5.2)
- httpauth (0.2.1)
- httpclient (2.5.3.3)
+ httpclient (2.6.0.1)
i18n (0.7.0)
ice_cube (0.11.1)
- ice_nine (0.10.0)
+ ice_nine (0.11.1)
inflecto (0.0.2)
ipaddress (0.8.0)
jquery-atwho-rails (1.0.1)
@@ -346,26 +360,26 @@ GEM
thor (>= 0.14, < 2.0)
jquery-scrollto-rails (1.4.3)
railties (> 3.1, < 5.0)
- jquery-turbolinks (2.0.1)
+ jquery-turbolinks (2.0.2)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (4.2.1)
railties (>= 3.2.16)
json (1.8.3)
- jwt (0.1.13)
- multi_json (>= 1.5)
+ jwt (1.5.1)
kaminari (0.15.1)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
- kgio (2.9.2)
+ kgio (2.9.3)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.1.2)
launchy (~> 2.2)
- listen (2.10.0)
+ listen (2.10.1)
celluloid (~> 0.16.0)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
+ lumberjack (1.0.9)
macaddr (1.7.1)
systemu (~> 2.6.2)
mail (2.6.3)
@@ -375,12 +389,14 @@ GEM
mime-types (1.25.1)
mimemagic (0.3.0)
mini_portile (0.6.2)
- minitest (5.3.5)
+ minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.11.2)
multi_xml (0.5.5)
multipart-post (1.2.0)
- mysql2 (0.3.16)
+ mysql2 (0.3.20)
+ nenv (0.2.0)
+ nested_form (0.3.2)
net-ldap (0.11)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
@@ -389,15 +405,18 @@ GEM
newrelic_rpm (3.9.4.245)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
+ notiffany (0.0.7)
+ nenv (~> 0.1)
+ shellany (~> 0.0)
nprogress-rails (0.1.2.3)
oauth (0.4.7)
- oauth2 (0.8.1)
- faraday (~> 0.8)
- httpauth (~> 0.1)
- jwt (~> 0.1.4)
- multi_json (~> 1.0)
+ oauth2 (1.0.0)
+ faraday (>= 0.8, < 0.10)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ multi_xml (~> 0.5)
rack (~> 1.2)
- octokit (3.7.0)
+ octokit (3.7.1)
sawyer (~> 0.6.0, >= 0.5.3)
omniauth (1.2.2)
hashie (>= 1.2, < 4)
@@ -406,30 +425,30 @@ GEM
multi_json (~> 1.7)
omniauth (~> 1.1)
omniauth-oauth (~> 1.0)
- omniauth-github (1.1.1)
+ omniauth-github (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-gitlab (1.0.0)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
- omniauth-google-oauth2 (0.2.5)
+ omniauth-google-oauth2 (0.2.6)
omniauth (> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-kerberos (0.2.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
- omniauth-multipassword (0.4.1)
+ omniauth-multipassword (0.4.2)
omniauth (~> 1.0)
- omniauth-oauth (1.0.1)
+ omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
- omniauth-oauth2 (1.1.1)
- oauth2 (~> 0.8.0)
- omniauth (~> 1.0)
+ omniauth-oauth2 (1.3.1)
+ oauth2 (~> 1.0)
+ omniauth (~> 1.2)
omniauth-saml (1.4.1)
omniauth (~> 1.1)
ruby-saml (~> 1.0.0)
- omniauth-shibboleth (1.1.1)
+ omniauth-shibboleth (1.1.2)
omniauth (>= 1.0.0)
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
@@ -445,7 +464,9 @@ GEM
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
- parser (2.2.0.2)
+ paranoia (2.1.3)
+ activerecord (~> 4.0)
+ parser (2.2.2.6)
ast (>= 1.1, < 3.0)
pg (0.18.2)
poltergeist (1.6.0)
@@ -453,60 +474,59 @@ GEM
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
- posix-spawn (0.3.9)
+ posix-spawn (0.3.11)
powerpack (0.0.9)
- pry (0.9.12.4)
- coderay (~> 1.0)
- method_source (~> 0.8)
+ pry (0.10.1)
+ coderay (~> 1.1.0)
+ method_source (~> 0.8.1)
slop (~> 3.4)
- pry-rails (0.3.2)
+ pry-rails (0.3.4)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
- quiet_assets (1.0.2)
+ quiet_assets (1.0.3)
railties (>= 3.1, < 5.0)
- racc (1.4.12)
rack (1.5.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.3.0)
rack
rack-cors (0.2.9)
- rack-mini-profiler (0.9.0)
+ rack-mini-profiler (0.9.7)
rack (>= 1.1.3)
rack-mount (0.8.3)
rack (>= 1.0.0)
- rack-oauth2 (1.0.8)
+ rack-oauth2 (1.0.10)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
- httpclient (>= 2.2.0.2)
+ httpclient (>= 2.4)
multi_json (>= 1.3.6)
rack (>= 1.1)
- rack-protection (1.5.1)
+ rack-protection (1.5.3)
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.1.11)
- actionmailer (= 4.1.11)
- actionpack (= 4.1.11)
- actionview (= 4.1.11)
- activemodel (= 4.1.11)
- activerecord (= 4.1.11)
- activesupport (= 4.1.11)
+ rails (4.1.12)
+ actionmailer (= 4.1.12)
+ actionpack (= 4.1.12)
+ actionview (= 4.1.12)
+ activemodel (= 4.1.12)
+ activerecord (= 4.1.12)
+ activesupport (= 4.1.12)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.1.11)
+ railties (= 4.1.12)
sprockets-rails (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
- railties (4.1.11)
- actionpack (= 4.1.11)
- activesupport (= 4.1.11)
+ railties (4.1.12)
+ actionpack (= 4.1.12)
+ activesupport (= 4.1.12)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.0.0)
- raindrops (0.13.0)
+ raindrops (0.15.0)
rake (10.4.2)
raphael-rails (2.1.2)
- rb-fsevent (0.9.4)
+ rb-fsevent (0.9.5)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
rbvmomi (1.8.2)
@@ -521,10 +541,10 @@ GEM
actionpack (~> 4)
redis-rack (~> 1.5.0)
redis-store (~> 1.1.0)
- redis-activesupport (4.0.0)
+ redis-activesupport (4.1.1)
activesupport (~> 4)
redis-store (~> 1.1.0)
- redis-namespace (1.5.1)
+ redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
redis-rack (1.5.0)
rack (~> 1.5)
@@ -535,7 +555,7 @@ GEM
redis-store (~> 1.1.0)
redis-store (1.1.6)
redis (>= 2.2)
- request_store (1.0.5)
+ request_store (1.2.0)
rerun (0.10.0)
listen (~> 2.7, >= 2.7.3)
rest-client (1.8.0)
@@ -545,22 +565,23 @@ GEM
rinku (1.7.3)
rotp (1.6.1)
rouge (1.10.1)
- rqrcode (0.4.2)
+ rqrcode (0.7.0)
+ chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
rspec (3.3.0)
rspec-core (~> 3.3.0)
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
- rspec-core (3.3.1)
+ rspec-core (3.3.2)
rspec-support (~> 3.3.0)
- rspec-expectations (3.3.0)
+ rspec-expectations (3.3.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
- rspec-mocks (3.3.0)
+ rspec-mocks (3.3.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
- rspec-rails (3.3.2)
+ rspec-rails (3.3.3)
actionpack (>= 3.0, < 4.3)
activesupport (>= 3.0, < 4.3)
railties (>= 3.0, < 4.3)
@@ -577,16 +598,16 @@ GEM
ruby-progressbar (~> 1.4)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
- ruby-progressbar (1.7.1)
+ ruby-progressbar (1.7.5)
ruby-saml (1.0.0)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
- ruby2ruby (2.1.3)
+ ruby2ruby (2.1.4)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
ruby_parser (3.5.0)
sexp_processor (~> 4.1)
- rubyntlm (0.5.0)
+ rubyntlm (0.5.2)
rubypants (0.2.0)
rugged (0.22.2)
safe_yaml (1.0.4)
@@ -610,9 +631,10 @@ GEM
select2-rails (3.5.9.3)
thor (~> 0.14)
settingslogic (2.0.9)
- sexp_processor (4.4.5)
+ sexp_processor (4.6.0)
sham_rack (1.3.6)
rack
+ shellany (0.0.1)
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
sidekiq (3.3.0)
@@ -631,19 +653,20 @@ GEM
json (~> 1.8)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
- sinatra (1.4.4)
+ sinatra (1.4.6)
rack (~> 1.4)
rack-protection (~> 1.4)
- tilt (~> 1.3, >= 1.3.4)
+ tilt (>= 1.3, < 3)
six (0.2.0)
slack-notifier (1.0.0)
- slim (2.0.2)
+ slim (2.0.3)
temple (~> 0.6.6)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
- spinach (0.8.7)
- colorize (= 0.5.8)
- gherkin-ruby (>= 0.3.1)
+ spinach (0.8.10)
+ colorize
+ gherkin-ruby (>= 0.3.2)
+ json
spinach-rails (0.2.1)
capybara (>= 2.0.0)
railties (>= 3)
@@ -674,31 +697,32 @@ GEM
railties (>= 3.2.5, < 5)
teaspoon-jasmine (2.2.0)
teaspoon (>= 1.0.0)
- temple (0.6.7)
+ temple (0.6.10)
term-ansicolor (1.3.2)
tins (~> 1.0)
- terminal-table (1.4.5)
- test_after_commit (0.2.2)
- thin (1.6.1)
- daemons (>= 1.0.9)
- eventmachine (>= 1.0.0)
- rack (>= 1.0.0)
+ terminal-table (1.5.2)
+ test_after_commit (0.2.7)
+ activerecord (>= 3.2)
+ thin (1.6.3)
+ daemons (~> 1.0, >= 1.0.9)
+ eventmachine (~> 1.0)
+ rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
tilt (1.4.1)
- timers (4.0.1)
+ timers (4.0.4)
hitimes
timfel-krb5-auth (0.8.3)
- tinder (1.9.3)
+ tinder (1.9.4)
eventmachine (~> 1.0)
- faraday (~> 0.8)
+ faraday (~> 0.8.9)
faraday_middleware (~> 0.9)
hashie (>= 1.0, < 3)
json (~> 1.8.0)
mime-types (~> 1.19)
multi_json (~> 1.7)
twitter-stream (~> 0.1)
- tins (1.5.4)
+ tins (1.6.0)
trollop (2.1.2)
turbolinks (2.5.3)
coffee-rails
@@ -708,35 +732,39 @@ GEM
simple_oauth (~> 0.1.4)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- uglifier (2.3.2)
+ uglifier (2.3.3)
execjs (>= 0.3.0)
json (>= 1.8.0)
underscore-rails (1.4.4)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
- unicorn (4.6.3)
+ unicorn (4.8.3)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
- unicorn-worker-killer (0.4.2)
+ unicorn-worker-killer (0.4.3)
+ get_process_mem (~> 0)
unicorn (~> 4)
uuid (2.3.8)
macaddr (~> 1.0)
version_sorter (2.0.0)
- virtus (1.0.1)
- axiom-types (~> 0.0.5)
+ virtus (1.0.5)
+ axiom-types (~> 0.1)
coercible (~> 1.0)
- descendants_tracker (~> 0.0.1)
- equalizer (~> 0.0.7)
+ descendants_tracker (~> 0.0, >= 0.0.3)
+ equalizer (~> 0.0, >= 0.0.9)
warden (1.2.3)
rack (>= 1.0)
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
- websocket-driver (0.5.4)
+ websocket-driver (0.6.2)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
+ whenever (0.8.4)
+ activesupport (>= 2.3.4)
+ chronic (>= 0.6.3)
wikicloth (0.8.1)
builder
expression_parser
@@ -748,146 +776,157 @@ PLATFORMS
ruby
DEPENDENCIES
- RedCloth
- ace-rails-ap
+ RedCloth (~> 4.2.9)
+ ace-rails-ap (~> 2.0.1)
+ activerecord-deprecated_finders (~> 1.0.3)
+ activerecord-session_store (~> 0.1.0)
acts-as-taggable-on (~> 3.4)
- addressable
- annotate (~> 2.6.0.beta2)
+ addressable (~> 2.3.8)
+ annotate (~> 2.6.0)
asana (~> 0.0.6)
asciidoctor (~> 1.5.2)
- attr_encrypted (= 1.3.4)
- awesome_print
- better_errors
- binding_of_caller
+ attr_encrypted (~> 1.3.4)
+ awesome_print (~> 1.2.0)
+ better_errors (~> 1.0.1)
+ binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.0)
- brakeman
+ brakeman (= 3.0.1)
browser (~> 1.0.0)
byebug
cal-heatmap-rails (~> 0.0.1)
capybara (~> 2.4.0)
capybara-screenshot (~> 1.0.0)
- carrierwave
- charlock_holmes
- coffee-rails
- colored
+ carrierwave (~> 0.9.0)
+ charlock_holmes (~> 0.6.9.4)
+ coffee-rails (~> 4.1.0)
+ colored (~> 1.2)
+ colorize (~> 0.5.8)
coveralls (~> 0.8.2)
creole (~> 0.3.6)
d3_rails (~> 3.5.5)
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
- devise (= 3.2.4)
- devise-async (= 0.9.0)
- devise-two-factor
+ devise (~> 3.2.4)
+ devise-async (~> 0.9.0)
+ devise-two-factor (~> 1.0.1)
diffy (~> 3.0.3)
- doorkeeper (= 2.1.3)
- dropzonejs-rails
- email_reply_parser
+ doorkeeper (~> 2.1.3)
+ dropzonejs-rails (~> 0.7.1)
+ email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
- enumerize
- factory_girl_rails
+ enumerize (~> 0.7.0)
+ factory_girl_rails (~> 4.3.0)
ffaker (~> 2.0.0)
fog (~> 1.25.0)
font-awesome-rails (~> 4.2)
foreman
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
- github-markup
+ github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-linguist (~> 3.0.1)
gitlab_emoji (~> 0.1)
gitlab_git (~> 7.2.15)
gitlab_meta (= 7.0)
- gitlab_omniauth-ldap (= 1.2.1)
+ gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.0.2)
gon (~> 5.0.0)
grape (~> 0.6.1)
grape-entity (~> 0.4.2)
- haml-rails
+ growl
+ guard-rspec (~> 4.2.0)
+ haml-rails (~> 0.5.3)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
- httparty
+ httparty (~> 0.13.3)
jquery-atwho-rails (~> 1.0.0)
- jquery-rails (= 3.1.3)
- jquery-scrollto-rails
- jquery-turbolinks
- jquery-ui-rails
+ jquery-rails (~> 3.1.3)
+ jquery-scrollto-rails (~> 1.4.3)
+ jquery-turbolinks (~> 2.0.1)
+ jquery-ui-rails (~> 4.2.1)
kaminari (~> 0.15.1)
- letter_opener
+ letter_opener (~> 1.1.2)
mail_room (~> 0.5.1)
- minitest (~> 5.3.0)
- mousetrap-rails
- mysql2
- newrelic_rpm
- nprogress-rails
- octokit (= 3.7.0)
+ minitest (~> 5.7.0)
+ mousetrap-rails (~> 1.4.6)
+ mysql2 (~> 0.3.16)
+ nested_form (~> 0.3.2)
+ newrelic_rpm (~> 3.9.4.245)
+ nprogress-rails (~> 0.1.2.3)
+ oauth2 (~> 1.0.0)
+ octokit (~> 3.7.0)
omniauth (~> 1.2.2)
- omniauth-bitbucket
- omniauth-github
- omniauth-gitlab
- omniauth-google-oauth2
- omniauth-kerberos
+ omniauth-bitbucket (~> 0.0.2)
+ omniauth-github (~> 1.1.1)
+ omniauth-gitlab (~> 1.0.0)
+ omniauth-google-oauth2 (~> 0.2.5)
+ omniauth-kerberos (~> 0.2.0)
omniauth-saml (~> 1.4.0)
- omniauth-shibboleth
- omniauth-twitter
+ omniauth-shibboleth (~> 1.1.1)
+ omniauth-twitter (~> 1.0.1)
omniauth_crowd
- org-ruby (= 0.9.12)
- pg
+ org-ruby (~> 0.9.12)
+ paranoia (~> 2.0)
+ pg (~> 0.18.2)
poltergeist (~> 1.6.0)
pry-rails
- quiet_assets (~> 1.0.1)
+ quiet_assets (~> 1.0.2)
rack-attack (~> 4.3.0)
- rack-cors
- rack-mini-profiler
+ rack-cors (~> 0.2.9)
+ rack-mini-profiler (~> 0.9.0)
rack-oauth2 (~> 1.0.5)
- rails (= 4.1.11)
+ rails (= 4.1.12)
raphael-rails (~> 2.1.2)
+ rb-fsevent
+ rb-inotify
rdoc (~> 3.6)
redcarpet (~> 3.3.2)
- redis-rails
- request_store
+ redis-rails (~> 4.0.0)
+ request_store (~> 1.2.0)
rerun (~> 0.10.0)
- rqrcode-rails3
+ rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
rubocop (= 0.28.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
sass-rails (~> 4.0.5)
- sdoc
- seed-fu
+ sdoc (~> 0.3.20)
+ seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- settingslogic
+ settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 3.3)
- sidetiq (= 0.6.3)
- simplecov
- sinatra
- six
+ sidekiq (= 3.3.0)
+ sidetiq (~> 0.6.3)
+ simplecov (~> 0.10.0)
+ sinatra (~> 1.4.4)
+ six (~> 0.2.0)
slack-notifier (~> 1.0.0)
- slim
- spinach-rails
- spring (~> 1.3.1)
- spring-commands-rspec (~> 1.0.0)
+ slim (~> 2.0.2)
+ spinach-rails (~> 0.2.1)
+ spring (~> 1.3.6)
+ spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 2.12.3)
- stamp
- state_machine
- task_list (= 1.0.2)
+ stamp (~> 0.5.0)
+ state_machine (~> 1.2.0)
+ task_list (~> 1.0.2)
teaspoon (~> 1.0.0)
- teaspoon-jasmine
- test_after_commit
- thin
+ teaspoon-jasmine (~> 2.2.0)
+ test_after_commit (~> 0.2.2)
+ thin (~> 1.6.1)
tinder (~> 1.9.2)
turbolinks (~> 2.5.0)
- uglifier
+ uglifier (~> 2.3.2)
underscore-rails (~> 1.4.4)
- unf
- unicorn (~> 4.6.3)
- unicorn-worker-killer
- version_sorter
- virtus
+ unf (~> 0.1.4)
+ unicorn (~> 4.8.2)
+ unicorn-worker-killer (~> 0.4.2)
+ version_sorter (~> 2.0.0)
+ virtus (~> 1.0.1)
webmock (~> 1.21.0)
+ whenever (~> 0.8.4)
wikicloth (= 0.8.1)
BUNDLED WITH
diff --git a/Procfile b/Procfile
index 18fd9eb3d92..08880b9c425 100644
--- a/Procfile
+++ b/Procfile
@@ -1,3 +1,3 @@
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
-worker: 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
+worker: 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
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg
new file mode 100644
index 00000000000..0e05674e840
--- /dev/null
+++ b/app/assets/images/ci/arch.jpg
Binary files differ
diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico
new file mode 100644
index 00000000000..9663d4d00b9
--- /dev/null
+++ b/app/assets/images/ci/favicon.ico
Binary files differ
diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif
new file mode 100644
index 00000000000..2fcb8f2da0d
--- /dev/null
+++ b/app/assets/images/ci/loader.gif
Binary files differ
diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png
new file mode 100644
index 00000000000..752d26adba7
--- /dev/null
+++ b/app/assets/images/ci/no_avatar.png
Binary files differ
diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png
new file mode 100644
index 00000000000..d5edc04e65f
--- /dev/null
+++ b/app/assets/images/ci/rails.png
Binary files differ
diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png
new file mode 100644
index 00000000000..65d29e3fd89
--- /dev/null
+++ b/app/assets/images/ci/service_sample.png
Binary files differ
diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js
new file mode 100644
index 00000000000..ab635881087
--- /dev/null
+++ b/app/assets/javascripts/ci/Chart.min.js
@@ -0,0 +1,39 @@
+var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a=
+Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&&
+isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c?
+b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)?
+0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1==
+a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*
+Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-0.5*e*Math.pow(2,10*
+(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)*
+a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0,
+scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",
+animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",
+scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a,
+c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,
+onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,
+pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",
+scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]);
+d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,
+m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+
+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(),
+c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE;
+h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/
+a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&&
+(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+
+1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;g<a.datasets.length;g++){b.beginPath();
+b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1*
+v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth=
+c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&
+(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=
+0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;
+for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d],
+0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,
+b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5),
+e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a,
+c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]<
+h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?h:d;d+=10}r=q-d-t;m=
+Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+
+d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*
+k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath();
+b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}}; \ No newline at end of file
diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee
new file mode 100644
index 00000000000..05aa0f366bb
--- /dev/null
+++ b/app/assets/javascripts/ci/application.js.coffee
@@ -0,0 +1,40 @@
+# This is a manifest file that'll be compiled into application.js, which will include all the files
+# listed below.
+#
+# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
+# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
+#
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
+# GO AFTER THE REQUIRES BELOW.
+#
+#= require pager
+#= require jquery_nested_form
+#= require_tree .
+#
+$(document).on 'click', '.edit-runner-link', (event) ->
+ event.preventDefault()
+
+ descr = $(this).closest('.runner-description').first()
+ descr.addClass('hide')
+ form = descr.next('.runner-description-form')
+ descrInput = form.find('input.description')
+ originalValue = descrInput.val()
+ form.removeClass('hide')
+ form.find('.cancel').on 'click', (event) ->
+ event.preventDefault()
+
+ form.addClass('hide')
+ descrInput.val(originalValue)
+ descr.removeClass('hide')
+
+$(document).on 'click', '.assign-all-runner', ->
+ $(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..')
+
+window.unbindEvents = ->
+ $(document).unbind('scroll')
+ $(document).off('scroll')
+
+document.addEventListener("page:fetch", unbindEvents)
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
new file mode 100644
index 00000000000..c30859b484b
--- /dev/null
+++ b/app/assets/javascripts/ci/build.coffee
@@ -0,0 +1,41 @@
+class CiBuild
+ @interval: null
+
+ constructor: (build_url, build_status) ->
+ clearInterval(CiBuild.interval)
+
+ if build_status == "running" || build_status == "pending"
+ #
+ # Bind autoscroll button to follow build output
+ #
+ $("#autoscroll-button").bind "click", ->
+ state = $(this).data("state")
+ if "enabled" is state
+ $(this).data "state", "disabled"
+ $(this).text "enable autoscroll"
+ else
+ $(this).data "state", "enabled"
+ $(this).text "disable autoscroll"
+
+ #
+ # Check for new build output if user still watching build page
+ # Only valid for runnig build when output changes during time
+ #
+ CiBuild.interval = setInterval =>
+ if window.location.href is build_url
+ $.ajax
+ url: build_url
+ dataType: "json"
+ success: (build) =>
+ if build.status == "running"
+ $('#build-trace code').html build.trace_html
+ $('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
+ @checkAutoscroll()
+ else
+ Turbolinks.visit build_url
+ , 4000
+
+ checkAutoscroll: ->
+ $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
+
+@CiBuild = CiBuild
diff --git a/app/assets/javascripts/ci/pager.js.coffee b/app/assets/javascripts/ci/pager.js.coffee
new file mode 100644
index 00000000000..226fbd654ab
--- /dev/null
+++ b/app/assets/javascripts/ci/pager.js.coffee
@@ -0,0 +1,42 @@
+@CiPager =
+ init: (@url, @limit = 0, preload, @disable = false) ->
+ if preload
+ @offset = 0
+ @getItems()
+ else
+ @offset = @limit
+ @initLoadMore()
+
+ getItems: ->
+ $(".loading").show()
+ $.ajax
+ type: "GET"
+ url: @url
+ data: "limit=" + @limit + "&offset=" + @offset
+ complete: =>
+ $(".loading").hide()
+ success: (data) =>
+ CiPager.append(data.count, data.html)
+ dataType: "json"
+
+ append: (count, html) ->
+ if count > 1
+ $(".content-list").append html
+ if count == @limit
+ @offset += count
+ else
+ @disable = true
+
+ initLoadMore: ->
+ $(document).unbind('scroll')
+ $(document).endlessScroll
+ bottomPixels: 400
+ fireDelay: 1000
+ fireOnce: true
+ ceaseFire: ->
+ CiPager.disable
+
+ callback: (i) =>
+ unless $(".loading").is(':visible')
+ $(".loading").show()
+ CiPager.getItems()
diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee
new file mode 100644
index 00000000000..7e028b4e115
--- /dev/null
+++ b/app/assets/javascripts/ci/projects.js.coffee
@@ -0,0 +1,6 @@
+$(document).on 'click', '.badge-codes-toggle', ->
+ $('.badge-codes-block').toggleClass("hide")
+ return false
+
+$(document).on 'click', '.sync-now', ->
+ $(this).find('i').addClass('fa-spin')
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 46f7feddf8d..d9ede637944 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -61,3 +61,9 @@
* Styles for JS behaviors.
*/
@import "behaviors.scss";
+
+/**
+ * CI specific styles:
+ */
+@import "ci/**/*";
+
diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/ci/builds.scss
new file mode 100644
index 00000000000..a11a935b54d
--- /dev/null
+++ b/app/assets/stylesheets/ci/builds.scss
@@ -0,0 +1,70 @@
+.ci-body {
+ pre.trace {
+ background: #111111;
+ color: #fff;
+ font-family: $monospace_font;
+ white-space: pre;
+ white-space: pre-wrap; /* css-3 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+ overflow: auto;
+ overflow-y: hidden;
+ font-size: 12px;
+
+ .fa-refresh {
+ font-size: 24px;
+ margin-left: 20px;
+ }
+ }
+
+ .autoscroll-container {
+ position: fixed;
+ bottom: 10px;
+ right: 20px;
+ z-index: 100;
+ }
+
+ .scroll-controls {
+ position: fixed;
+ bottom: 10px;
+ left: 250px;
+ z-index: 100;
+
+ a {
+ display: block;
+ margin-bottom: 5px;
+ }
+ }
+
+ .page-sidebar-collapsed {
+ .scroll-controls {
+ left: 70px;
+ }
+ }
+
+ .build-widget {
+ padding: 10px;
+ background: $background-color;
+ margin-bottom: 20px;
+ border-radius: 4px;
+
+ .title {
+ margin-top: 0;
+ color: #666;
+ line-height: 1.5;
+ }
+ .attr-name {
+ color: #777;
+ }
+ }
+
+ .alert-disabled {
+ background: $background-color;
+
+ a {
+ color: #3084bb !important;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/ci/lint.scss b/app/assets/stylesheets/ci/lint.scss
new file mode 100644
index 00000000000..6d2bd33b28b
--- /dev/null
+++ b/app/assets/stylesheets/ci/lint.scss
@@ -0,0 +1,10 @@
+.ci-body {
+ .incorrect-syntax{
+ font-size: 19px;
+ color: red;
+ }
+ .correct-syntax{
+ font-size: 19px;
+ color: #47a447;
+ }
+}
diff --git a/app/assets/stylesheets/ci/projects.scss b/app/assets/stylesheets/ci/projects.scss
new file mode 100644
index 00000000000..b246fb9e07d
--- /dev/null
+++ b/app/assets/stylesheets/ci/projects.scss
@@ -0,0 +1,56 @@
+.ci-body {
+ .project-title {
+ margin: 0;
+ color: #444;
+ font-size: 20px;
+ line-height: 1.5;
+ }
+
+ .builds {
+ @extend .table;
+
+ .build {
+ &.alert{
+ margin-bottom: 6px;
+ }
+ }
+ }
+
+ .projects-table {
+ td {
+ vertical-align: middle !important;
+ }
+ }
+
+ .commit-info {
+ font-size: 14px;
+
+ .attr-name {
+ font-weight: 300;
+ color: #666;
+ margin-right: 5px;
+ }
+
+ pre.commit-message {
+ font-size: 14px;
+ background: none;
+ padding: 0;
+ margin: 0;
+ border: none;
+ margin: 20px 0;
+ border-bottom: 1px solid #EEE;
+ padding-bottom: 20px;
+ border-radius: 0;
+ }
+ }
+
+ .loading{
+ font-size: 20px;
+ }
+
+ .ci-charts {
+ fieldset {
+ margin-bottom: 16px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/ci/runners.scss b/app/assets/stylesheets/ci/runners.scss
new file mode 100644
index 00000000000..2b15ab83129
--- /dev/null
+++ b/app/assets/stylesheets/ci/runners.scss
@@ -0,0 +1,36 @@
+.ci-body {
+ .runner-state {
+ padding: 6px 12px;
+ margin-right: 10px;
+ color: #FFF;
+
+ &.runner-state-shared {
+ background: #32b186;
+ }
+ &.runner-state-specific {
+ background: #3498db;
+ }
+ }
+
+ .runner-status-online {
+ color: green;
+ }
+
+ .runner-status-offline {
+ color: gray;
+ }
+
+ .runner-status-paused {
+ color: red;
+ }
+
+ .runner {
+ .btn {
+ padding: 1px 6px;
+ }
+
+ h4 {
+ font-weight: normal;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/ci/xterm.scss
new file mode 100644
index 00000000000..532dede0b23
--- /dev/null
+++ b/app/assets/stylesheets/ci/xterm.scss
@@ -0,0 +1,906 @@
+.ci-body {
+ // color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
+ // see also: https://gist.github.com/jasonm23/2868981
+
+ $black: #000000;
+ $red: #cd0000;
+ $green: #00cd00;
+ $yellow: #cdcd00;
+ $blue: #0000ee; // according to wikipedia, this is the xterm standard
+ //$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile)
+ $magenta: #cd00cd;
+ $cyan: #00cdcd;
+ $white: #e5e5e5;
+ $l-black: #7f7f7f;
+ $l-red: #ff0000;
+ $l-green: #00ff00;
+ $l-yellow: #ffff00;
+ $l-blue: #5c5cff;
+ $l-magenta: #ff00ff;
+ $l-cyan: #00ffff;
+ $l-white: #ffffff;
+
+ .term-bold {
+ font-weight: bold;
+ }
+ .term-italic {
+ font-style: italic;
+ }
+ .term-conceal {
+ visibility: hidden;
+ }
+ .term-underline {
+ text-decoration: underline;
+ }
+ .term-cross {
+ text-decoration: line-through;
+ }
+
+ .term-fg-black {
+ color: $black;
+ }
+ .term-fg-red {
+ color: $red;
+ }
+ .term-fg-green {
+ color: $green;
+ }
+ .term-fg-yellow {
+ color: $yellow;
+ }
+ .term-fg-blue {
+ color: $blue;
+ }
+ .term-fg-magenta {
+ color: $magenta;
+ }
+ .term-fg-cyan {
+ color: $cyan;
+ }
+ .term-fg-white {
+ color: $white;
+ }
+ .term-fg-l-black {
+ color: $l-black;
+ }
+ .term-fg-l-red {
+ color: $l-red;
+ }
+ .term-fg-l-green {
+ color: $l-green;
+ }
+ .term-fg-l-yellow {
+ color: $l-yellow;
+ }
+ .term-fg-l-blue {
+ color: $l-blue;
+ }
+ .term-fg-l-magenta {
+ color: $l-magenta;
+ }
+ .term-fg-l-cyan {
+ color: $l-cyan;
+ }
+ .term-fg-l-white {
+ color: $l-white;
+ }
+
+ .term-bg-black {
+ background-color: $black;
+ }
+ .term-bg-red {
+ background-color: $red;
+ }
+ .term-bg-green {
+ background-color: $green;
+ }
+ .term-bg-yellow {
+ background-color: $yellow;
+ }
+ .term-bg-blue {
+ background-color: $blue;
+ }
+ .term-bg-magenta {
+ background-color: $magenta;
+ }
+ .term-bg-cyan {
+ background-color: $cyan;
+ }
+ .term-bg-white {
+ background-color: $white;
+ }
+ .term-bg-l-black {
+ background-color: $l-black;
+ }
+ .term-bg-l-red {
+ background-color: $l-red;
+ }
+ .term-bg-l-green {
+ background-color: $l-green;
+ }
+ .term-bg-l-yellow {
+ background-color: $l-yellow;
+ }
+ .term-bg-l-blue {
+ background-color: $l-blue;
+ }
+ .term-bg-l-magenta {
+ background-color: $l-magenta;
+ }
+ .term-bg-l-cyan {
+ background-color: $l-cyan;
+ }
+ .term-bg-l-white {
+ background-color: $l-white;
+ }
+
+
+ .xterm-fg-0 {
+ color: #000000;
+ }
+ .xterm-fg-1 {
+ color: #800000;
+ }
+ .xterm-fg-2 {
+ color: #008000;
+ }
+ .xterm-fg-3 {
+ color: #808000;
+ }
+ .xterm-fg-4 {
+ color: #000080;
+ }
+ .xterm-fg-5 {
+ color: #800080;
+ }
+ .xterm-fg-6 {
+ color: #008080;
+ }
+ .xterm-fg-7 {
+ color: #c0c0c0;
+ }
+ .xterm-fg-8 {
+ color: #808080;
+ }
+ .xterm-fg-9 {
+ color: #ff0000;
+ }
+ .xterm-fg-10 {
+ color: #00ff00;
+ }
+ .xterm-fg-11 {
+ color: #ffff00;
+ }
+ .xterm-fg-12 {
+ color: #0000ff;
+ }
+ .xterm-fg-13 {
+ color: #ff00ff;
+ }
+ .xterm-fg-14 {
+ color: #00ffff;
+ }
+ .xterm-fg-15 {
+ color: #ffffff;
+ }
+ .xterm-fg-16 {
+ color: #000000;
+ }
+ .xterm-fg-17 {
+ color: #00005f;
+ }
+ .xterm-fg-18 {
+ color: #000087;
+ }
+ .xterm-fg-19 {
+ color: #0000af;
+ }
+ .xterm-fg-20 {
+ color: #0000d7;
+ }
+ .xterm-fg-21 {
+ color: #0000ff;
+ }
+ .xterm-fg-22 {
+ color: #005f00;
+ }
+ .xterm-fg-23 {
+ color: #005f5f;
+ }
+ .xterm-fg-24 {
+ color: #005f87;
+ }
+ .xterm-fg-25 {
+ color: #005faf;
+ }
+ .xterm-fg-26 {
+ color: #005fd7;
+ }
+ .xterm-fg-27 {
+ color: #005fff;
+ }
+ .xterm-fg-28 {
+ color: #008700;
+ }
+ .xterm-fg-29 {
+ color: #00875f;
+ }
+ .xterm-fg-30 {
+ color: #008787;
+ }
+ .xterm-fg-31 {
+ color: #0087af;
+ }
+ .xterm-fg-32 {
+ color: #0087d7;
+ }
+ .xterm-fg-33 {
+ color: #0087ff;
+ }
+ .xterm-fg-34 {
+ color: #00af00;
+ }
+ .xterm-fg-35 {
+ color: #00af5f;
+ }
+ .xterm-fg-36 {
+ color: #00af87;
+ }
+ .xterm-fg-37 {
+ color: #00afaf;
+ }
+ .xterm-fg-38 {
+ color: #00afd7;
+ }
+ .xterm-fg-39 {
+ color: #00afff;
+ }
+ .xterm-fg-40 {
+ color: #00d700;
+ }
+ .xterm-fg-41 {
+ color: #00d75f;
+ }
+ .xterm-fg-42 {
+ color: #00d787;
+ }
+ .xterm-fg-43 {
+ color: #00d7af;
+ }
+ .xterm-fg-44 {
+ color: #00d7d7;
+ }
+ .xterm-fg-45 {
+ color: #00d7ff;
+ }
+ .xterm-fg-46 {
+ color: #00ff00;
+ }
+ .xterm-fg-47 {
+ color: #00ff5f;
+ }
+ .xterm-fg-48 {
+ color: #00ff87;
+ }
+ .xterm-fg-49 {
+ color: #00ffaf;
+ }
+ .xterm-fg-50 {
+ color: #00ffd7;
+ }
+ .xterm-fg-51 {
+ color: #00ffff;
+ }
+ .xterm-fg-52 {
+ color: #5f0000;
+ }
+ .xterm-fg-53 {
+ color: #5f005f;
+ }
+ .xterm-fg-54 {
+ color: #5f0087;
+ }
+ .xterm-fg-55 {
+ color: #5f00af;
+ }
+ .xterm-fg-56 {
+ color: #5f00d7;
+ }
+ .xterm-fg-57 {
+ color: #5f00ff;
+ }
+ .xterm-fg-58 {
+ color: #5f5f00;
+ }
+ .xterm-fg-59 {
+ color: #5f5f5f;
+ }
+ .xterm-fg-60 {
+ color: #5f5f87;
+ }
+ .xterm-fg-61 {
+ color: #5f5faf;
+ }
+ .xterm-fg-62 {
+ color: #5f5fd7;
+ }
+ .xterm-fg-63 {
+ color: #5f5fff;
+ }
+ .xterm-fg-64 {
+ color: #5f8700;
+ }
+ .xterm-fg-65 {
+ color: #5f875f;
+ }
+ .xterm-fg-66 {
+ color: #5f8787;
+ }
+ .xterm-fg-67 {
+ color: #5f87af;
+ }
+ .xterm-fg-68 {
+ color: #5f87d7;
+ }
+ .xterm-fg-69 {
+ color: #5f87ff;
+ }
+ .xterm-fg-70 {
+ color: #5faf00;
+ }
+ .xterm-fg-71 {
+ color: #5faf5f;
+ }
+ .xterm-fg-72 {
+ color: #5faf87;
+ }
+ .xterm-fg-73 {
+ color: #5fafaf;
+ }
+ .xterm-fg-74 {
+ color: #5fafd7;
+ }
+ .xterm-fg-75 {
+ color: #5fafff;
+ }
+ .xterm-fg-76 {
+ color: #5fd700;
+ }
+ .xterm-fg-77 {
+ color: #5fd75f;
+ }
+ .xterm-fg-78 {
+ color: #5fd787;
+ }
+ .xterm-fg-79 {
+ color: #5fd7af;
+ }
+ .xterm-fg-80 {
+ color: #5fd7d7;
+ }
+ .xterm-fg-81 {
+ color: #5fd7ff;
+ }
+ .xterm-fg-82 {
+ color: #5fff00;
+ }
+ .xterm-fg-83 {
+ color: #5fff5f;
+ }
+ .xterm-fg-84 {
+ color: #5fff87;
+ }
+ .xterm-fg-85 {
+ color: #5fffaf;
+ }
+ .xterm-fg-86 {
+ color: #5fffd7;
+ }
+ .xterm-fg-87 {
+ color: #5fffff;
+ }
+ .xterm-fg-88 {
+ color: #870000;
+ }
+ .xterm-fg-89 {
+ color: #87005f;
+ }
+ .xterm-fg-90 {
+ color: #870087;
+ }
+ .xterm-fg-91 {
+ color: #8700af;
+ }
+ .xterm-fg-92 {
+ color: #8700d7;
+ }
+ .xterm-fg-93 {
+ color: #8700ff;
+ }
+ .xterm-fg-94 {
+ color: #875f00;
+ }
+ .xterm-fg-95 {
+ color: #875f5f;
+ }
+ .xterm-fg-96 {
+ color: #875f87;
+ }
+ .xterm-fg-97 {
+ color: #875faf;
+ }
+ .xterm-fg-98 {
+ color: #875fd7;
+ }
+ .xterm-fg-99 {
+ color: #875fff;
+ }
+ .xterm-fg-100 {
+ color: #878700;
+ }
+ .xterm-fg-101 {
+ color: #87875f;
+ }
+ .xterm-fg-102 {
+ color: #878787;
+ }
+ .xterm-fg-103 {
+ color: #8787af;
+ }
+ .xterm-fg-104 {
+ color: #8787d7;
+ }
+ .xterm-fg-105 {
+ color: #8787ff;
+ }
+ .xterm-fg-106 {
+ color: #87af00;
+ }
+ .xterm-fg-107 {
+ color: #87af5f;
+ }
+ .xterm-fg-108 {
+ color: #87af87;
+ }
+ .xterm-fg-109 {
+ color: #87afaf;
+ }
+ .xterm-fg-110 {
+ color: #87afd7;
+ }
+ .xterm-fg-111 {
+ color: #87afff;
+ }
+ .xterm-fg-112 {
+ color: #87d700;
+ }
+ .xterm-fg-113 {
+ color: #87d75f;
+ }
+ .xterm-fg-114 {
+ color: #87d787;
+ }
+ .xterm-fg-115 {
+ color: #87d7af;
+ }
+ .xterm-fg-116 {
+ color: #87d7d7;
+ }
+ .xterm-fg-117 {
+ color: #87d7ff;
+ }
+ .xterm-fg-118 {
+ color: #87ff00;
+ }
+ .xterm-fg-119 {
+ color: #87ff5f;
+ }
+ .xterm-fg-120 {
+ color: #87ff87;
+ }
+ .xterm-fg-121 {
+ color: #87ffaf;
+ }
+ .xterm-fg-122 {
+ color: #87ffd7;
+ }
+ .xterm-fg-123 {
+ color: #87ffff;
+ }
+ .xterm-fg-124 {
+ color: #af0000;
+ }
+ .xterm-fg-125 {
+ color: #af005f;
+ }
+ .xterm-fg-126 {
+ color: #af0087;
+ }
+ .xterm-fg-127 {
+ color: #af00af;
+ }
+ .xterm-fg-128 {
+ color: #af00d7;
+ }
+ .xterm-fg-129 {
+ color: #af00ff;
+ }
+ .xterm-fg-130 {
+ color: #af5f00;
+ }
+ .xterm-fg-131 {
+ color: #af5f5f;
+ }
+ .xterm-fg-132 {
+ color: #af5f87;
+ }
+ .xterm-fg-133 {
+ color: #af5faf;
+ }
+ .xterm-fg-134 {
+ color: #af5fd7;
+ }
+ .xterm-fg-135 {
+ color: #af5fff;
+ }
+ .xterm-fg-136 {
+ color: #af8700;
+ }
+ .xterm-fg-137 {
+ color: #af875f;
+ }
+ .xterm-fg-138 {
+ color: #af8787;
+ }
+ .xterm-fg-139 {
+ color: #af87af;
+ }
+ .xterm-fg-140 {
+ color: #af87d7;
+ }
+ .xterm-fg-141 {
+ color: #af87ff;
+ }
+ .xterm-fg-142 {
+ color: #afaf00;
+ }
+ .xterm-fg-143 {
+ color: #afaf5f;
+ }
+ .xterm-fg-144 {
+ color: #afaf87;
+ }
+ .xterm-fg-145 {
+ color: #afafaf;
+ }
+ .xterm-fg-146 {
+ color: #afafd7;
+ }
+ .xterm-fg-147 {
+ color: #afafff;
+ }
+ .xterm-fg-148 {
+ color: #afd700;
+ }
+ .xterm-fg-149 {
+ color: #afd75f;
+ }
+ .xterm-fg-150 {
+ color: #afd787;
+ }
+ .xterm-fg-151 {
+ color: #afd7af;
+ }
+ .xterm-fg-152 {
+ color: #afd7d7;
+ }
+ .xterm-fg-153 {
+ color: #afd7ff;
+ }
+ .xterm-fg-154 {
+ color: #afff00;
+ }
+ .xterm-fg-155 {
+ color: #afff5f;
+ }
+ .xterm-fg-156 {
+ color: #afff87;
+ }
+ .xterm-fg-157 {
+ color: #afffaf;
+ }
+ .xterm-fg-158 {
+ color: #afffd7;
+ }
+ .xterm-fg-159 {
+ color: #afffff;
+ }
+ .xterm-fg-160 {
+ color: #d70000;
+ }
+ .xterm-fg-161 {
+ color: #d7005f;
+ }
+ .xterm-fg-162 {
+ color: #d70087;
+ }
+ .xterm-fg-163 {
+ color: #d700af;
+ }
+ .xterm-fg-164 {
+ color: #d700d7;
+ }
+ .xterm-fg-165 {
+ color: #d700ff;
+ }
+ .xterm-fg-166 {
+ color: #d75f00;
+ }
+ .xterm-fg-167 {
+ color: #d75f5f;
+ }
+ .xterm-fg-168 {
+ color: #d75f87;
+ }
+ .xterm-fg-169 {
+ color: #d75faf;
+ }
+ .xterm-fg-170 {
+ color: #d75fd7;
+ }
+ .xterm-fg-171 {
+ color: #d75fff;
+ }
+ .xterm-fg-172 {
+ color: #d78700;
+ }
+ .xterm-fg-173 {
+ color: #d7875f;
+ }
+ .xterm-fg-174 {
+ color: #d78787;
+ }
+ .xterm-fg-175 {
+ color: #d787af;
+ }
+ .xterm-fg-176 {
+ color: #d787d7;
+ }
+ .xterm-fg-177 {
+ color: #d787ff;
+ }
+ .xterm-fg-178 {
+ color: #d7af00;
+ }
+ .xterm-fg-179 {
+ color: #d7af5f;
+ }
+ .xterm-fg-180 {
+ color: #d7af87;
+ }
+ .xterm-fg-181 {
+ color: #d7afaf;
+ }
+ .xterm-fg-182 {
+ color: #d7afd7;
+ }
+ .xterm-fg-183 {
+ color: #d7afff;
+ }
+ .xterm-fg-184 {
+ color: #d7d700;
+ }
+ .xterm-fg-185 {
+ color: #d7d75f;
+ }
+ .xterm-fg-186 {
+ color: #d7d787;
+ }
+ .xterm-fg-187 {
+ color: #d7d7af;
+ }
+ .xterm-fg-188 {
+ color: #d7d7d7;
+ }
+ .xterm-fg-189 {
+ color: #d7d7ff;
+ }
+ .xterm-fg-190 {
+ color: #d7ff00;
+ }
+ .xterm-fg-191 {
+ color: #d7ff5f;
+ }
+ .xterm-fg-192 {
+ color: #d7ff87;
+ }
+ .xterm-fg-193 {
+ color: #d7ffaf;
+ }
+ .xterm-fg-194 {
+ color: #d7ffd7;
+ }
+ .xterm-fg-195 {
+ color: #d7ffff;
+ }
+ .xterm-fg-196 {
+ color: #ff0000;
+ }
+ .xterm-fg-197 {
+ color: #ff005f;
+ }
+ .xterm-fg-198 {
+ color: #ff0087;
+ }
+ .xterm-fg-199 {
+ color: #ff00af;
+ }
+ .xterm-fg-200 {
+ color: #ff00d7;
+ }
+ .xterm-fg-201 {
+ color: #ff00ff;
+ }
+ .xterm-fg-202 {
+ color: #ff5f00;
+ }
+ .xterm-fg-203 {
+ color: #ff5f5f;
+ }
+ .xterm-fg-204 {
+ color: #ff5f87;
+ }
+ .xterm-fg-205 {
+ color: #ff5faf;
+ }
+ .xterm-fg-206 {
+ color: #ff5fd7;
+ }
+ .xterm-fg-207 {
+ color: #ff5fff;
+ }
+ .xterm-fg-208 {
+ color: #ff8700;
+ }
+ .xterm-fg-209 {
+ color: #ff875f;
+ }
+ .xterm-fg-210 {
+ color: #ff8787;
+ }
+ .xterm-fg-211 {
+ color: #ff87af;
+ }
+ .xterm-fg-212 {
+ color: #ff87d7;
+ }
+ .xterm-fg-213 {
+ color: #ff87ff;
+ }
+ .xterm-fg-214 {
+ color: #ffaf00;
+ }
+ .xterm-fg-215 {
+ color: #ffaf5f;
+ }
+ .xterm-fg-216 {
+ color: #ffaf87;
+ }
+ .xterm-fg-217 {
+ color: #ffafaf;
+ }
+ .xterm-fg-218 {
+ color: #ffafd7;
+ }
+ .xterm-fg-219 {
+ color: #ffafff;
+ }
+ .xterm-fg-220 {
+ color: #ffd700;
+ }
+ .xterm-fg-221 {
+ color: #ffd75f;
+ }
+ .xterm-fg-222 {
+ color: #ffd787;
+ }
+ .xterm-fg-223 {
+ color: #ffd7af;
+ }
+ .xterm-fg-224 {
+ color: #ffd7d7;
+ }
+ .xterm-fg-225 {
+ color: #ffd7ff;
+ }
+ .xterm-fg-226 {
+ color: #ffff00;
+ }
+ .xterm-fg-227 {
+ color: #ffff5f;
+ }
+ .xterm-fg-228 {
+ color: #ffff87;
+ }
+ .xterm-fg-229 {
+ color: #ffffaf;
+ }
+ .xterm-fg-230 {
+ color: #ffffd7;
+ }
+ .xterm-fg-231 {
+ color: #ffffff;
+ }
+ .xterm-fg-232 {
+ color: #080808;
+ }
+ .xterm-fg-233 {
+ color: #121212;
+ }
+ .xterm-fg-234 {
+ color: #1c1c1c;
+ }
+ .xterm-fg-235 {
+ color: #262626;
+ }
+ .xterm-fg-236 {
+ color: #303030;
+ }
+ .xterm-fg-237 {
+ color: #3a3a3a;
+ }
+ .xterm-fg-238 {
+ color: #444444;
+ }
+ .xterm-fg-239 {
+ color: #4e4e4e;
+ }
+ .xterm-fg-240 {
+ color: #585858;
+ }
+ .xterm-fg-241 {
+ color: #626262;
+ }
+ .xterm-fg-242 {
+ color: #6c6c6c;
+ }
+ .xterm-fg-243 {
+ color: #767676;
+ }
+ .xterm-fg-244 {
+ color: #808080;
+ }
+ .xterm-fg-245 {
+ color: #8a8a8a;
+ }
+ .xterm-fg-246 {
+ color: #949494;
+ }
+ .xterm-fg-247 {
+ color: #9e9e9e;
+ }
+ .xterm-fg-248 {
+ color: #a8a8a8;
+ }
+ .xterm-fg-249 {
+ color: #b2b2b2;
+ }
+ .xterm-fg-250 {
+ color: #bcbcbc;
+ }
+ .xterm-fg-251 {
+ color: #c6c6c6;
+ }
+ .xterm-fg-252 {
+ color: #d0d0d0;
+ }
+ .xterm-fg-253 {
+ color: #dadada;
+ }
+ .xterm-fg-254 {
+ color: #e4e4e4;
+ }
+ .xterm-fg-255 {
+ color: #eeeeee;
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 4c112534ae6..9b6472a7b13 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -134,9 +134,6 @@ class ApplicationController < ActionController::Base
def repository
@repository ||= project.repository
- rescue Grit::NoSuchPathError => e
- log_exception(e)
- nil
end
def authorize_project!(action)
diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb
new file mode 100644
index 00000000000..4ec2dc9c2cf
--- /dev/null
+++ b/app/controllers/ci/admin/application_controller.rb
@@ -0,0 +1,10 @@
+module Ci
+ module Admin
+ class ApplicationController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :authenticate_admin!
+
+ layout "ci/admin"
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb
new file mode 100644
index 00000000000..71e253fac67
--- /dev/null
+++ b/app/controllers/ci/admin/application_settings_controller.rb
@@ -0,0 +1,31 @@
+module Ci
+ class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController
+ before_action :set_application_setting
+
+ def show
+ end
+
+ def update
+ if @application_setting.update_attributes(application_setting_params)
+ redirect_to ci_admin_application_settings_path,
+ notice: 'Application settings saved successfully'
+ else
+ render :show
+ end
+ end
+
+ private
+
+ def set_application_setting
+ @application_setting = Ci::ApplicationSetting.current
+ @application_setting ||= Ci::ApplicationSetting.create_from_defaults
+ end
+
+ def application_setting_params
+ params.require(:application_setting).permit(
+ :all_broken_builds,
+ :add_pusher,
+ )
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb
new file mode 100644
index 00000000000..38abfdeafbf
--- /dev/null
+++ b/app/controllers/ci/admin/builds_controller.rb
@@ -0,0 +1,18 @@
+module Ci
+ class Admin::BuildsController < Ci::Admin::ApplicationController
+ def index
+ @scope = params[:scope]
+ @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30)
+
+ @builds =
+ case @scope
+ when "pending"
+ @builds.pending
+ when "running"
+ @builds.running
+ else
+ @builds
+ end
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb
new file mode 100644
index 00000000000..5939efff980
--- /dev/null
+++ b/app/controllers/ci/admin/events_controller.rb
@@ -0,0 +1,9 @@
+module Ci
+ class Admin::EventsController < Ci::Admin::ApplicationController
+ EVENTS_PER_PAGE = 50
+
+ def index
+ @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE)
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb
new file mode 100644
index 00000000000..5bbd0ce7396
--- /dev/null
+++ b/app/controllers/ci/admin/projects_controller.rb
@@ -0,0 +1,19 @@
+module Ci
+ class Admin::ProjectsController < Ci::Admin::ApplicationController
+ def index
+ @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30)
+ end
+
+ def destroy
+ project.destroy
+
+ redirect_to ci_projects_url
+ end
+
+ protected
+
+ def project
+ @project ||= Ci::Project.find(params[:id])
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb
new file mode 100644
index 00000000000..e7de6eb12ca
--- /dev/null
+++ b/app/controllers/ci/admin/runner_projects_controller.rb
@@ -0,0 +1,34 @@
+module Ci
+ class Admin::RunnerProjectsController < Ci::Admin::ApplicationController
+ layout 'ci/project'
+
+ def index
+ @runner_projects = project.runner_projects.all
+ @runner_project = project.runner_projects.new
+ end
+
+ def create
+ @runner = Ci::Runner.find(params[:runner_project][:runner_id])
+
+ if @runner.assign_to(project, current_user)
+ redirect_to ci_admin_runner_path(@runner)
+ else
+ redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project'
+ end
+ end
+
+ def destroy
+ rp = Ci::RunnerProject.find(params[:id])
+ runner = rp.runner
+ rp.destroy
+
+ redirect_to ci_admin_runner_path(runner)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb
new file mode 100644
index 00000000000..dc3508b49dd
--- /dev/null
+++ b/app/controllers/ci/admin/runners_controller.rb
@@ -0,0 +1,69 @@
+module Ci
+ class Admin::RunnersController < Ci::Admin::ApplicationController
+ before_action :runner, except: :index
+
+ def index
+ @runners = Ci::Runner.order('id DESC')
+ @runners = @runners.search(params[:search]) if params[:search].present?
+ @runners = @runners.page(params[:page]).per(30)
+ @active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count
+ end
+
+ def show
+ @builds = @runner.builds.order('id DESC').first(30)
+ @projects = Ci::Project.all
+ @projects = @projects.search(params[:search]) if params[:search].present?
+ @projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any?
+ @projects = @projects.page(params[:page]).per(30)
+ end
+
+ def update
+ @runner.update_attributes(runner_params)
+
+ respond_to do |format|
+ format.js
+ format.html { redirect_to ci_admin_runner_path(@runner) }
+ end
+ end
+
+ def destroy
+ @runner.destroy
+
+ redirect_to ci_admin_runners_path
+ end
+
+ def resume
+ if @runner.update_attributes(active: true)
+ redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if @runner.update_attributes(active: false)
+ redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
+ end
+ end
+
+ def assign_all
+ Ci::Project.unassigned(@runner).all.each do |project|
+ @runner.assign_to(project, current_user)
+ end
+
+ redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects"
+ end
+
+ private
+
+ def runner
+ @runner ||= Ci::Runner.find(params[:id])
+ end
+
+ def runner_params
+ params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active)
+ end
+ end
+end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
new file mode 100644
index 00000000000..a5868da377f
--- /dev/null
+++ b/app/controllers/ci/application_controller.rb
@@ -0,0 +1,76 @@
+module Ci
+ class ApplicationController < ::ApplicationController
+ def self.railtie_helpers_paths
+ "app/helpers/ci"
+ end
+
+ helper_method :gl_project
+
+ private
+
+ def authenticate_public_page!
+ unless project.public
+ unless current_user
+ redirect_to(new_user_sessions_path) and return
+ end
+
+ return access_denied! unless can?(current_user, :read_project, gl_project)
+ end
+ end
+
+ def authenticate_token!
+ unless project.valid_token?(params[:token])
+ return head(403)
+ end
+ end
+
+ def authorize_access_project!
+ unless can?(current_user, :read_project, gl_project)
+ return page_404
+ end
+ end
+
+ def authorize_manage_builds!
+ unless can?(current_user, :admin_project, gl_project)
+ return page_404
+ end
+ end
+
+ def authenticate_admin!
+ return render_404 unless current_user.is_admin?
+ end
+
+ def authorize_manage_project!
+ unless can?(current_user, :admin_project, gl_project)
+ return page_404
+ end
+ end
+
+ def page_404
+ render file: "#{Rails.root}/public/404.html", status: 404, layout: false
+ end
+
+ def default_headers
+ headers['X-Frame-Options'] = 'DENY'
+ headers['X-XSS-Protection'] = '1; mode=block'
+ end
+
+ # JSON for infinite scroll via Pager object
+ def pager_json(partial, count)
+ html = render_to_string(
+ partial,
+ layout: false,
+ formats: [:html]
+ )
+
+ render json: {
+ html: html,
+ count: count
+ }
+ end
+
+ def gl_project
+ ::Project.find(@project.gitlab_id)
+ end
+ end
+end
diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb
new file mode 100644
index 00000000000..80ee8666792
--- /dev/null
+++ b/app/controllers/ci/builds_controller.rb
@@ -0,0 +1,78 @@
+module Ci
+ class BuildsController < Ci::ApplicationController
+ before_action :authenticate_user!, except: [:status, :show]
+ before_action :authenticate_public_page!, only: :show
+ before_action :project
+ before_action :authorize_access_project!, except: [:status, :show]
+ before_action :authorize_manage_project!, except: [:status, :show, :retry, :cancel]
+ before_action :authorize_manage_builds!, only: [:retry, :cancel]
+ before_action :build, except: [:show]
+ layout 'ci/build'
+
+ def show
+ if params[:id] =~ /\A\d+\Z/
+ @build = build
+ else
+ # try to find commit by sha
+ commit = commit_by_sha
+
+ if commit
+ # Redirect to commit page
+ redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha)
+ return
+ end
+ end
+
+ raise ActiveRecord::RecordNotFound unless @build
+
+ @builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC')
+ @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20)
+ @commit = @build.commit
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @build.to_json(methods: :trace_html)
+ end
+ end
+ end
+
+ def retry
+ if @build.commands.blank?
+ return page_404
+ end
+
+ build = Ci::Build.retry(@build)
+
+ if params[:return_to]
+ redirect_to URI.parse(params[:return_to]).path
+ else
+ redirect_to ci_project_build_path(project, build)
+ end
+ end
+
+ def status
+ render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
+ end
+
+ def cancel
+ @build.cancel
+
+ redirect_to ci_project_build_path(@project, @build)
+ end
+
+ protected
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def build
+ @build ||= project.builds.unscoped.find_by(id: params[:id])
+ end
+
+ def commit_by_sha
+ @project.commits.find_by(sha: params[:id])
+ end
+ end
+end
diff --git a/app/controllers/ci/charts_controller.rb b/app/controllers/ci/charts_controller.rb
new file mode 100644
index 00000000000..aa875e70987
--- /dev/null
+++ b/app/controllers/ci/charts_controller.rb
@@ -0,0 +1,24 @@
+module Ci
+ class ChartsController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_access_project!
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def show
+ @charts = {}
+ @charts[:week] = Ci::Charts::WeekChart.new(@project)
+ @charts[:month] = Ci::Charts::MonthChart.new(@project)
+ @charts[:year] = Ci::Charts::YearChart.new(@project)
+ @charts[:build_times] = Ci::Charts::BuildTime.new(@project)
+ end
+
+ protected
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb
new file mode 100644
index 00000000000..7a0a500fbe6
--- /dev/null
+++ b/app/controllers/ci/commits_controller.rb
@@ -0,0 +1,38 @@
+module Ci
+ class CommitsController < Ci::ApplicationController
+ before_action :authenticate_user!, except: [:status, :show]
+ before_action :authenticate_public_page!, only: :show
+ before_action :project
+ before_action :authorize_access_project!, except: [:status, :show, :cancel]
+ before_action :authorize_manage_builds!, only: [:cancel]
+ before_action :commit, only: :show
+ layout 'ci/commit'
+
+ def show
+ @builds = @commit.builds
+ end
+
+ def status
+ commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
+ render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage])
+ rescue ActiveRecord::RecordNotFound
+ render json: { status: "not_found" }
+ end
+
+ def cancel
+ commit.builds.running_or_pending.each(&:cancel)
+
+ redirect_to ci_project_ref_commits_path(project, commit.ref, commit.sha)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+
+ def commit
+ @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb
new file mode 100644
index 00000000000..89b784a1e89
--- /dev/null
+++ b/app/controllers/ci/events_controller.rb
@@ -0,0 +1,21 @@
+module Ci
+ class EventsController < Ci::ApplicationController
+ EVENTS_PER_PAGE = 50
+
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/helps_controller.rb b/app/controllers/ci/helps_controller.rb
new file mode 100644
index 00000000000..a1ee4111614
--- /dev/null
+++ b/app/controllers/ci/helps_controller.rb
@@ -0,0 +1,16 @@
+module Ci
+ class HelpsController < Ci::ApplicationController
+ skip_filter :check_config
+
+ def show
+ end
+
+ def oauth2
+ if valid_config?
+ redirect_to ci_root_path
+ else
+ render layout: 'ci/empty'
+ end
+ end
+ end
+end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
new file mode 100644
index 00000000000..a81e4e319ff
--- /dev/null
+++ b/app/controllers/ci/lints_controller.rb
@@ -0,0 +1,26 @@
+module Ci
+ class LintsController < Ci::ApplicationController
+ before_action :authenticate_user!
+
+ def show
+ end
+
+ def create
+ if params[:content].blank?
+ @status = false
+ @error = "Please provide content of .gitlab-ci.yml"
+ else
+ @config_processor = Ci::GitlabCiYamlProcessor.new params[:content]
+ @stages = @config_processor.stages
+ @builds = @config_processor.builds
+ @status = true
+ end
+ rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ @error = e.message
+ @status = false
+ rescue Exception => e
+ @error = "Undefined error"
+ @status = false
+ end
+ end
+end
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
new file mode 100644
index 00000000000..6483a84ee91
--- /dev/null
+++ b/app/controllers/ci/projects_controller.rb
@@ -0,0 +1,137 @@
+module Ci
+ class ProjectsController < Ci::ApplicationController
+ PROJECTS_BATCH = 100
+
+ before_action :authenticate_user!, except: [:build, :badge, :index, :show]
+ before_action :authenticate_public_page!, only: :show
+ before_action :project, only: [:build, :integration, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
+ before_action :authorize_access_project!, except: [:build, :gitlab, :badge, :index, :show, :new, :create]
+ before_action :authorize_manage_project!, only: [:edit, :integration, :update, :destroy, :toggle_shared_runners, :dumped_yaml]
+ before_action :authenticate_token!, only: [:build]
+ before_action :no_cache, only: [:badge]
+ protect_from_forgery except: :build
+
+ layout 'ci/project', except: [:index, :gitlab]
+
+ def index
+ @projects = Ci::Project.ordered_by_last_commit_date.public_only.page(params[:page]) unless current_user
+ end
+
+ def gitlab
+ @limit, @offset = (params[:limit] || PROJECTS_BATCH).to_i, (params[:offset] || 0).to_i
+ @page = @offset == 0 ? 1 : (@offset / @limit + 1)
+
+ @gl_projects = current_user.authorized_projects
+ @gl_projects = @gl_projects.where("name LIKE ?", "%#{params[:search]}%") if params[:search]
+ @gl_projects = @gl_projects.page(@page).per(@limit)
+
+ @projects = Ci::Project.where(gitlab_id: @gl_projects.map(&:id)).ordered_by_last_commit_date
+ @total_count = @gl_projects.size
+
+ @gl_projects = @gl_projects.where.not(id: @projects.map(&:gitlab_id))
+
+ respond_to do |format|
+ format.json do
+ pager_json("ci/projects/gitlab", @total_count)
+ end
+ end
+ rescue
+ @error = 'Failed to fetch GitLab projects'
+ end
+
+ def show
+ @ref = params[:ref]
+
+ @commits = @project.commits.reverse_order
+ @commits = @commits.where(ref: @ref) if @ref
+ @commits = @commits.page(params[:page]).per(20)
+ end
+
+ def integration
+ end
+
+ def create
+ project_data = OpenStruct.new(JSON.parse(params["project"]))
+
+ unless can?(current_user, :admin_project, ::Project.find(project_data.id))
+ return redirect_to ci_root_path, alert: 'You have to have at least master role to enable CI for this project'
+ end
+
+ @project = Ci::CreateProjectService.new.execute(current_user, project_data, ci_project_url(":project_id"))
+
+ if @project.persisted?
+ redirect_to ci_project_path(@project, show_guide: true), notice: 'Project was successfully created.'
+ else
+ redirect_to :back, alert: 'Cannot save project'
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ if project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, project)
+
+ redirect_to :back, notice: 'Project was successfully updated.'
+ else
+ render action: "edit"
+ end
+ end
+
+ def destroy
+ project.gl_project.gitlab_ci_service.update_attributes(active: false)
+ project.destroy
+
+ Ci::EventService.new.remove_project(current_user, project)
+
+ redirect_to ci_projects_url
+ end
+
+ def build
+ @commit = Ci::CreateCommitService.new.execute(@project, params.dup)
+
+ if @commit && @commit.valid?
+ head 201
+ else
+ head 400
+ end
+ end
+
+ # Project status badge
+ # Image with build status for sha or ref
+ def badge
+ image = Ci::ImageForBuildService.new.execute(@project, params)
+
+ send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
+ end
+
+ def toggle_shared_runners
+ project.toggle!(:shared_runners_enabled)
+ redirect_to :back
+ end
+
+ def dumped_yaml
+ send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml'
+ end
+
+ protected
+
+ def project
+ @project ||= Ci::Project.find(params[:id])
+ end
+
+ def no_cache
+ response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
+ response.headers["Pragma"] = "no-cache"
+ response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
+ end
+
+ def project_params
+ params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build,
+ :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients,
+ :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token,
+ { variables_attributes: [:id, :key, :value, :_destroy] })
+ end
+ end
+end
diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb
new file mode 100644
index 00000000000..a8bdd5bb362
--- /dev/null
+++ b/app/controllers/ci/runner_projects_controller.rb
@@ -0,0 +1,34 @@
+module Ci
+ class RunnerProjectsController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def create
+ @runner = Ci::Runner.find(params[:runner_project][:runner_id])
+
+ return head(403) unless current_user.ci_authorized_runners.include?(@runner)
+
+ if @runner.assign_to(project, current_user)
+ redirect_to ci_project_runners_path(project)
+ else
+ redirect_to ci_project_runners_path(project), alert: 'Failed adding runner to project'
+ end
+ end
+
+ def destroy
+ runner_project = project.runner_projects.find(params[:id])
+ runner_project.destroy
+
+ redirect_to ci_project_runners_path(project)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/runners_controller.rb b/app/controllers/ci/runners_controller.rb
new file mode 100644
index 00000000000..a672370302b
--- /dev/null
+++ b/app/controllers/ci/runners_controller.rb
@@ -0,0 +1,73 @@
+module Ci
+ class RunnersController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+ before_action :authorize_access_project!
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @runners = @project.runners.order('id DESC')
+ @specific_runners =
+ Ci::Runner.specific.includes(:runner_projects).
+ where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ).
+ where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20)
+ @shared_runners = Ci::Runner.shared.active
+ @shared_runners_count = @shared_runners.count(:all)
+ end
+
+ def edit
+ end
+
+ def update
+ if @runner.update_attributes(runner_params)
+ redirect_to edit_ci_project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to edit_ci_project_runner_path(@project, @runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def destroy
+ if @runner.only_for?(@project)
+ @runner.destroy
+ end
+
+ redirect_to ci_project_runners_path(@project)
+ end
+
+ def resume
+ if @runner.update_attributes(active: true)
+ redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if @runner.update_attributes(active: false)
+ redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def show
+ end
+
+ protected
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def set_runner
+ @runner ||= @project.runners.find(params[:id])
+ end
+
+ def runner_params
+ params.require(:runner).permit(:description, :tag_list, :contacted_at, :active)
+ end
+ end
+end
diff --git a/app/controllers/ci/services_controller.rb b/app/controllers/ci/services_controller.rb
new file mode 100644
index 00000000000..52c96a34ce8
--- /dev/null
+++ b/app/controllers/ci/services_controller.rb
@@ -0,0 +1,59 @@
+module Ci
+ class ServicesController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_access_project!
+ before_action :authorize_manage_project!
+ before_action :service, only: [:edit, :update, :test]
+
+ respond_to :html
+
+ layout 'ci/project'
+
+ def index
+ @project.build_missing_services
+ @services = @project.services.reload
+ end
+
+ def edit
+ end
+
+ def update
+ if @service.update_attributes(service_params)
+ redirect_to edit_ci_project_service_path(@project, @service.to_param)
+ else
+ render 'edit'
+ end
+ end
+
+ def test
+ last_build = @project.builds.last
+
+ if @service.execute(last_build)
+ message = { notice: 'We successfully tested the service' }
+ else
+ message = { alert: 'We tried to test the service but error occurred' }
+ end
+
+ redirect_to :back, message
+ end
+
+ private
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def service
+ @service ||= @project.services.find { |service| service.to_param == params[:id] }
+ end
+
+ def service_params
+ params.require(:service).permit(
+ :type, :active, :webhook, :notify_only_broken_builds,
+ :email_recipients, :email_only_broken_builds, :email_add_pusher,
+ :hipchat_token, :hipchat_room, :hipchat_server
+ )
+ end
+ end
+end
diff --git a/app/controllers/ci/triggers_controller.rb b/app/controllers/ci/triggers_controller.rb
new file mode 100644
index 00000000000..a39cc5d3a56
--- /dev/null
+++ b/app/controllers/ci/triggers_controller.rb
@@ -0,0 +1,43 @@
+module Ci
+ class TriggersController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_access_project!
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @triggers = @project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def create
+ @trigger = @project.triggers.new
+ @trigger.save
+
+ if @trigger.valid?
+ redirect_to ci_project_triggers_path(@project)
+ else
+ @triggers = @project.triggers.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def destroy
+ trigger.destroy
+
+ redirect_to ci_project_triggers_path(@project)
+ end
+
+ private
+
+ def trigger
+ @trigger ||= @project.triggers.find(params[:id])
+ end
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+ end
+end
diff --git a/app/controllers/ci/variables_controller.rb b/app/controllers/ci/variables_controller.rb
new file mode 100644
index 00000000000..9c6c775fde8
--- /dev/null
+++ b/app/controllers/ci/variables_controller.rb
@@ -0,0 +1,33 @@
+module Ci
+ class VariablesController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_access_project!
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def show
+ end
+
+ def update
+ if project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, project)
+
+ redirect_to ci_project_variables_path(project), notice: 'Variables were successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ end
+
+ def project_params
+ params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
+ end
+ end
+end
diff --git a/app/controllers/ci/web_hooks_controller.rb b/app/controllers/ci/web_hooks_controller.rb
new file mode 100644
index 00000000000..24074a6d9ac
--- /dev/null
+++ b/app/controllers/ci/web_hooks_controller.rb
@@ -0,0 +1,53 @@
+module Ci
+ class WebHooksController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_access_project!
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def index
+ @web_hooks = @project.web_hooks
+ @web_hook = Ci::WebHook.new
+ end
+
+ def create
+ @web_hook = @project.web_hooks.new(web_hook_params)
+ @web_hook.save
+
+ if @web_hook.valid?
+ redirect_to ci_project_web_hooks_path(@project)
+ else
+ @web_hooks = @project.web_hooks.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def test
+ Ci::TestHookService.new.execute(hook, current_user)
+
+ redirect_to :back
+ end
+
+ def destroy
+ hook.destroy
+
+ redirect_to ci_project_web_hooks_path(@project)
+ end
+
+ private
+
+ def hook
+ @web_hook ||= @project.web_hooks.find(params[:id])
+ end
+
+ def project
+ @project = Ci::Project.find(params[:project_id])
+ end
+
+ def web_hook_params
+ params.require(:web_hook).permit(:url)
+ end
+ end
+end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index fc31118124b..dc22101cd5e 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -1,7 +1,7 @@
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include PageLayoutHelper
-
+
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
diff --git a/app/helpers/ci/application_helper.rb b/app/helpers/ci/application_helper.rb
new file mode 100644
index 00000000000..3198fe55f91
--- /dev/null
+++ b/app/helpers/ci/application_helper.rb
@@ -0,0 +1,140 @@
+module Ci
+ module ApplicationHelper
+ def loader_html
+ image_tag 'ci/loader.gif', alt: 'Loading'
+ end
+
+ # Navigation link helper
+ #
+ # Returns an `li` element with an 'active' class if the supplied
+ # controller(s) and/or action(s) are currently active. The content of the
+ # element is the value passed to the block.
+ #
+ # options - The options hash used to determine if the element is "active" (default: {})
+ # :controller - One or more controller names to check (optional).
+ # :action - One or more action names to check (optional).
+ # :path - A shorthand path, such as 'dashboard#index', to check (optional).
+ # :html_options - Extra options to be passed to the list element (optional).
+ # block - An optional block that will become the contents of the returned
+ # `li` element.
+ #
+ # When both :controller and :action are specified, BOTH must match in order
+ # to be marked as active. When only one is given, either can match.
+ #
+ # Examples
+ #
+ # # Assuming we're on TreeController#show
+ #
+ # # Controller matches, but action doesn't
+ # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" }
+ # # => '<li>Hello</li>'
+ #
+ # # Controller matches
+ # nav_link(controller: [:tree, :refs]) { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Shorthand path
+ # nav_link(path: 'tree#show') { "Hello" }
+ # # => '<li class="active">Hello</li>'
+ #
+ # # Supplying custom options for the list element
+ # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
+ # # => '<li class="home active">Hello</li>'
+ #
+ # Returns a list item element String
+ def nav_link(options = {}, &block)
+ if path = options.delete(:path)
+ if path.respond_to?(:each)
+ c = path.map { |p| p.split('#').first }
+ a = path.map { |p| p.split('#').last }
+ else
+ c, a, _ = path.split('#')
+ end
+ else
+ c = options.delete(:controller)
+ a = options.delete(:action)
+ end
+
+ if c && a
+ # When given both options, make sure BOTH are active
+ klass = current_controller?(*c) && current_action?(*a) ? 'active' : ''
+ else
+ # Otherwise check EITHER option
+ klass = current_controller?(*c) || current_action?(*a) ? 'active' : ''
+ end
+
+ # Add our custom class into the html_options, which may or may not exist
+ # and which may or may not already have a :class key
+ o = options.delete(:html_options) || {}
+ o[:class] ||= ''
+ o[:class] += ' ' + klass
+ o[:class].strip!
+
+ if block_given?
+ content_tag(:li, capture(&block), o)
+ else
+ content_tag(:li, nil, o)
+ end
+ end
+
+ # Check if a particular controller is the current one
+ #
+ # args - One or more controller names to check
+ #
+ # Examples
+ #
+ # # On TreeController
+ # current_controller?(:tree) # => true
+ # current_controller?(:commits) # => false
+ # current_controller?(:commits, :tree) # => true
+ def current_controller?(*args)
+ args.any? { |v| v.to_s.downcase == controller.controller_name }
+ end
+
+ # Check if a particular action is the current one
+ #
+ # args - One or more action names to check
+ #
+ # Examples
+ #
+ # # On Projects#new
+ # current_action?(:new) # => true
+ # current_action?(:create) # => false
+ # current_action?(:new, :create) # => true
+ def current_action?(*args)
+ args.any? { |v| v.to_s.downcase == action_name }
+ end
+
+ def date_from_to(from, to)
+ "#{from.to_s(:short)} - #{to.to_s(:short)}"
+ end
+
+ def body_data_page
+ path = controller.controller_path.split('/')
+ namespace = path.first if path.second
+
+ [namespace, controller.controller_name, controller.action_name].compact.join(":")
+ end
+
+ def duration_in_words(finished_at, started_at)
+ if finished_at && started_at
+ interval_in_seconds = finished_at.to_i - started_at.to_i
+ elsif started_at
+ interval_in_seconds = Time.now.to_i - started_at.to_i
+ end
+
+ time_interval_in_words(interval_in_seconds)
+ end
+
+ def time_interval_in_words(interval_in_seconds)
+ minutes = interval_in_seconds / 60
+ seconds = interval_in_seconds - minutes * 60
+
+ if minutes >= 1
+ "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}"
+ else
+ "#{pluralize(seconds, "second")}"
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb
new file mode 100644
index 00000000000..cdabdad17d2
--- /dev/null
+++ b/app/helpers/ci/builds_helper.rb
@@ -0,0 +1,41 @@
+module Ci
+ module BuildsHelper
+ def build_ref_link build
+ gitlab_ref_link build.project, build.ref
+ end
+
+ def build_compare_link build
+ gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha
+ end
+
+ def build_commit_link build
+ gitlab_commit_link build.project, build.short_sha
+ end
+
+ def build_url(build)
+ ci_project_build_url(build.project, build)
+ end
+
+ def build_status_alert_class(build)
+ if build.success?
+ 'alert-success'
+ elsif build.failed?
+ 'alert-danger'
+ elsif build.canceled?
+ 'alert-disabled'
+ else
+ 'alert-warning'
+ end
+ end
+
+ def build_icon_css_class(build)
+ if build.success?
+ 'fa-circle cgreen'
+ elsif build.failed?
+ 'fa-circle cred'
+ else
+ 'fa-circle light'
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb
new file mode 100644
index 00000000000..74de30e006e
--- /dev/null
+++ b/app/helpers/ci/commits_helper.rb
@@ -0,0 +1,39 @@
+module Ci
+ module CommitsHelper
+ def commit_status_alert_class(commit)
+ return 'alert-info' unless commit
+
+ case commit.status
+ when 'success'
+ 'alert-success'
+ when 'failed', 'canceled'
+ 'alert-danger'
+ when 'skipped'
+ 'alert-disabled'
+ else
+ 'alert-warning'
+ end
+ end
+
+ def ci_commit_path(commit)
+ ci_project_ref_commits_path(commit.project, commit.ref, commit.sha)
+ end
+
+ def commit_link(commit)
+ link_to(commit.short_sha, ci_commit_path(commit))
+ end
+
+ def truncate_first_line(message, length = 50)
+ truncate(message.each_line.first.chomp, length: length) if message
+ end
+
+ def ci_commit_title(commit)
+ content_tag :span do
+ link_to(
+ simple_sanitize(commit.project.name), ci_project_path(commit.project)
+ ) + ' @ ' +
+ gitlab_commit_link(@project, @commit.sha)
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb
new file mode 100644
index 00000000000..2b89a0ce93e
--- /dev/null
+++ b/app/helpers/ci/gitlab_helper.rb
@@ -0,0 +1,36 @@
+module Ci
+ module GitlabHelper
+ def no_turbolink
+ { :"data-no-turbolink" => "data-no-turbolink" }
+ end
+
+ def gitlab_ref_link project, ref
+ gitlab_url = project.gitlab_url.dup
+ gitlab_url << "/commits/#{ref}"
+ link_to ref, gitlab_url, no_turbolink
+ end
+
+ def gitlab_compare_link project, before, after
+ gitlab_url = project.gitlab_url.dup
+ gitlab_url << "/compare/#{before}...#{after}"
+
+ link_to "#{before}...#{after}", gitlab_url, no_turbolink
+ end
+
+ def gitlab_commit_link project, sha
+ gitlab_url = project.gitlab_url.dup
+ gitlab_url << "/commit/#{sha}"
+ link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink
+ end
+
+ def yaml_web_editor_link(project)
+ commits = project.commits
+
+ if commits.any? && commits.last.push_data[:ci_yaml_file]
+ "#{@project.gitlab_url}/edit/master/.gitlab-ci.yml"
+ else
+ "#{@project.gitlab_url}/new/master"
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/icons_helper.rb b/app/helpers/ci/icons_helper.rb
new file mode 100644
index 00000000000..be40f79e880
--- /dev/null
+++ b/app/helpers/ci/icons_helper.rb
@@ -0,0 +1,11 @@
+module Ci
+ module IconsHelper
+ def boolean_to_icon(value)
+ if value.to_s == "true"
+ content_tag :i, nil, class: 'fa fa-circle cgreen'
+ else
+ content_tag :i, nil, class: 'fa fa-power-off clgray'
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb
new file mode 100644
index 00000000000..fd991a4165a
--- /dev/null
+++ b/app/helpers/ci/projects_helper.rb
@@ -0,0 +1,36 @@
+module Ci
+ module ProjectsHelper
+ def ref_tab_class ref = nil
+ 'active' if ref == @ref
+ end
+
+ def success_ratio(success_builds, failed_builds)
+ failed_builds = failed_builds.count(:all)
+ success_builds = success_builds.count(:all)
+
+ return 100 if failed_builds.zero?
+
+ ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100
+ ratio.to_i
+ end
+
+ def markdown_badge_code(project, ref)
+ url = status_ci_project_url(project, ref: ref, format: 'png')
+ "[![build status](#{url})](#{ci_project_url(project, ref: ref)})"
+ end
+
+ def html_badge_code(project, ref)
+ url = status_ci_project_url(project, ref: ref, format: 'png')
+ "<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>"
+ end
+
+ def project_uses_specific_runner?(project)
+ project.runners.any?
+ end
+
+ def no_runners_for_project?(project)
+ project.runners.blank? &&
+ Ci::Runner.shared.blank?
+ end
+ end
+end
diff --git a/app/helpers/ci/routes_helper.rb b/app/helpers/ci/routes_helper.rb
new file mode 100644
index 00000000000..42cd54b064f
--- /dev/null
+++ b/app/helpers/ci/routes_helper.rb
@@ -0,0 +1,29 @@
+module Ci
+ module RoutesHelper
+ class Base
+ include Gitlab::Application.routes.url_helpers
+
+ def default_url_options
+ {
+ host: Settings.gitlab['host'],
+ protocol: Settings.gitlab['https'] ? "https" : "http",
+ port: Settings.gitlab['port']
+ }
+ end
+ end
+
+ def url_helpers
+ @url_helpers ||= Base.new
+ end
+
+ def self.method_missing(method, *args, &block)
+ @url_helpers ||= Base.new
+
+ if @url_helpers.respond_to?(method)
+ @url_helpers.send(method, *args, &block)
+ else
+ super method, *args, &block
+ end
+ end
+ end
+end
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
new file mode 100644
index 00000000000..03c9914641e
--- /dev/null
+++ b/app/helpers/ci/runners_helper.rb
@@ -0,0 +1,22 @@
+module Ci
+ module RunnersHelper
+ def runner_status_icon(runner)
+ unless runner.contacted_at
+ return content_tag :i, nil,
+ class: "fa fa-warning-sign",
+ title: "New runner. Has not connected yet"
+ end
+
+ status =
+ if runner.active?
+ runner.contacted_at > 3.hour.ago ? :online : :offline
+ else
+ :paused
+ end
+
+ content_tag :i, nil,
+ class: "fa fa-circle runner-status-#{status}",
+ title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
+ end
+ end
+end
diff --git a/app/helpers/ci/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb
new file mode 100644
index 00000000000..0d2438928ce
--- /dev/null
+++ b/app/helpers/ci/triggers_helper.rb
@@ -0,0 +1,7 @@
+module Ci
+ module TriggersHelper
+ def ci_build_trigger_url(project_id, ref_name)
+ "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
+ end
+ end
+end
diff --git a/app/helpers/ci/user_helper.rb b/app/helpers/ci/user_helper.rb
new file mode 100644
index 00000000000..c332d6ed9cf
--- /dev/null
+++ b/app/helpers/ci/user_helper.rb
@@ -0,0 +1,15 @@
+module Ci
+ module UserHelper
+ def user_avatar_url(user = nil, size = nil, default = 'identicon')
+ size = 40 if size.nil? || size <= 0
+
+ if user.blank? || user.avatar_url.blank?
+ 'ci/no_avatar.png'
+ elsif /^(http(s?):\/\/(www|secure)\.gravatar\.com\/avatar\/(\w*))/ =~ user.avatar_url
+ Regexp.last_match[0] + "?s=#{size}&d=#{default}"
+ else
+ user.avatar_url
+ end
+ end
+ end
+end
diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb
new file mode 100644
index 00000000000..6fb4fba85e5
--- /dev/null
+++ b/app/mailers/ci/emails/builds.rb
@@ -0,0 +1,17 @@
+module Ci
+ module Emails
+ module Builds
+ def build_fail_email(build_id, to)
+ @build = Ci::Build.find(build_id)
+ @project = @build.project
+ mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
+ end
+
+ def build_success_email(build_id, to)
+ @build = Ci::Build.find(build_id)
+ @project = @build.project
+ mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
+ end
+ end
+ end
+end
diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb
new file mode 100644
index 00000000000..4462da0d7d2
--- /dev/null
+++ b/app/mailers/ci/notify.rb
@@ -0,0 +1,47 @@
+module Ci
+ class Notify < ActionMailer::Base
+ include Ci::Emails::Builds
+
+ add_template_helper Ci::ApplicationHelper
+ add_template_helper Ci::GitlabHelper
+
+ default_url_options[:host] = Gitlab.config.gitlab.host
+ default_url_options[:protocol] = Gitlab.config.gitlab.protocol
+ default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port?
+ default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
+
+ default from: Gitlab.config.gitlab.email_from
+
+ # Just send email with 3 seconds delay
+ def self.delay
+ delay_for(2.seconds)
+ end
+
+ private
+
+ # Formats arguments into a String suitable for use as an email subject
+ #
+ # extra - Extra Strings to be inserted into the subject
+ #
+ # Examples
+ #
+ # >> subject('Lorem ipsum')
+ # => "GitLab-CI | Lorem ipsum"
+ #
+ # # Automatically inserts Project name when @project is set
+ # >> @project = Project.last
+ # => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...>
+ # >> subject('Lorem ipsum')
+ # => "GitLab-CI | Ruby on Rails | Lorem ipsum "
+ #
+ # # Accepts multiple arguments
+ # >> subject('Lorem ipsum', 'Dolor sit amet')
+ # => "GitLab-CI | Lorem ipsum | Dolor sit amet"
+ def subject(*extra)
+ subject = "GitLab-CI"
+ subject << (@project ? " | #{@project.name}" : "")
+ subject << " | " + extra.join(' | ') if extra.present?
+ subject
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5717c89e61d..f196ffd53f3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -100,7 +100,7 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
if @project
- headers['X-GitLab-Project'] = @project.name
+ headers['X-GitLab-Project'] = @project.name
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index f8e5afa9b01..a020b24a550 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -149,6 +149,7 @@ class Ability
:admin_merge_request,
:create_merge_request,
:create_wiki,
+ :manage_builds,
:push_code
]
end
diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb
new file mode 100644
index 00000000000..0cf496f7d81
--- /dev/null
+++ b/app/models/ci/application_setting.rb
@@ -0,0 +1,27 @@
+# == Schema Information
+#
+# Table name: application_settings
+#
+# id :integer not null, primary key
+# all_broken_builds :boolean
+# add_pusher :boolean
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class ApplicationSetting < ActiveRecord::Base
+ extend Ci::Model
+
+ def self.current
+ Ci::ApplicationSetting.last
+ end
+
+ def self.create_from_defaults
+ create(
+ all_broken_builds: Settings.gitlab_ci['all_broken_builds'],
+ add_pusher: Settings.gitlab_ci['add_pusher'],
+ )
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
new file mode 100644
index 00000000000..8096d4fa5ae
--- /dev/null
+++ b/app/models/ci/build.rb
@@ -0,0 +1,285 @@
+# == 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)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# deploy :boolean default(FALSE)
+# trigger_request_id :integer
+#
+
+module Ci
+ class Build < ActiveRecord::Base
+ extend Ci::Model
+
+ LAZY_ATTRIBUTES = ['trace']
+
+ belongs_to :commit, class_name: 'Ci::Commit'
+ belongs_to :project, class_name: 'Ci::Project'
+ belongs_to :runner, class_name: 'Ci::Runner'
+ belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+
+ serialize :options
+
+ validates :commit, presence: true
+ validates :status, presence: true
+ validates :coverage, numericality: true, allow_blank: true
+
+ scope :running, ->() { where(status: "running") }
+ scope :pending, ->() { where(status: "pending") }
+ scope :success, ->() { where(status: "success") }
+ scope :failed, ->() { where(status: "failed") }
+ scope :unstarted, ->() { where(runner_id: nil) }
+ scope :running_or_pending, ->() { where(status:[:running, :pending]) }
+
+ acts_as_taggable
+
+ # To prevent db load megabytes of data from trace
+ default_scope -> { select(Ci::Build.columns_without_lazy) }
+
+ class << self
+ def columns_without_lazy
+ (column_names - LAZY_ATTRIBUTES).map do |column_name|
+ "#{table_name}.#{column_name}"
+ end
+ end
+
+ def last_month
+ where('created_at > ?', Date.today - 1.month)
+ end
+
+ def first_pending
+ pending.unstarted.order('created_at ASC').first
+ end
+
+ def create_from(build)
+ new_build = build.dup
+ new_build.status = :pending
+ new_build.runner_id = nil
+ new_build.save
+ end
+
+ def retry(build)
+ new_build = Ci::Build.new(status: :pending)
+ new_build.options = build.options
+ new_build.commands = build.commands
+ new_build.tag_list = build.tag_list
+ new_build.commit_id = build.commit_id
+ new_build.project_id = build.project_id
+ new_build.name = build.name
+ new_build.allow_failure = build.allow_failure
+ new_build.stage = build.stage
+ new_build.trigger_request = build.trigger_request
+ new_build.save
+ new_build
+ end
+ end
+
+ state_machine :status, initial: :pending do
+ event :run do
+ transition pending: :running
+ end
+
+ event :drop do
+ transition running: :failed
+ end
+
+ event :success do
+ transition running: :success
+ end
+
+ event :cancel do
+ transition [:pending, :running] => :canceled
+ end
+
+ after_transition pending: :running do |build, transition|
+ build.update_attributes started_at: Time.now
+ end
+
+ after_transition any => [:success, :failed, :canceled] do |build, transition|
+ build.update_attributes finished_at: Time.now
+ project = build.project
+
+ if project.web_hooks?
+ Ci::WebHookService.new.build_end(build)
+ end
+
+ if build.commit.success?
+ build.commit.create_next_builds(build.trigger_request)
+ end
+
+ project.execute_services(build)
+
+ if project.coverage_enabled?
+ build.update_coverage
+ end
+ end
+
+ state :pending, value: 'pending'
+ state :running, value: 'running'
+ state :failed, value: 'failed'
+ state :success, value: 'success'
+ state :canceled, value: 'canceled'
+ end
+
+ delegate :sha, :short_sha, :before_sha, :ref,
+ to: :commit, prefix: false
+
+ def trace_html
+ html = Ci::Ansi2html::convert(trace) if trace.present?
+ html ||= ''
+ end
+
+ def trace
+ if project && read_attribute(:trace).present?
+ read_attribute(:trace).gsub(project.token, 'xxxxxx')
+ end
+ end
+
+ def started?
+ !pending? && !canceled? && started_at
+ end
+
+ def active?
+ running? || pending?
+ end
+
+ def complete?
+ canceled? || success? || failed?
+ end
+
+ def ignored?
+ failed? && allow_failure?
+ end
+
+ def timeout
+ project.timeout
+ end
+
+ def variables
+ yaml_variables + project_variables + trigger_variables
+ end
+
+ def duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
+
+ def project
+ commit.project
+ end
+
+ def project_id
+ commit.project_id
+ end
+
+ def project_name
+ project.name
+ end
+
+ def repo_url
+ project.repo_url_with_auth
+ end
+
+ def allow_git_fetch
+ project.allow_git_fetch
+ end
+
+ def update_coverage
+ coverage = extract_coverage(trace, project.coverage_regex)
+
+ if coverage.is_a? Numeric
+ update_attributes(coverage: coverage)
+ end
+ end
+
+ def extract_coverage(text, regex)
+ begin
+ matches = text.gsub(Regexp.new(regex)).to_a.last
+ coverage = matches.gsub(/\d+(\.\d+)?/).first
+
+ if coverage.present?
+ coverage.to_f
+ end
+ rescue => ex
+ # if bad regex or something goes wrong we dont want to interrupt transition
+ # so we just silentrly ignore error for now
+ end
+ end
+
+ def trace
+ if File.exist?(path_to_trace)
+ File.read(path_to_trace)
+ else
+ # backward compatibility
+ read_attribute :trace
+ end
+ end
+
+ def trace=(trace)
+ unless Dir.exists? dir_to_trace
+ FileUtils.mkdir_p dir_to_trace
+ end
+
+ File.write(path_to_trace, trace)
+ end
+
+ def dir_to_trace
+ File.join(
+ Settings.gitlab_ci.builds_path,
+ created_at.utc.strftime("%Y_%m"),
+ project.id.to_s
+ )
+ end
+
+ def path_to_trace
+ "#{dir_to_trace}/#{id}.log"
+ end
+
+ private
+
+ def yaml_variables
+ if commit.config_processor
+ commit.config_processor.variables.map do |key, value|
+ { key: key, value: value, public: true }
+ end
+ else
+ []
+ end
+ end
+
+ def project_variables
+ project.variables.map do |variable|
+ { key: variable.key, value: variable.value, public: false }
+ end
+ end
+
+ def trigger_variables
+ if trigger_request && trigger_request.variables
+ trigger_request.variables.map do |key, value|
+ { key: key, value: value, public: false }
+ end
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
new file mode 100644
index 00000000000..23cd47dfe37
--- /dev/null
+++ b/app/models/ci/commit.rb
@@ -0,0 +1,267 @@
+# == 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
+#
+
+module Ci
+ class Commit < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+ has_many :builds, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+
+ serialize :push_data
+
+ validates_presence_of :ref, :sha, :before_sha, :push_data
+ validate :valid_commit_sha
+
+ def self.truncate_sha(sha)
+ sha[0...8]
+ end
+
+ def to_param
+ sha
+ end
+
+ def last_build
+ builds.order(:id).last
+ end
+
+ def retry
+ builds_without_retry.each do |build|
+ Ci::Build.retry(build)
+ end
+ end
+
+ def valid_commit_sha
+ if self.sha == Ci::Git::BLANK_SHA
+ self.errors.add(:sha, " cant be 00000000 (branch removal)")
+ end
+ end
+
+ def new_branch?
+ before_sha == Ci::Git::BLANK_SHA
+ end
+
+ def compare?
+ !new_branch?
+ end
+
+ def git_author_name
+ commit_data[:author][:name] if commit_data && commit_data[:author]
+ end
+
+ def git_author_email
+ commit_data[:author][:email] if commit_data && commit_data[:author]
+ end
+
+ def git_commit_message
+ commit_data[:message] if commit_data && commit_data[:message]
+ end
+
+ def short_before_sha
+ Ci::Commit.truncate_sha(before_sha)
+ end
+
+ def short_sha
+ Ci::Commit.truncate_sha(sha)
+ end
+
+ def commit_data
+ push_data[:commits].find do |commit|
+ commit[:id] == sha
+ end
+ rescue
+ nil
+ end
+
+ def project_recipients
+ recipients = project.email_recipients.split(' ')
+
+ if project.email_add_pusher? && push_data[:user_email].present?
+ recipients << push_data[:user_email]
+ end
+
+ recipients.uniq
+ end
+
+ def stage
+ return unless config_processor
+ stages = builds_without_retry.select(&:active?).map(&:stage)
+ config_processor.stages.find { |stage| stages.include? stage }
+ end
+
+ def create_builds_for_stage(stage, trigger_request)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag)
+ builds_attrs.map do |build_attrs|
+ builds.create!({
+ project: project,
+ name: build_attrs[:name],
+ commands: build_attrs[:script],
+ tag_list: build_attrs[:tags],
+ options: build_attrs[:options],
+ allow_failure: build_attrs[:allow_failure],
+ stage: build_attrs[:stage],
+ trigger_request: trigger_request,
+ })
+ end
+ end
+
+ def create_next_builds(trigger_request)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ stages = builds.where(trigger_request: trigger_request).group_by(&:stage)
+
+ config_processor.stages.any? do |stage|
+ !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present?
+ end
+ end
+
+ def create_builds(trigger_request = nil)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ config_processor.stages.any? do |stage|
+ create_builds_for_stage(stage, trigger_request).present?
+ end
+ end
+
+ def builds_without_retry
+ @builds_without_retry ||=
+ begin
+ grouped_builds = builds.group_by(&:name)
+ grouped_builds.map do |name, builds|
+ builds.sort_by(&:id).last
+ end
+ end
+ end
+
+ def builds_without_retry_sorted
+ return builds_without_retry unless config_processor
+
+ stages = config_processor.stages
+ builds_without_retry.sort_by do |build|
+ [stages.index(build.stage) || -1, build.name || ""]
+ end
+ end
+
+ def retried_builds
+ @retried_builds ||= (builds.order(id: :desc) - builds_without_retry)
+ end
+
+ def status
+ if skip_ci?
+ return 'skipped'
+ elsif yaml_errors.present?
+ return 'failed'
+ elsif builds.none?
+ return 'skipped'
+ elsif success?
+ 'success'
+ elsif pending?
+ 'pending'
+ elsif running?
+ 'running'
+ elsif canceled?
+ 'canceled'
+ else
+ 'failed'
+ end
+ end
+
+ def pending?
+ builds_without_retry.all? do |build|
+ build.pending?
+ end
+ end
+
+ def running?
+ builds_without_retry.any? do |build|
+ build.running? || build.pending?
+ end
+ end
+
+ def success?
+ builds_without_retry.all? do |build|
+ build.success? || build.ignored?
+ end
+ end
+
+ def failed?
+ status == 'failed'
+ end
+
+ def canceled?
+ builds_without_retry.all? do |build|
+ build.canceled?
+ end
+ end
+
+ def duration
+ @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i
+ end
+
+ def finished_at
+ @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at)
+ end
+
+ def coverage
+ if project.coverage_enabled?
+ coverage_array = builds_without_retry.map(&:coverage).compact
+ if coverage_array.size >= 1
+ '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
+ end
+ end
+ end
+
+ def matrix?
+ builds_without_retry.size > 1
+ end
+
+ def config_processor
+ @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file] || project.generated_yaml_config)
+ rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ save_yaml_error(e.message)
+ nil
+ rescue Exception => e
+ logger.error e.message + "\n" + e.backtrace.join("\n")
+ save_yaml_error("Undefined yaml error")
+ nil
+ end
+
+ def skip_ci?
+ return false if builds.any?
+ commits = push_data[:commits]
+ commits.present? && commits.last[:message] =~ /(\[ci skip\])/
+ end
+
+ def update_committed!
+ update!(committed_at: DateTime.now)
+ end
+
+ private
+
+ def save_yaml_error(error)
+ return if self.yaml_errors?
+ self.yaml_errors = error
+ save
+ end
+ end
+end
diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb
new file mode 100644
index 00000000000..cac3a7a49c1
--- /dev/null
+++ b/app/models/ci/event.rb
@@ -0,0 +1,27 @@
+# == 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
+#
+
+module Ci
+ class Event < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates :description,
+ presence: true,
+ length: { in: 5..200 }
+
+ scope :admin, ->(){ where(is_admin: true) }
+ scope :project_wide, ->(){ where(is_admin: false) }
+ end
+end
diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb
new file mode 100644
index 00000000000..2cf1783616f
--- /dev/null
+++ b/app/models/ci/project.rb
@@ -0,0 +1,225 @@
+# == 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
+#
+
+module Ci
+ class Project < ActiveRecord::Base
+ extend Ci::Model
+
+ include Ci::ProjectStatus
+
+ belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id
+
+ has_many :commits, ->() { order(:committed_at) }, dependent: :destroy, class_name: 'Ci::Commit'
+ has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
+ has_many :runners, through: :runner_projects, class_name: 'Ci::Runner'
+ has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook'
+ has_many :events, dependent: :destroy, class_name: 'Ci::Event'
+ has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
+
+ # Project services
+ has_many :services, dependent: :destroy, class_name: 'Ci::Service'
+ has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService'
+ has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService'
+ has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService'
+
+ accepts_nested_attributes_for :variables, allow_destroy: true
+
+ #
+ # Validations
+ #
+ validates_presence_of :name, :timeout, :token, :default_ref,
+ :path, :ssh_url_to_repo, :gitlab_id
+
+ validates_uniqueness_of :gitlab_id
+
+ validates :polling_interval,
+ presence: true,
+ if: ->(project) { project.always_build.present? }
+
+ scope :public_only, ->() { where(public: true) }
+
+ before_validation :set_default_values
+
+ class << self
+ include Ci::CurrentSettings
+
+ def base_build_script
+ <<-eos
+ git submodule update --init
+ ls -la
+ eos
+ end
+
+ def parse(project)
+ params = {
+ name: project.name_with_namespace,
+ gitlab_id: project.id,
+ path: project.path_with_namespace,
+ default_ref: project.default_branch || 'master',
+ ssh_url_to_repo: project.ssh_url_to_repo,
+ email_add_pusher: current_application_settings.add_pusher,
+ email_only_broken_builds: current_application_settings.all_broken_builds,
+ }
+
+ project = Ci::Project.new(params)
+ project.build_missing_services
+ project
+ end
+
+ # TODO: remove
+ def from_gitlab(user, scope = :owned, options)
+ opts = user.authenticate_options
+ opts.merge! options
+
+ raise 'Implement me of fix'
+ #projects = Ci::Network.new.projects(opts.compact, scope)
+
+ if projects
+ projects.map { |pr| OpenStruct.new(pr) }
+ else
+ []
+ end
+ end
+
+ def already_added?(project)
+ where(gitlab_id: project.id).any?
+ end
+
+ def unassigned(runner)
+ joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \
+ "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}").
+ where('#{Ci::RunnerProject.table_name}.project_id' => nil)
+ end
+
+ def ordered_by_last_commit_date
+ last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY project_id)"
+ joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.id = last_commit.project_id").
+ order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
+ end
+
+ def search(query)
+ where("LOWER(#{Ci::Project.table_name}.name) LIKE :query",
+ query: "%#{query.try(:downcase)}%")
+ end
+ end
+
+ def any_runners?
+ if runners.active.any?
+ return true
+ end
+
+ shared_runners_enabled && Ci::Runner.shared.active.any?
+ end
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def tracked_refs
+ @tracked_refs ||= default_ref.split(",").map{|ref| ref.strip}
+ end
+
+ def valid_token? token
+ self.token && self.token == token
+ end
+
+ def no_running_builds?
+ # Get running builds not later than 3 days ago to ignore hangs
+ builds.running.where("updated_at > ?", 3.days.ago).empty?
+ end
+
+ def email_notification?
+ email_add_pusher || email_recipients.present?
+ end
+
+ def web_hooks?
+ web_hooks.any?
+ end
+
+ def services?
+ services.any?
+ end
+
+ def timeout_in_minutes
+ timeout / 60
+ end
+
+ def timeout_in_minutes=(value)
+ self.timeout = value.to_i * 60
+ end
+
+ def coverage_enabled?
+ coverage_regex.present?
+ end
+
+ # Build a clone-able repo url
+ # using http and basic auth
+ def repo_url_with_auth
+ auth = "gitlab-ci-token:#{token}@"
+ url = gitlab_url + ".git"
+ url.sub(/^https?:\/\//) do |prefix|
+ prefix + auth
+ end
+ end
+
+ def available_services_names
+ %w(slack mail hip_chat)
+ end
+
+ def build_missing_services
+ available_services_names.each do |service_name|
+ service = services.find { |service| service.to_param == service_name }
+
+ # If service is available but missing in db
+ # we should create an instance. Ex `create_gitlab_ci_service`
+ service = self.send :"create_#{service_name}_service" if service.nil?
+ end
+ end
+
+ def execute_services(data)
+ services.each do |service|
+
+ # Call service hook only if it is active
+ begin
+ service.execute(data) if service.active && service.can_execute?(data)
+ rescue => e
+ logger.error(e)
+ end
+ end
+ end
+
+ def gitlab_url
+ File.join(Gitlab.config.gitlab.url, path)
+ end
+
+ def setup_finished?
+ commits.any?
+ end
+ end
+end
diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb
new file mode 100644
index 00000000000..6d5cafe81a2
--- /dev/null
+++ b/app/models/ci/project_status.rb
@@ -0,0 +1,47 @@
+module Ci
+ module ProjectStatus
+ def status
+ last_commit.status if last_commit
+ end
+
+ def broken?
+ last_commit.failed? if last_commit
+ end
+
+ def success?
+ last_commit.success? if last_commit
+ end
+
+ def broken_or_success?
+ broken? || success?
+ end
+
+ def last_commit
+ @last_commit ||= commits.last if commits.any?
+ end
+
+ def last_commit_date
+ last_commit.try(:created_at)
+ end
+
+ def human_status
+ status
+ end
+
+ # only check for toggling build status within same ref.
+ def last_commit_changed_status?
+ ref = last_commit.ref
+ last_commits = commits.where(ref: ref).last(2)
+
+ if last_commits.size < 2
+ false
+ else
+ last_commits[0].status != last_commits[1].status
+ end
+ end
+
+ def last_commit_for_ref(ref)
+ commits.where(ref: ref).last
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
new file mode 100644
index 00000000000..1e9f78a3748
--- /dev/null
+++ b/app/models/ci/runner.rb
@@ -0,0 +1,80 @@
+# == 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)
+#
+
+module Ci
+ class Runner < ActiveRecord::Base
+ extend Ci::Model
+
+ has_many :builds, class_name: 'Ci::Build'
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
+ has_many :projects, through: :runner_projects, class_name: 'Ci::Project'
+
+ has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
+
+ before_validation :set_default_values
+
+ scope :specific, ->() { where(is_shared: false) }
+ scope :shared, ->() { where(is_shared: true) }
+ scope :active, ->() { where(active: true) }
+ scope :paused, ->() { where(active: false) }
+
+ acts_as_taggable
+
+ def self.search(query)
+ where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
+ query: "%#{query.try(:downcase)}%")
+ end
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def assign_to(project, current_user = nil)
+ self.is_shared = false if shared?
+ self.save
+ project.runner_projects.create!(runner_id: self.id)
+ end
+
+ def display_name
+ return token unless !description.blank?
+
+ description
+ end
+
+ def shared?
+ is_shared
+ end
+
+ def belongs_to_one_project?
+ runner_projects.count == 1
+ end
+
+ def specific?
+ !shared?
+ end
+
+ def only_for?(project)
+ projects == [project]
+ end
+
+ def short_sha
+ token[0...10]
+ end
+ end
+end
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
new file mode 100644
index 00000000000..44453ee4b41
--- /dev/null
+++ b/app/models/ci/runner_project.rb
@@ -0,0 +1,21 @@
+# == 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
+#
+
+module Ci
+ class RunnerProject < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :runner, class_name: 'Ci::Runner'
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates_uniqueness_of :runner_id, scope: :project_id
+ end
+end
diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb
new file mode 100644
index 00000000000..ed5e3f940b6
--- /dev/null
+++ b/app/models/ci/service.rb
@@ -0,0 +1,105 @@
+# == 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
+#
+
+# To add new service you should build a class inherited from Service
+# and implement a set of methods
+module Ci
+ class Service < ActiveRecord::Base
+ extend Ci::Model
+
+ serialize :properties, JSON
+
+ default_value_for :active, false
+
+ after_initialize :initialize_properties
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates :project_id, presence: true
+
+ def activated?
+ active
+ end
+
+ def category
+ :common
+ end
+
+ def initialize_properties
+ self.properties = {} if properties.nil?
+ end
+
+ def title
+ # implement inside child
+ end
+
+ def description
+ # implement inside child
+ end
+
+ def help
+ # implement inside child
+ end
+
+ def to_param
+ # implement inside child
+ end
+
+ def fields
+ # implement inside child
+ []
+ end
+
+ def can_test?
+ project.builds.any?
+ end
+
+ def can_execute?(build)
+ true
+ end
+
+ def execute(build)
+ # implement inside child
+ end
+
+ # Provide convenient accessor methods
+ # for each serialized property.
+ def self.prop_accessor(*args)
+ args.each do |arg|
+ class_eval %{
+ def #{arg}
+ (properties || {})['#{arg}']
+ end
+
+ def #{arg}=(value)
+ self.properties ||= {}
+ self.properties['#{arg}'] = value
+ end
+ }
+ end
+ end
+
+ def self.boolean_accessor(*args)
+ self.prop_accessor(*args)
+
+ args.each do |arg|
+ class_eval %{
+ def #{arg}?
+ ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
+ end
+ }
+ end
+ end
+ end
+end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
new file mode 100644
index 00000000000..fe224b7dc70
--- /dev/null
+++ b/app/models/ci/trigger.rb
@@ -0,0 +1,39 @@
+# == Schema Information
+#
+# Table name: triggers
+#
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer not null
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class Trigger < ActiveRecord::Base
+ extend Ci::Model
+
+ acts_as_paranoid
+
+ belongs_to :project, class_name: 'Ci::Project'
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+
+ validates_presence_of :token
+ validates_uniqueness_of :token
+
+ before_validation :set_default_values
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def last_trigger_request
+ trigger_requests.last
+ end
+
+ def short_token
+ token[0...10]
+ end
+ end
+end
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
new file mode 100644
index 00000000000..29cd9553394
--- /dev/null
+++ b/app/models/ci/trigger_request.rb
@@ -0,0 +1,23 @@
+# == Schema Information
+#
+# Table name: trigger_requests
+#
+# id :integer not null, primary key
+# trigger_id :integer not null
+# variables :text
+# created_at :datetime
+# updated_at :datetime
+# commit_id :integer
+#
+
+module Ci
+ class TriggerRequest < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :trigger, class_name: 'Ci::Trigger'
+ belongs_to :commit, class_name: 'Ci::Commit'
+ has_many :builds, class_name: 'Ci::Build'
+
+ serialize :variables
+ end
+end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
new file mode 100644
index 00000000000..7a542802fa6
--- /dev/null
+++ b/app/models/ci/variable.rb
@@ -0,0 +1,25 @@
+# == 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)
+#
+
+module Ci
+ class Variable < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates_presence_of :key
+ validates_uniqueness_of :key, scope: :project_id
+
+ attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
+ end
+end
diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb
new file mode 100644
index 00000000000..8f03b0625da
--- /dev/null
+++ b/app/models/ci/web_hook.rb
@@ -0,0 +1,44 @@
+# == 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
+#
+
+module Ci
+ class WebHook < ActiveRecord::Base
+ extend Ci::Model
+
+ include HTTParty
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ # HTTParty timeout
+ default_timeout 10
+
+ validates :url, presence: true,
+ format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
+
+ def execute(data)
+ parsed_url = URI.parse(url)
+ if parsed_url.userinfo.blank?
+ Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false)
+ else
+ post_url = url.gsub("#{parsed_url.userinfo}@", "")
+ auth = {
+ username: URI.decode(parsed_url.user),
+ password: URI.decode(parsed_url.password),
+ }
+ Ci::WebHook.post(post_url,
+ body: data.to_json,
+ headers: { "Content-Type" => "application/json" },
+ verify: false,
+ basic_auth: auth)
+ end
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 49525eb9227..81951467d41 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -329,7 +329,7 @@ class Project < ActiveRecord::Base
end
def web_url
- Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self)
+ Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self)
end
def web_url_without_protocol
@@ -455,7 +455,7 @@ class Project < ActiveRecord::Base
if avatar.present?
[gitlab_config.url, avatar.url].join
elsif avatar_in_git
- Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
+ Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self)
end
end
diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb
new file mode 100644
index 00000000000..58825fe066c
--- /dev/null
+++ b/app/models/project_services/ci/hip_chat_message.rb
@@ -0,0 +1,78 @@
+module Ci
+ class HipChatMessage
+ attr_reader :build
+
+ def initialize(build)
+ @build = build
+ end
+
+ def to_s
+ lines = Array.new
+ lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_url(project)}\">#{project.name}</a> - ")
+
+ if commit.matrix?
+ lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_ref_commits_url(project, commit.ref, commit.sha)}\">Commit ##{commit.id}</a></br>")
+ else
+ first_build = commit.builds_without_retry.first
+ lines.push("<a href=\"#{Ci::RoutesHelper.ci_project_build_url(project, first_build)}\">Build '#{first_build.name}' ##{first_build.id}</a></br>")
+ end
+
+ lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>")
+ lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).")
+ lines.join('')
+ end
+
+ def status_color(build_or_commit=nil)
+ build_or_commit ||= commit_status
+ case build_or_commit
+ when :success
+ 'green'
+ when :failed, :canceled
+ 'red'
+ else # :pending, :running or unknown
+ 'yellow'
+ end
+ end
+
+ def notify?
+ [:failed, :canceled].include?(commit_status)
+ end
+
+
+ private
+
+ def commit
+ build.commit
+ end
+
+ def project
+ commit.project
+ end
+
+ def build_status
+ build.status.to_sym
+ end
+
+ def commit_status
+ commit.status.to_sym
+ end
+
+ def humanized_status(build_or_commit=nil)
+ build_or_commit ||= commit_status
+ case build_or_commit
+ when :pending
+ "Pending"
+ when :running
+ "Running"
+ when :failed
+ "Failed"
+ when :success
+ "Successful"
+ when :canceled
+ "Canceled"
+ else
+ "Unknown"
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb
new file mode 100644
index 00000000000..0e6e97394bc
--- /dev/null
+++ b/app/models/project_services/ci/hip_chat_service.rb
@@ -0,0 +1,93 @@
+# == 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
+#
+
+module Ci
+ class HipChatService < Ci::Service
+ prop_accessor :hipchat_token, :hipchat_room, :hipchat_server
+ boolean_accessor :notify_only_broken_builds
+ validates :hipchat_token, presence: true, if: :activated?
+ validates :hipchat_room, presence: true, if: :activated?
+ default_value_for :notify_only_broken_builds, true
+
+ def title
+ "HipChat"
+ end
+
+ def description
+ "Private group chat, video chat, instant messaging for teams"
+ end
+
+ def help
+ end
+
+ def to_param
+ 'hip_chat'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' },
+ { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' },
+ { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' },
+ { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
+ ]
+ end
+
+ def can_execute?(build)
+ return if build.allow_failure?
+
+ commit = build.commit
+ return unless commit
+ return unless commit.builds_without_retry.include? build
+
+ case commit.status.to_sym
+ when :failed
+ true
+ when :success
+ true unless notify_only_broken_builds?
+ else
+ false
+ end
+ end
+
+ def execute(build)
+ msg = Ci::HipChatMessage.new(build)
+ opts = default_options.merge(
+ token: hipchat_token,
+ room: hipchat_room,
+ server: server_url,
+ color: msg.status_color,
+ notify: msg.notify?
+ )
+ Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts)
+ end
+
+ private
+
+ def default_options
+ {
+ service_name: 'GitLab CI',
+ message_format: 'html'
+ }
+ end
+
+ def server_url
+ if hipchat_server.blank?
+ 'https://api.hipchat.com'
+ else
+ hipchat_server
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb
new file mode 100644
index 00000000000..1bd2f33612b
--- /dev/null
+++ b/app/models/project_services/ci/mail_service.rb
@@ -0,0 +1,84 @@
+# == 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
+#
+
+module Ci
+ class MailService < Ci::Service
+ delegate :email_recipients, :email_recipients=,
+ :email_add_pusher, :email_add_pusher=,
+ :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false
+
+ before_save :update_project
+
+ default_value_for :active, true
+
+ def title
+ 'Mail'
+ end
+
+ def description
+ 'Email notification'
+ end
+
+ def to_param
+ 'mail'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' },
+ { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' },
+ { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' }
+ ]
+ end
+
+ def can_execute?(build)
+ return if build.allow_failure?
+
+ # it doesn't make sense to send emails for retried builds
+ commit = build.commit
+ return unless commit
+ return unless commit.builds_without_retry.include?(build)
+
+ case build.status.to_sym
+ when :failed
+ true
+ when :success
+ true unless email_only_broken_builds
+ else
+ false
+ end
+ end
+
+ def execute(build)
+ build.commit.project_recipients.each do |recipient|
+ case build.status.to_sym
+ when :success
+ mailer.build_success_email(build.id, recipient)
+ when :failed
+ mailer.build_fail_email(build.id, recipient)
+ end
+ end
+ end
+
+ private
+
+ def update_project
+ project.save!
+ end
+
+ def mailer
+ Ci::Notify.delay
+ end
+ end
+end
diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb
new file mode 100644
index 00000000000..491ace50111
--- /dev/null
+++ b/app/models/project_services/ci/slack_message.rb
@@ -0,0 +1,97 @@
+require 'slack-notifier'
+
+module Ci
+ class SlackMessage
+ def initialize(commit)
+ @commit = commit
+ end
+
+ def pretext
+ ''
+ end
+
+ def color
+ attachment_color
+ end
+
+ def fallback
+ format(attachment_message)
+ end
+
+ def attachments
+ fields = []
+
+ if commit.matrix?
+ commit.builds_without_retry.each do |build|
+ next if build.allow_failure?
+ next unless build.failed?
+ fields << {
+ title: build.name,
+ value: "Build <#{Ci::RoutesHelper.ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)."
+ }
+ end
+ end
+
+ [{
+ text: attachment_message,
+ color: attachment_color,
+ fields: fields
+ }]
+ end
+
+ private
+
+ attr_reader :commit
+
+ def attachment_message
+ out = "<#{Ci::RoutesHelper.ci_project_url(project)}|#{project_name}>: "
+ if commit.matrix?
+ out << "Commit <#{Ci::RoutesHelper.ci_project_ref_commits_url(project, commit.ref, commit.sha)}|\##{commit.id}> "
+ else
+ build = commit.builds_without_retry.first
+ out << "Build <#{Ci::RoutesHelper.ci_project_build_path(project, build)}|\##{build.id}> "
+ end
+ out << "(<#{commit_sha_link}|#{commit.short_sha}>) "
+ out << "of <#{commit_ref_link}|#{commit.ref}> "
+ out << "by #{commit.git_author_name} " if commit.git_author_name
+ out << "#{commit_status} in "
+ out << "#{commit.duration} second(s)"
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def project
+ commit.project
+ end
+
+ def project_name
+ project.name
+ end
+
+ def commit_sha_link
+ "#{project.gitlab_url}/commit/#{commit.sha}"
+ end
+
+ def commit_ref_link
+ "#{project.gitlab_url}/commits/#{commit.ref}"
+ end
+
+ def attachment_color
+ if commit.success?
+ 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def commit_status
+ if commit.success?
+ 'succeeded'
+ else
+ 'failed'
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb
new file mode 100644
index 00000000000..76db573dc17
--- /dev/null
+++ b/app/models/project_services/ci/slack_service.rb
@@ -0,0 +1,81 @@
+# == 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
+#
+
+module Ci
+ class SlackService < Ci::Service
+ prop_accessor :webhook
+ boolean_accessor :notify_only_broken_builds
+ validates :webhook, presence: true, if: :activated?
+
+ default_value_for :notify_only_broken_builds, true
+
+ def title
+ 'Slack'
+ end
+
+ def description
+ 'A team communication tool for the 21st century'
+ end
+
+ def to_param
+ 'slack'
+ end
+
+ def help
+ 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present?
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' },
+ { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
+ ]
+ end
+
+ def can_execute?(build)
+ return if build.allow_failure?
+
+ commit = build.commit
+ return unless commit
+ return unless commit.builds_without_retry.include?(build)
+
+ case commit.status.to_sym
+ when :failed
+ true
+ when :success
+ true unless notify_only_broken_builds?
+ else
+ false
+ end
+ end
+
+ def execute(build)
+ message = Ci::SlackMessage.new(build.commit)
+ options = default_options.merge(
+ color: message.color,
+ fallback: message.fallback,
+ attachments: message.attachments
+ )
+ Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options)
+ end
+
+ private
+
+ def default_options
+ {
+ username: 'GitLab CI'
+ }
+ end
+ end
+end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 0ebc0a3ba1a..9558292fea3 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -19,7 +19,7 @@
#
class GitlabIssueTrackerService < IssueTrackerService
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index bfa8fc7b860..35e30b1cb0b 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -19,7 +19,7 @@
#
class JiraService < IssueTrackerService
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
diff --git a/app/models/user.rb b/app/models/user.rb
index bff8eeed96d..25371f9138a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -753,4 +753,13 @@ class User < ActiveRecord::Base
def can_be_removed?
!solo_owned_groups.present?
end
+
+ def ci_authorized_projects
+ @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects)
+ end
+
+ def ci_authorized_runners
+ Ci::Runner.specific.includes(:runner_projects).
+ where(ci_runner_projects: { project_id: ci_authorized_projects } )
+ end
end
diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb
new file mode 100644
index 00000000000..0a1abf89a95
--- /dev/null
+++ b/app/services/ci/create_commit_service.rb
@@ -0,0 +1,50 @@
+module Ci
+ class CreateCommitService
+ def execute(project, params)
+ before_sha = params[:before]
+ sha = params[:checkout_sha] || params[:after]
+ origin_ref = params[:ref]
+
+ unless origin_ref && sha.present?
+ return false
+ end
+
+ ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '')
+
+ # Skip branch removal
+ if sha == Ci::Git::BLANK_SHA
+ return false
+ end
+
+ commit = project.commits.find_by_sha_and_ref(sha, ref)
+
+ # Create commit if not exists yet
+ unless commit
+ data = {
+ ref: ref,
+ sha: sha,
+ tag: origin_ref.start_with?('refs/tags/'),
+ before_sha: before_sha,
+ push_data: {
+ before: before_sha,
+ after: sha,
+ ref: ref,
+ user_name: params[:user_name],
+ user_email: params[:user_email],
+ repository: params[:repository],
+ commits: params[:commits],
+ total_commits_count: params[:total_commits_count],
+ ci_yaml_file: params[:ci_yaml_file]
+ }
+ }
+
+ commit = project.commits.create(data)
+ end
+
+ commit.update_committed!
+ commit.create_builds unless commit.builds.any?
+
+ commit
+ end
+ end
+end
diff --git a/app/services/ci/create_project_service.rb b/app/services/ci/create_project_service.rb
new file mode 100644
index 00000000000..0419612d521
--- /dev/null
+++ b/app/services/ci/create_project_service.rb
@@ -0,0 +1,35 @@
+module Ci
+ class CreateProjectService
+ include Gitlab::Application.routes.url_helpers
+
+ def execute(current_user, params, project_route, forked_project = nil)
+ @project = Ci::Project.parse(params)
+
+ Ci::Project.transaction do
+ @project.save!
+
+ data = {
+ token: @project.token,
+ project_url: project_route.gsub(":project_id", @project.id.to_s),
+ }
+
+ gl_project = ::Project.find(@project.gitlab_id)
+ gl_project.build_missing_services
+ gl_project.gitlab_ci_service.update_attributes(data.merge(active: true))
+ end
+
+ if forked_project
+ # Copy settings
+ settings = forked_project.attributes.select do |attr_name, value|
+ ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name
+ end
+
+ @project.update(settings)
+ end
+
+ Ci::EventService.new.create_project(current_user, @project)
+
+ @project
+ end
+ end
+end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
new file mode 100644
index 00000000000..9bad09f2f54
--- /dev/null
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -0,0 +1,17 @@
+module Ci
+ class CreateTriggerRequestService
+ def execute(project, trigger, ref, variables = nil)
+ commit = project.commits.where(ref: ref).last
+ return unless commit
+
+ trigger_request = trigger.trigger_requests.create!(
+ commit: commit,
+ variables: variables
+ )
+
+ if commit.create_builds(trigger_request)
+ trigger_request
+ end
+ end
+ end
+end
diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb
new file mode 100644
index 00000000000..3f4e02dd26c
--- /dev/null
+++ b/app/services/ci/event_service.rb
@@ -0,0 +1,31 @@
+module Ci
+ class EventService
+ def remove_project(user, project)
+ create(
+ description: "Project \"#{project.name}\" has been removed by #{user.username}",
+ user_id: user.id,
+ is_admin: true
+ )
+ end
+
+ def create_project(user, project)
+ create(
+ description: "Project \"#{project.name}\" has been created by #{user.username}",
+ user_id: user.id,
+ is_admin: true
+ )
+ end
+
+ def change_project_settings(user, project)
+ create(
+ project_id: project.id,
+ user_id: user.id,
+ description: "User \"#{user.username}\" updated projects settings"
+ )
+ end
+
+ def create(*args)
+ Ci::Event.create!(*args)
+ end
+ end
+end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
new file mode 100644
index 00000000000..b95835ba093
--- /dev/null
+++ b/app/services/ci/image_for_build_service.rb
@@ -0,0 +1,31 @@
+module Ci
+ class ImageForBuildService
+ def execute(project, params)
+ image_name =
+ if params[:sha]
+ commit = project.commits.find_by(sha: params[:sha])
+ image_for_commit(commit)
+ elsif params[:ref]
+ commit = project.last_commit_for_ref(params[:ref])
+ image_for_commit(commit)
+ else
+ 'build-unknown.svg'
+ end
+
+ image_path = Rails.root.join('public/ci', image_name)
+
+ OpenStruct.new(
+ path: image_path,
+ name: image_name
+ )
+ end
+
+ private
+
+ def image_for_commit(commit)
+ return 'build-unknown.svg' unless commit
+
+ 'build-' + commit.status + ".svg"
+ end
+ end
+end
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
new file mode 100644
index 00000000000..33f1c1e918d
--- /dev/null
+++ b/app/services/ci/register_build_service.rb
@@ -0,0 +1,40 @@
+module Ci
+ # This class responsible for assigning
+ # proper pending build to runner on runner API request
+ class RegisterBuildService
+ def execute(current_runner)
+ builds = Ci::Build.pending.unstarted
+
+ builds =
+ if current_runner.shared?
+ # don't run projects which have not enables shared runners
+ builds.includes(:project).where(ci_projects: { shared_runners_enabled: true })
+ else
+ # do run projects which are only assigned to this runner
+ builds.where(project_id: current_runner.projects)
+ end
+
+ builds = builds.order('created_at ASC')
+
+ build = builds.find do |build|
+ (build.tag_list - current_runner.tag_list).empty?
+ end
+
+
+ if build
+ # In case when 2 runners try to assign the same build, second runner will be declined
+ # with StateMachine::InvalidTransition in run! method.
+ build.with_lock do
+ build.runner_id = current_runner.id
+ build.save!
+ build.run!
+ end
+ end
+
+ build
+
+ rescue StateMachine::InvalidTransition
+ nil
+ end
+ end
+end
diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb
new file mode 100644
index 00000000000..3a17596aaeb
--- /dev/null
+++ b/app/services/ci/test_hook_service.rb
@@ -0,0 +1,7 @@
+module Ci
+ class TestHookService
+ def execute(hook, current_user)
+ Ci::WebHookService.new.build_end(hook.project.commits.last.last_build)
+ end
+ end
+end
diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb
new file mode 100644
index 00000000000..87984b20fa1
--- /dev/null
+++ b/app/services/ci/web_hook_service.rb
@@ -0,0 +1,36 @@
+module Ci
+ class WebHookService
+ def build_end(build)
+ execute_hooks(build.project, build_data(build))
+ end
+
+ def execute_hooks(project, data)
+ project.web_hooks.each do |web_hook|
+ async_execute_hook(web_hook, data)
+ end
+ end
+
+ def async_execute_hook(hook, data)
+ Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data)
+ end
+
+ def build_data(build)
+ project = build.project
+ data = {}
+ data.merge!({
+ build_id: build.id,
+ build_name: build.name,
+ build_status: build.status,
+ build_started_at: build.started_at,
+ build_finished_at: build.finished_at,
+ project_id: project.id,
+ project_name: project.name,
+ gitlab_url: project.gitlab_url,
+ ref: build.ref,
+ sha: build.sha,
+ before_sha: build.before_sha,
+ push_data: build.commit.push_data
+ })
+ end
+ end
+end
diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml
new file mode 100644
index 00000000000..634c9daa477
--- /dev/null
+++ b/app/views/ci/admin/application_settings/_form.html.haml
@@ -0,0 +1,24 @@
+= form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ - if @application_setting.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @application_setting.errors.full_messages.each do |msg|
+ %p= msg
+
+ %fieldset
+ %legend Default Project Settings
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :all_broken_builds do
+ = f.check_box :all_broken_builds
+ Send emails only on broken builds
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :add_pusher do
+ = f.check_box :add_pusher
+ Add pusher to recipients list
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml
new file mode 100644
index 00000000000..7ef0aa89ed6
--- /dev/null
+++ b/app/views/ci/admin/application_settings/show.html.haml
@@ -0,0 +1,3 @@
+%h3.page-title Settings
+%hr
+= render 'form'
diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml
new file mode 100644
index 00000000000..47f8df8f98e
--- /dev/null
+++ b/app/views/ci/admin/builds/_build.html.haml
@@ -0,0 +1,32 @@
+- if build.commit && build.project
+ %tr.build.alert{class: build_status_alert_class(build)}
+ %td.build-link
+ = link_to ci_project_build_url(build.project, build) do
+ %strong #{build.id}
+
+ %td.status
+ = build.status
+
+ %td.commit-link
+ = commit_link(build.commit)
+
+ %td.runner
+ - if build.runner
+ = link_to build.runner.id, ci_admin_runner_path(build.runner)
+
+ %td.build-project
+ = truncate build.project.name, length: 30
+
+ %td.build-message
+ %span= truncate(build.commit.git_commit_message, length: 30)
+
+ %td.build-branch
+ %span= truncate(build.ref, length: 25)
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_in_words build.finished_at} ago
diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml
new file mode 100644
index 00000000000..d23119162cc
--- /dev/null
+++ b/app/views/ci/admin/builds/index.html.haml
@@ -0,0 +1,28 @@
+%ul.nav.nav-tabs.append-bottom-20
+ %li{class: ("active" if @scope.nil?)}
+ = link_to 'All builds', ci_admin_builds_path
+
+ %li{class: ("active" if @scope == "pending")}
+ = link_to "Pending", ci_admin_builds_path(scope: :pending)
+
+ %li{class: ("active" if @scope == "running")}
+ = link_to "Running", ci_admin_builds_path(scope: :running)
+
+
+%table.builds
+ %thead
+ %tr
+ %th Build
+ %th Status
+ %th Commit
+ %th Runner
+ %th Project
+ %th Message
+ %th Branch
+ %th Duration
+ %th Finished at
+
+ - @builds.each do |build|
+ = render "ci/admin/builds/build", build: build
+
+= paginate @builds
diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml
new file mode 100644
index 00000000000..f9ab0994304
--- /dev/null
+++ b/app/views/ci/admin/events/index.html.haml
@@ -0,0 +1,17 @@
+%table.table
+ %thead
+ %tr
+ %th User ID
+ %th Description
+ %th When
+ - @events.each do |event|
+ %tr
+ %td
+ = event.user_id
+ %td
+ = event.description
+ %td.light
+ = time_ago_in_words event.updated_at
+ ago
+
+= paginate @events \ No newline at end of file
diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml
new file mode 100644
index 00000000000..505dd4b3fdc
--- /dev/null
+++ b/app/views/ci/admin/projects/_project.html.haml
@@ -0,0 +1,28 @@
+- last_commit = project.last_commit
+%tr.alert{class: commit_status_alert_class(last_commit) }
+ %td
+ = project.id
+ %td
+ = link_to [:ci, project] do
+ %strong= project.name
+ %td
+ - if last_commit
+ #{last_commit.status} (#{commit_link(last_commit)})
+ - if project.last_commit_date
+ = time_ago_in_words project.last_commit_date
+ ago
+ - else
+ No builds yet
+ %td
+ - if project.public
+ %i.fa.fa-globe
+ Public
+ - else
+ %i.fa.fa-lock
+ Private
+ %td
+ = project.commits.count
+ %td
+ = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do
+ %i.fa.fa-remove
+ Remove
diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml
new file mode 100644
index 00000000000..dc7b041473b
--- /dev/null
+++ b/app/views/ci/admin/projects/index.html.haml
@@ -0,0 +1,15 @@
+%table.table
+ %thead
+ %tr
+ %th ID
+ %th Name
+ %th Last build
+ %th Access
+ %th Builds
+ %th
+
+ - @projects.each do |project|
+ = render "ci/admin/projects/project", project: project
+
+= paginate @projects
+
diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml
new file mode 100644
index 00000000000..f049b4f4c4e
--- /dev/null
+++ b/app/views/ci/admin/runner_projects/index.html.haml
@@ -0,0 +1,57 @@
+%p.lead
+ To register new runner visit #{link_to 'this page ', ci_runners_path}
+
+.row
+ .col-md-8
+ %h5 Activated:
+ %table.table
+ %tr
+ %th Runner ID
+ %th Runner Description
+ %th Last build
+ %th Builds Stats
+ %th Registered
+ %th
+
+ - @runner_projects.each do |runner_project|
+ - runner = runner_project.runner
+ - builds = runner.builds.where(project_id: @project.id)
+ %tr
+ %td
+ %span.badge.badge-info= runner.id
+ %td
+ = runner.display_name
+ %td
+ - last_build = builds.last
+ - if last_build
+ = link_to last_build.short_sha, [last_build.project, last_build]
+ - else
+ unknown
+ %td
+ %span.badge.badge-success
+ #{builds.success.count}
+ %span /
+ %span.badge.badge-important
+ #{builds.failed.count}
+ %td
+ #{time_ago_in_words(runner_project.created_at)} ago
+ %td
+ = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right'
+ .col-md-4
+ %h5 Available
+ %table.table
+ %tr
+ %th ID
+ %th Token
+ %th
+
+ - (Ci::Runner.all - @project.runners).each do |runner|
+ %tr
+ %td
+ = runner.id
+ %td
+ = runner.token
+ %td
+ = form_for [:ci, @project, @runner_project] do |f|
+ = f.hidden_field :runner_id, value: runner.id
+ = f.submit 'Add', class: 'btn btn-sm'
diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/ci/admin/runners/_runner.html.haml
new file mode 100644
index 00000000000..701782d26bb
--- /dev/null
+++ b/app/views/ci/admin/runners/_runner.html.haml
@@ -0,0 +1,48 @@
+%tr{id: dom_id(runner)}
+ %td
+ - if runner.shared?
+ %span.label.label-success shared
+ - else
+ %span.label.label-info specific
+ - unless runner.active?
+ %span.label.label-danger paused
+
+ %td
+ = link_to ci_admin_runner_path(runner) do
+ = runner.short_sha
+ %td
+ .runner-description
+ = runner.description
+ %span (#{link_to 'edit', '#', class: 'edit-runner-link'})
+ .runner-description-form.hide
+ = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f|
+ .form-group
+ = f.text_field :description, class: 'form-control'
+ = f.submit 'Save', class: 'btn'
+ %span (#{link_to 'cancel', '#', class: 'cancel'})
+ %td
+ - if runner.shared?
+ \-
+ - else
+ = runner.projects.count(:all)
+ %td
+ #{runner.builds.count(:all)}
+ %td
+ - runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ %td
+ - if runner.contacted_at
+ #{time_ago_in_words(runner.contacted_at)} ago
+ - else
+ Never
+ %td
+ .pull-right
+ = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm'
+ &nbsp;
+ - if runner.active?
+ = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm'
+ - else
+ = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm'
+ = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+
diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml
new file mode 100644
index 00000000000..b9d6703ff41
--- /dev/null
+++ b/app/views/ci/admin/runners/index.html.haml
@@ -0,0 +1,52 @@
+%p.lead
+ %span To register new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication.
+ %code #{GitlabCi::REGISTRATION_TOKEN}
+
+.bs-callout
+ %p
+ A 'runner' is a process which runs a build.
+ You can setup as many runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+ %br
+
+ %div
+ %span Each runner can be in one of the following states:
+ %ul
+ %li
+ %span.label.label-success shared
+ \- run builds from all unassigned projects
+ %li
+ %span.label.label-info specific
+ \- run builds from assigned projects
+ %li
+ %span.label.label-danger paused
+ \- runner will not receive any new build
+
+.append-bottom-20.clearfix
+ .pull-left
+ = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token'
+ = submit_tag 'Search', class: 'btn'
+
+ .pull-right.light
+ Runners with last contact less than a minute ago: #{@active_runners_cnt}
+
+%br
+
+%table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Projects
+ %th Builds
+ %th Tags
+ %th Last contact
+ %th
+
+ - @runners.each do |runner|
+ = render "ci/admin/runners/runner", runner: runner
+= paginate @runners
diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml
new file mode 100644
index 00000000000..24e0ad3b070
--- /dev/null
+++ b/app/views/ci/admin/runners/show.html.haml
@@ -0,0 +1,118 @@
+= content_for :title do
+ %h3.project-title
+ Runner ##{@runner.id}
+ .pull-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
+
+
+
+- if @runner.shared?
+ .bs-callout.bs-callout-success
+ %h4 This runner will process build from ALL UNASSIGNED projects
+ %p
+ If you want runners to build only specific projects, enable them in the table below.
+ Keep in mind that this is a one way transition.
+- else
+ .bs-callout.bs-callout-info
+ %h4 This runner will process build only from ASSIGNED projects
+ %p You can't make this a shared runner.
+%hr
+= form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label_tag :token, class: 'control-label' do
+ Token
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', readonly: true
+ .form-group
+ = label_tag :description, class: 'control-label' do
+ Description
+ .col-sm-10
+ = f.text_field :description, class: 'form-control'
+ .form-group
+ = label_tag :tag_list, class: 'control-label' do
+ Tags
+ .col-sm-10
+ = f.text_field :tag_list, class: 'form-control'
+ .help-block You can setup builds to only use runners with specific tags
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+
+.row
+ .col-md-6
+ %h4 Restrict projects for this runner
+ - if @runner.projects.any?
+ %table.table
+ %thead
+ %tr
+ %th Assigned projects
+ %th
+ - @runner.runner_projects.each do |runner_project|
+ - project = runner_project.project
+ %tr.alert-info
+ %td
+ %strong
+ = project.name
+ %td
+ .pull-right
+ = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
+
+ %table.table
+ %thead
+ %tr
+ %th Project
+ %th
+ .pull-right
+ = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner),
+ class: 'btn btn-sm assign-all-runner',
+ title: 'Assign runner to all projects',
+ method: :put
+
+ %tr
+ %td
+ = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control'
+ = submit_tag 'Search', class: 'btn'
+
+ %td
+ - @projects.each do |project|
+ %tr
+ %td
+ = project.name
+ %td
+ .pull-right
+ = form_for [:ci, :admin, project, project.runner_projects.new] do |f|
+ = f.hidden_field :runner_id, value: @runner.id
+ = f.submit 'Enable', class: 'btn btn-xs'
+ = paginate @projects
+
+ .col-md-6
+ %h4 Recent builds served by this runner
+ %table.builds.runner-builds
+ %thead
+ %tr
+ %th Status
+ %th Project
+ %th Commit
+ %th Finished at
+
+ - @builds.each do |build|
+ %tr.build.alert{class: build_status_alert_class(build)}
+ %td.status
+ = build.status
+
+ %td.status
+ = build.project.name
+
+ %td.build-link
+ = link_to ci_project_build_path(build.project, build) do
+ %strong #{build.short_sha}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_in_words build.finished_at} ago
diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/ci/admin/runners/update.js.haml
new file mode 100644
index 00000000000..2b7d3067e20
--- /dev/null
+++ b/app/views/ci/admin/runners/update.js.haml
@@ -0,0 +1,2 @@
+:plain
+ $("#runner_#{@runner.id}").replaceWith("#{escape_javascript(render(@runner))}")
diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml
new file mode 100644
index 00000000000..da306c9f020
--- /dev/null
+++ b/app/views/ci/builds/_build.html.haml
@@ -0,0 +1,45 @@
+%tr.build.alert{class: build_status_alert_class(build)}
+ %td.status
+ = build.status
+
+ %td.build-link
+ = link_to ci_project_build_path(build.project, build) do
+ %strong Build ##{build.id}
+
+ %td
+ = build.stage
+
+ %td
+ = build.name
+ .pull-right
+ - if build.tags.any?
+ - build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.trigger_request
+ %span.label.label-info triggered
+ - if build.allow_failure
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_in_words build.finished_at} ago
+
+ - if build.project.coverage_enabled?
+ %td.coverage
+ - if build.coverage
+ #{build.coverage}%
+
+ %td
+ - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project)
+ .pull-right
+ - if build.active?
+ = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do
+ %i.fa.fa-remove.cred
+ - elsif build.commands.present?
+ = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do
+ %i.fa.fa-repeat
diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml
new file mode 100644
index 00000000000..d1e955b5012
--- /dev/null
+++ b/app/views/ci/builds/show.html.haml
@@ -0,0 +1,167 @@
+#up-build-trace
+- if @commit.matrix?
+ %ul.nav.nav-tabs.append-bottom-10
+ - @commit.builds_without_retry_sorted.each do |build|
+ %li{class: ('active' if build == @build) }
+ = link_to ci_project_build_url(@project, build) do
+ %i{class: build_icon_css_class(build)}
+ %span
+ Build ##{build.id}
+ - if build.name
+ &middot;
+ = build.name
+
+ - unless @commit.builds_without_retry.include?(@build)
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa.fa-warning-sign
+ This build was retried.
+
+.row
+ .col-md-9
+ .build-head.alert{class: build_status_alert_class(@build)}
+ %h4
+ - if @build.commit.tag?
+ Build for tag
+ %code #{@build.ref}
+ - else
+ Build for commit
+ %code #{@build.short_sha}
+ from
+
+ = link_to ci_project_path(@build.project, ref: @build.ref) do
+ %span.label.label-primary= "#{@build.ref}"
+
+ - if @build.duration
+ .pull-right
+ %span
+ %i.fa.fa-time
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+
+ .clearfix
+ = @build.status
+ .pull-right
+ = @build.updated_at.stamp('19:00 Aug 27')
+
+
+
+ .clearfix
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+ .clearfix
+ .scroll-controls
+ = link_to '#up-build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
+
+ %pre.trace#build-trace
+ %code.bash
+ = preserve do
+ = raw @build.trace_html
+ %div#down-build-trace
+
+ .col-md-3
+ - if @build.coverage
+ .build-widget
+ %h4.title
+ Test coverage
+ %h1 #{@build.coverage}%
+
+
+ .build-widget
+ %h4.title
+ Build
+ - if current_user && can?(current_user, :manage_builds, gl_project)
+ .pull-right
+ - if @build.active?
+ = link_to "Cancel", cancel_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-danger'
+ - elsif @build.commands.present?
+ = link_to "Retry", retry_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-primary', method: :post
+
+ - if @build.duration
+ %p
+ %span.attr-name Duration:
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+ %p
+ %span.attr-name Created:
+ #{time_ago_in_words(@build.created_at)} ago
+ - if @build.finished_at
+ %p
+ %span.attr-name Finished:
+ #{time_ago_in_words(@build.finished_at)} ago
+ %p
+ %span.attr-name Runner:
+ - if @build.runner && current_user && current_user.admin
+ \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)}
+ - elsif @build.runner
+ \##{@build.runner.id}
+
+ - if @build.trigger_request
+ .build-widget
+ %h4.title
+ Trigger
+
+ %p
+ %span.attr-name Token:
+ #{@build.trigger_request.trigger.short_token}
+
+ - if @build.trigger_request.variables
+ %p
+ %span.attr-name Variables:
+
+ %code
+ - @build.trigger_request.variables.each do |key, value|
+ #{key}=#{value}
+
+ .build-widget
+ %h4.title
+ Commit
+ .pull-right
+ %small #{build_commit_link @build}
+
+ - if @build.commit.compare?
+ %p
+ %span.attr-name Compare:
+ #{build_compare_link @build}
+ %p
+ %span.attr-name Branch:
+ #{build_ref_link @build}
+ %p
+ %span.attr-name Author:
+ #{@build.commit.git_author_name}
+ %p
+ %span.attr-name Message:
+ #{@build.commit.git_commit_message}
+
+ - if @build.tags.any?
+ .build-widget
+ %h4.title
+ Tags
+ - @build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+
+ - if @builds.present?
+ .build-widget
+ %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}:
+ %table.builds
+ - @builds.each_with_index do |build, i|
+ %tr.build.alert{class: build_status_alert_class(build)}
+ %td
+ = link_to ci_project_build_url(@project, build) do
+ %span ##{build.id}
+ %td
+ - if build.name
+ = build.name
+ %td.status= build.status
+
+
+ = paginate @builds
+
+
+:javascript
+ new CiBuild("#{ci_project_build_url(@project, @build)}", "#{@build.status}")
diff --git a/app/views/ci/charts/_build_times.haml b/app/views/ci/charts/_build_times.haml
new file mode 100644
index 00000000000..c3c2f572414
--- /dev/null
+++ b/app/views/ci/charts/_build_times.haml
@@ -0,0 +1,21 @@
+%fieldset
+ %legend
+ Commit duration in minutes for last 30 commits
+
+ %canvas#build_timesChart.padded{width: 800, height: 300}
+
+:javascript
+ var data = {
+ labels : #{@charts[:build_times].labels.to_json},
+ datasets : [
+ {
+ fillColor : "#4A3",
+ strokeColor : "rgba(151,187,205,1)",
+ pointColor : "rgba(151,187,205,1)",
+ pointStrokeColor : "#fff",
+ data : #{@charts[:build_times].build_times.to_json}
+ }
+ ]
+ }
+ var ctx = $("#build_timesChart").get(0).getContext("2d");
+ new Chart(ctx).Line(data,{"scaleOverlay": true});
diff --git a/app/views/ci/charts/_builds.haml b/app/views/ci/charts/_builds.haml
new file mode 100644
index 00000000000..1b0039fb834
--- /dev/null
+++ b/app/views/ci/charts/_builds.haml
@@ -0,0 +1,41 @@
+%fieldset
+ %legend
+ Builds chart for last week
+ (#{date_from_to(Date.today - 7.days, Date.today)})
+
+ %canvas#weekChart.padded{width: 800, height: 200}
+
+%fieldset
+ %legend
+ Builds chart for last month
+ (#{date_from_to(Date.today - 30.days, Date.today)})
+
+ %canvas#monthChart.padded{width: 800, height: 300}
+
+%fieldset
+ %legend Builds chart for last year
+ %canvas#yearChart.padded{width: 800, height: 400}
+
+- [:week, :month, :year].each do |scope|
+ :javascript
+ var data = {
+ labels : #{@charts[scope].labels.to_json},
+ datasets : [
+ {
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ pointColor : "rgba(220,220,220,1)",
+ pointStrokeColor : "#EEE",
+ data : #{@charts[scope].total.to_json}
+ },
+ {
+ fillColor : "#4A3",
+ strokeColor : "rgba(151,187,205,1)",
+ pointColor : "rgba(151,187,205,1)",
+ pointStrokeColor : "#fff",
+ data : #{@charts[scope].success.to_json}
+ }
+ ]
+ }
+ var ctx = $("##{scope}Chart").get(0).getContext("2d");
+ new Chart(ctx).Line(data,{"scaleOverlay": true});
diff --git a/app/views/ci/charts/_overall.haml b/app/views/ci/charts/_overall.haml
new file mode 100644
index 00000000000..f522f35a629
--- /dev/null
+++ b/app/views/ci/charts/_overall.haml
@@ -0,0 +1,21 @@
+%fieldset
+ %legend Overall
+ %p
+ Total:
+ %strong= pluralize @project.builds.count(:all), 'build'
+ %p
+ Successful:
+ %strong= pluralize @project.builds.success.count(:all), 'build'
+ %p
+ Failed:
+ %strong= pluralize @project.builds.failed.count(:all), 'build'
+
+ %p
+ Success ratio:
+ %strong
+ #{success_ratio(@project.builds.success, @project.builds.failed)}%
+
+ %p
+ Commits covered:
+ %strong
+ = @project.commits.count(:all)
diff --git a/app/views/ci/charts/show.html.haml b/app/views/ci/charts/show.html.haml
new file mode 100644
index 00000000000..0497f037721
--- /dev/null
+++ b/app/views/ci/charts/show.html.haml
@@ -0,0 +1,4 @@
+#charts.ci-charts
+ = render 'builds'
+ = render 'build_times'
+= render 'overall'
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
new file mode 100644
index 00000000000..c1b1988d147
--- /dev/null
+++ b/app/views/ci/commits/_commit.html.haml
@@ -0,0 +1,32 @@
+%tr.build.alert{class: commit_status_alert_class(commit)}
+ %td.status
+ = commit.status
+ - if commit.running?
+ &middot;
+ = commit.stage
+
+
+ %td.build-link
+ = link_to ci_project_ref_commits_path(commit.project, commit.ref, commit.sha) do
+ %strong #{commit.short_sha}
+
+ %td.build-message
+ %span= truncate_first_line(commit.git_commit_message)
+
+ %td.build-branch
+ - unless @ref
+ %span
+ = link_to truncate(commit.ref, length: 25), ci_project_path(@project, ref: commit.ref)
+
+ %td.duration
+ - if commit.duration > 0
+ #{time_interval_in_words commit.duration}
+
+ %td.timestamp
+ - if commit.finished_at
+ %span #{time_ago_in_words commit.finished_at} ago
+
+ - if commit.project.coverage_enabled?
+ %td.coverage
+ - if commit.coverage
+ #{commit.coverage}%
diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml
new file mode 100644
index 00000000000..1aeb557314a
--- /dev/null
+++ b/app/views/ci/commits/show.html.haml
@@ -0,0 +1,88 @@
+.commit-info
+ %pre.commit-message
+ #{@commit.git_commit_message}
+
+ .row
+ .col-sm-6
+ - if @commit.compare?
+ %p
+ %span.attr-name Compare:
+ #{gitlab_compare_link(@project, @commit.short_before_sha, @commit.short_sha)}
+ - else
+ %p
+ %span.attr-name Commit:
+ #{gitlab_commit_link(@project, @commit.sha)}
+
+ %p
+ %span.attr-name Branch:
+ #{gitlab_ref_link(@project, @commit.ref)}
+ .col-sm-6
+ %p
+ %span.attr-name Author:
+ #{@commit.git_author_name} (#{@commit.git_author_email})
+ - if @commit.created_at
+ %p
+ %span.attr-name Created at:
+ #{@commit.created_at.to_s(:short)}
+
+- if current_user && can?(current_user, :manage_builds, gl_project)
+ .pull-right
+ - if @commit.builds.running_or_pending.any?
+ = link_to "Cancel", cancel_ci_project_ref_commits_path(@project, @commit.ref, @commit.sha), class: 'btn btn-sm btn-danger'
+
+
+- if @commit.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - @commit.yaml_errors.split(",").each do |error|
+ %li= error
+
+- unless @commit.push_data[:ci_yaml_file]
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+%h3 Status
+
+.build.alert{class: commit_status_alert_class(@commit)}
+ .status
+ = @commit.status.titleize
+
+%h3
+ Builds
+ - if @commit.duration > 0
+ %small.pull-right
+ %i.fa.fa-time
+ #{time_interval_in_words @commit.duration}
+
+%table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @project.coverage_enabled?
+ %th Coverage
+ %th
+ = render @commit.builds_without_retry_sorted, controls: true
+
+- if @commit.retried_builds.any?
+ %h3
+ Retried builds
+
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @project.coverage_enabled?
+ %th Coverage
+ %th
+ = render @commit.retried_builds
diff --git a/app/views/ci/errors/show.haml b/app/views/ci/errors/show.haml
new file mode 100644
index 00000000000..2788112c835
--- /dev/null
+++ b/app/views/ci/errors/show.haml
@@ -0,0 +1,2 @@
+%h3.error Error
+= @error
diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml
new file mode 100644
index 00000000000..779f49b3d3a
--- /dev/null
+++ b/app/views/ci/events/index.html.haml
@@ -0,0 +1,19 @@
+%h3.page-title Events
+
+%table.table
+ %thead
+ %tr
+ %th User ID
+ %th Description
+ %th When
+ - @events.each do |event|
+ %tr
+ %td
+ = event.user_id
+ %td
+ = event.description
+ %td.light
+ = time_ago_in_words event.updated_at
+ ago
+
+= paginate @events \ No newline at end of file
diff --git a/app/views/ci/helps/oauth2.html.haml b/app/views/ci/helps/oauth2.html.haml
new file mode 100644
index 00000000000..2031b7340d4
--- /dev/null
+++ b/app/views/ci/helps/oauth2.html.haml
@@ -0,0 +1,20 @@
+.welcome-block
+ %h1
+ Welcome to GitLab CI
+ %p
+ GitLab CI integrates with your GitLab installation and runs tests for your projects.
+
+ %h3 You need only 2 steps to set it up
+
+ %ol
+ %li
+ In the GitLab admin area under OAuth applications create a new entry. The redirect url should be
+ %code= callback_ci_user_sessions_url
+ %li
+ Update the GitLab CI config with the application id and the application secret from GitLab.
+ %li
+ Restart your GitLab CI instance
+ %li
+ Refresh this page when GitLab CI has started again
+
+
diff --git a/app/views/ci/helps/show.html.haml b/app/views/ci/helps/show.html.haml
new file mode 100644
index 00000000000..9b32d529c60
--- /dev/null
+++ b/app/views/ci/helps/show.html.haml
@@ -0,0 +1,40 @@
+.jumbotron
+ %h2
+ GitLab CI
+ %span= GitlabCi::VERSION
+ %small= GitlabCi::REVISION
+ %p
+ GitLab CI integrates with your GitLab installation and run tests for your projects.
+ %br
+ Login with your GitLab account, add a project with one click and enjoy running your tests.
+ %br
+ Read more about GitLab CI at #{link_to "about.gitlab.com/gitlab-ci", "https://about.gitlab.com/gitlab-ci/", target: "_blank"}.
+
+
+.bs-callout.bs-callout-success
+ %h4
+ = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/api' do
+ %i.fa.fa-cogs
+ API
+ %p Explore how you can access GitLab CI via the API.
+
+.bs-callout.bs-callout-info
+ %h4
+ = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/tree/master/doc/examples' do
+ %i.fa.fa-info-sign
+ Build script examples
+ %p This includes the build script we use to test GitLab CE.
+
+.bs-callout.bs-callout-danger
+ %h4
+ = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/issues' do
+ %i.fa.fa-bug
+ Issue tracker
+ %p Reports about recent bugs and problems..
+
+.bs-callout.bs-callout-warning
+ %h4
+ = link_to 'http://feedback.gitlab.com/forums/176466-general/category/64310-gitlab-ci' do
+ %i.fa.fa-thumbs-up
+ Feedback forum
+ %p Suggest improvements or new features for GitLab CI.
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
new file mode 100644
index 00000000000..e2179e60f3e
--- /dev/null
+++ b/app/views/ci/lints/_create.html.haml
@@ -0,0 +1,39 @@
+- if @status
+ %p
+ %b Status:
+ syntax is correct
+ %i.fa.fa-ok.correct-syntax
+
+ %table.table.table-bordered
+ %thead
+ %tr
+ %th Parameter
+ %th Value
+ %tbody
+ - @stages.each do |stage|
+ - @builds.select { |build| build[:stage] == stage }.each do |build|
+ %tr
+ %td #{stage.capitalize} Job - #{build[:name]}
+ %td
+ %pre
+ = simple_format build[:script]
+
+ %br
+ %b Tag list:
+ = build[:tags]
+ %br
+ %b Refs only:
+ = build[:only] && build[:only].join(", ")
+ %br
+ %b Refs except:
+ = build[:except] && build[:except].join(", ")
+
+-else
+ %p
+ %b Status:
+ syntax is incorrect
+ %i.fa.fa-remove.incorrect-syntax
+ %b Error:
+ = @error
+
+
diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml
new file mode 100644
index 00000000000..a96c0b11b6e
--- /dev/null
+++ b/app/views/ci/lints/create.js.haml
@@ -0,0 +1,2 @@
+:plain
+ $(".results").html("#{escape_javascript(render "create")}") \ No newline at end of file
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
new file mode 100644
index 00000000000..a9b954771c5
--- /dev/null
+++ b/app/views/ci/lints/show.html.haml
@@ -0,0 +1,25 @@
+%h2 Check your .gitlab-ci.yml
+%hr
+
+= form_tag ci_lint_path, method: :post, remote: true do
+ .control-group
+ = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label'
+ .controls
+ = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
+
+ .control-group.clearfix
+ .controls.pull-left.prepend-top-10
+ = submit_tag "Validate", class: 'btn btn-success submit-yml'
+
+
+%p.text-center.loading
+ %i.fa.fa-refresh.fa-spin
+
+.results.prepend-top-20
+
+:coffeescript
+ $(".loading").hide()
+ $('form').bind 'ajax:beforeSend', ->
+ $(".loading").show()
+ $('form').bind 'ajax:complete', ->
+ $(".loading").hide()
diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml
new file mode 100644
index 00000000000..d818e8b6756
--- /dev/null
+++ b/app/views/ci/notify/build_fail_email.html.haml
@@ -0,0 +1,19 @@
+- content_for :header do
+ %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
+ GitLab CI (build failed)
+%h3
+ Project:
+ = link_to ci_project_url(@project) do
+ = @project.name
+
+%p
+ Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+%p
+ Author: #{@build.commit.git_author_name}
+%p
+ Branch: #{@build.commit.ref}
+%p
+ Message: #{@build.commit.git_commit_message}
+
+%p
+ Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)}
diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb
new file mode 100644
index 00000000000..1add215a1c8
--- /dev/null
+++ b/app/views/ci/notify/build_fail_email.text.erb
@@ -0,0 +1,9 @@
+Build failed for <%= @project.name %>
+
+Status: <%= @build.status %>
+Commit: <%= @build.commit.short_sha %>
+Author: <%= @build.commit.git_author_name %>
+Branch: <%= @build.commit.ref %>
+Message: <%= @build.commit.git_commit_message %>
+
+Url: <%= ci_project_build_url(@build.project, @build) %>
diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml
new file mode 100644
index 00000000000..a20dcaee24e
--- /dev/null
+++ b/app/views/ci/notify/build_success_email.html.haml
@@ -0,0 +1,20 @@
+- content_for :header do
+ %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
+ GitLab CI (build successful)
+
+%h3
+ Project:
+ = link_to ci_project_url(@project) do
+ = @project.name
+
+%p
+ Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+%p
+ Author: #{@build.commit.git_author_name}
+%p
+ Branch: #{@build.commit.ref}
+%p
+ Message: #{@build.commit.git_commit_message}
+
+%p
+ Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)}
diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb
new file mode 100644
index 00000000000..7ebd17e7270
--- /dev/null
+++ b/app/views/ci/notify/build_success_email.text.erb
@@ -0,0 +1,9 @@
+Build successful for <%= @project.name %>
+
+Status: <%= @build.status %>
+Commit: <%= @build.commit.short_sha %>
+Author: <%= @build.commit.git_author_name %>
+Branch: <%= @build.commit.ref %>
+Message: <%= @build.commit.git_commit_message %>
+
+Url: <%= ci_project_build_url(@build.project, @build) %>
diff --git a/app/views/ci/projects/_form.html.haml b/app/views/ci/projects/_form.html.haml
new file mode 100644
index 00000000000..d50e1a83b06
--- /dev/null
+++ b/app/views/ci/projects/_form.html.haml
@@ -0,0 +1,101 @@
+.bs-callout.help-callout
+ %p
+ If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path}
+ %p
+ Edit your
+ #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@project)}
+
+= nested_form_for [:ci, @project], html: { class: 'form-horizontal' } do |f|
+ - if @project.errors.any?
+ #error_explanation
+ %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
+ .alert.alert-error
+ %ul
+ - @project.errors.full_messages.each do |msg|
+ %li= msg
+
+ %fieldset
+ %legend Build settings
+ .form-group
+ = label_tag nil, class: 'control-label' do
+ Get code
+ .col-sm-10
+ %p Get recent application code using the following command:
+ .radio
+ = label_tag do
+ = f.radio_button :allow_git_fetch, 'false'
+ %strong git clone
+ .light Slower but makes sure you have a clean dir before every build
+ .radio
+ = label_tag do
+ = f.radio_button :allow_git_fetch, 'true'
+ %strong git fetch
+ .light Faster
+ .form-group
+ = f.label :timeout_in_minutes, 'Timeout', class: 'control-label'
+ .col-sm-10
+ = f.number_field :timeout_in_minutes, class: 'form-control', min: '0'
+ .light per build in minutes
+
+
+ %fieldset
+ %legend Build Schedule
+ .form-group
+ = f.label :always_build, 'Schedule build', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.label :always_build do
+ = f.check_box :always_build
+ %span.light Repeat last build after X hours if no builds
+ .form-group
+ = f.label :polling_interval, "Build interval", class: 'control-label'
+ .col-sm-10
+ = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control'
+ .light In hours
+
+ %fieldset
+ %legend Project settings
+ .form-group
+ = f.label :default_ref, "Make tabs for the following branches", class: 'control-label'
+ .col-sm-10
+ = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable'
+ .light You will be able to filter builds by the following branches
+ .form-group
+ = f.label :public, 'Public mode', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.label :public do
+ = f.check_box :public
+ %span.light Anyone can see project and builds
+ .form-group
+ = f.label :coverage_regex, "Test coverage parsing", class: 'control-label'
+ .col-sm-10
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ .light We will use this regular expression to find test coverage output in build trace. Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%$
+
+
+
+ %fieldset
+ %legend Advanced settings
+ .form-group
+ = f.label :token, "CI token", class: 'control-label'
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89'
+
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-save'
+ = link_to 'Cancel', projects_path, class: 'btn'
+ - unless @project.new_record?
+ = link_to 'Remove Project', ci_project_path(@project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right'
diff --git a/app/views/ci/projects/_gl_projects.html.haml b/app/views/ci/projects/_gl_projects.html.haml
new file mode 100644
index 00000000000..7bd30b37caf
--- /dev/null
+++ b/app/views/ci/projects/_gl_projects.html.haml
@@ -0,0 +1,15 @@
+- @gl_projects.sort_by(&:name_with_namespace).each do |project|
+ %tr.light
+ %td
+ = project.name_with_namespace
+ %td
+ %small Not added to CI
+ %td
+ %td
+ - if Ci::Project.already_added?(project)
+ %strong.cgreen
+ Added
+ - else
+ = form_tag ci_projects_path do
+ = hidden_field_tag :project, project.to_json(methods: [:name_with_namespace, :path_with_namespace, :ssh_url_to_repo])
+ = submit_tag 'Add project to CI', class: 'btn btn-default btn-sm'
diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml
new file mode 100644
index 00000000000..1888e1bde93
--- /dev/null
+++ b/app/views/ci/projects/_info.html.haml
@@ -0,0 +1,2 @@
+- if no_runners_for_project?(@project)
+ = render 'no_runners'
diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/ci/projects/_no_runners.html.haml
new file mode 100644
index 00000000000..c0a296fb17d
--- /dev/null
+++ b/app/views/ci/projects/_no_runners.html.haml
@@ -0,0 +1,8 @@
+.alert.alert-danger
+ %p
+ There are NO runners to build this project.
+ %br
+ You can add Specific runner for this project on Runners page
+
+ - if current_user.is_admin
+ or add Shared runner for whole application in admin are.
diff --git a/app/views/ci/projects/_project.html.haml b/app/views/ci/projects/_project.html.haml
new file mode 100644
index 00000000000..b3ad47ce432
--- /dev/null
+++ b/app/views/ci/projects/_project.html.haml
@@ -0,0 +1,22 @@
+- last_commit = project.last_commit
+%tr.alert{class: commit_status_alert_class(last_commit) }
+ %td
+ = link_to [:ci, project] do
+ %strong= project.name
+ %td
+ - if last_commit
+ #{last_commit.status} (#{commit_link(last_commit)})
+ - if project.last_commit_date
+ = time_ago_in_words project.last_commit_date
+ ago
+ - else
+ No builds yet
+ %td
+ - if project.public
+ %i.fa.fa-globe
+ Public
+ - else
+ %i.fa.fa-lock
+ Private
+ %td
+ = project.commits.count
diff --git a/app/views/ci/projects/_public.html.haml b/app/views/ci/projects/_public.html.haml
new file mode 100644
index 00000000000..c2157ab741a
--- /dev/null
+++ b/app/views/ci/projects/_public.html.haml
@@ -0,0 +1,21 @@
+= content_for :title do
+ %h3.project-title
+ Public projects
+
+.bs-callout
+ = link_to new_ci_user_sessions_path(state: generate_oauth_state(request.fullpath)) do
+ %strong Login with GitLab
+ to see your private projects
+
+- if @projects.present?
+ .projects
+ %table.table
+ %tr
+ %th Name
+ %th Last commit
+ %th Access
+ %th Commits
+ = render @projects
+ = paginate @projects
+- else
+ %h4 No public projects yet
diff --git a/app/views/ci/projects/_search.html.haml b/app/views/ci/projects/_search.html.haml
new file mode 100644
index 00000000000..6d84b25a6af
--- /dev/null
+++ b/app/views/ci/projects/_search.html.haml
@@ -0,0 +1,17 @@
+.search
+ = form_tag "#", method: :get, class: 'ci-search-form' do |f|
+ .input-group
+ = search_field_tag "search", params[:search], placeholder: "Search", class: "search-input form-control"
+ .input-group-addon
+ %i.fa.fa-search
+
+
+:coffeescript
+ $('.ci-search-form').submit ->
+ NProgress.start()
+ query = $('.ci-search-form .search-input').val()
+ $.get '#{gitlab_ci_projects_path}', { search: query }, (data) ->
+ $(".projects").html data.html
+ NProgress.done()
+ CiPager.init "#{gitlab_ci_projects_path}" + "?search=" + query, #{Ci::ProjectsController::PROJECTS_BATCH}, false
+ false
diff --git a/app/views/ci/projects/edit.html.haml b/app/views/ci/projects/edit.html.haml
new file mode 100644
index 00000000000..298007a6565
--- /dev/null
+++ b/app/views/ci/projects/edit.html.haml
@@ -0,0 +1,21 @@
+- if @project.generated_yaml_config
+ %p.alert.alert-danger
+ CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_project_path(@project)}
+ or
+ %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview
+ yaml file which is based on your old jobs.
+ Put this file to the root of your project and name it .gitlab-ci.yml
+
+= render 'form'
+
+- if @project.generated_yaml_config
+ #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"}
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} ×
+ %h4.modal-title Content of .gitlab-ci.yml
+ .modal-body
+ = text_area_tag :yaml, @project.generated_yaml_config, size: "70x25", class: "form-control"
+ .modal-footer
+ %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close
diff --git a/app/views/ci/projects/gitlab.html.haml b/app/views/ci/projects/gitlab.html.haml
new file mode 100644
index 00000000000..f57dfcb0790
--- /dev/null
+++ b/app/views/ci/projects/gitlab.html.haml
@@ -0,0 +1,27 @@
+- if @offset == 0
+ .clearfix.light
+ .pull-left.fetch-status
+ - if params[:search].present?
+ by keyword: "#{params[:search]}",
+ #{@total_count} projects, #{@projects.size} of them added to CI
+ %br
+
+ %table.table.projects-table.content-list
+ %thead
+ %tr
+ %th Project Name
+ %th Last commit
+ %th Access
+ %th Commits
+
+ = render @projects
+
+ = render "gl_projects"
+
+ %p.text-center.hide.loading
+ %i.fa.fa-refresh.fa-spin
+
+- else
+ = render @projects
+
+ = render "gl_projects"
diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml
new file mode 100644
index 00000000000..085a70811ae
--- /dev/null
+++ b/app/views/ci/projects/index.html.haml
@@ -0,0 +1,13 @@
+- if current_user
+ .gray-content-block.top-block
+ = render "search"
+ .projects.prepend-top-default
+ %p.fetch-status.light
+ %i.fa.fa-refresh.fa-spin
+ :coffeescript
+ $.get '#{gitlab_ci_projects_path}', (data) ->
+ $(".projects").html data.html
+ CiPager.init "#{gitlab_ci_projects_path}", #{Ci::ProjectsController::PROJECTS_BATCH}, false
+
+- else
+ = render 'public'
diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml
new file mode 100644
index 00000000000..6443378af99
--- /dev/null
+++ b/app/views/ci/projects/show.html.haml
@@ -0,0 +1,60 @@
+= render 'ci/shared/guide' unless @project.setup_finished?
+
+- if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners?
+ .alert.alert-danger
+ Builds for this project wont be served unless you configure runners on
+ = link_to "Runners page", ci_project_runners_path(@project)
+
+%ul.nav.nav-tabs.append-bottom-20
+ %li{class: ref_tab_class}
+ = link_to 'All commits', ci_project_path(@project)
+ - @project.tracked_refs.each do |ref|
+ %li{class: ref_tab_class(ref)}
+ = link_to ref, ci_project_path(@project, ref: ref)
+
+ - if @ref && !@project.tracked_refs.include?(@ref)
+ %li{class: 'active'}
+ = link_to @ref, ci_project_path(@project, ref: @ref)
+
+ %li.pull-right
+ = link_to 'View on GitLab', @project.gitlab_url, no_turbolink.merge( class: 'btn btn-sm' )
+
+- if @ref
+ %p
+ Paste build status image for #{@ref} with next link
+ = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do
+ Status Badge
+ .badge-codes-block.bs-callout.bs-callout-info.hide
+ %p
+ Status badge for
+ %span.label.label-info #{@ref}
+ branch
+ %div
+ %label Markdown:
+ = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control'
+ %label Html:
+ = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control'
+
+
+
+
+%table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Commit
+ %th Message
+ %th Branch
+ %th Total duration
+ %th Finished at
+ - if @project.coverage_enabled?
+ %th Coverage
+
+ = render @commits
+
+= paginate @commits
+
+- if @commits.empty?
+ .bs-callout
+ %h4 No commits yet
+
diff --git a/app/views/ci/runners/_runner.html.haml b/app/views/ci/runners/_runner.html.haml
new file mode 100644
index 00000000000..ef8622e2807
--- /dev/null
+++ b/app/views/ci/runners/_runner.html.haml
@@ -0,0 +1,35 @@
+%li.runner{id: dom_id(runner)}
+ %h4
+ = runner_status_icon(runner)
+ %span.monospace
+ - if @runners.include?(runner)
+ = link_to runner.short_sha, ci_project_runner_path(@project, runner)
+ %small
+ =link_to edit_ci_project_runner_path(@project, runner) do
+ %i.fa.fa-edit.btn
+ - else
+ = runner.short_sha
+
+ .pull-right
+ - if @runners.include?(runner)
+ - if runner.belongs_to_one_project?
+ = link_to 'Remove runner', [:ci, @project, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - else
+ - runner_project = @project.runner_projects.find_by(runner_id: runner)
+ = link_to 'Disable for this project', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - elsif runner.specific?
+ = form_for [:ci, @project, @project.runner_projects.new] do |f|
+ = f.hidden_field :runner_id, value: runner.id
+ = f.submit 'Enable for this project', class: 'btn btn-sm'
+ .pull-right
+ %small.light
+ \##{runner.id}
+ - if runner.description.present?
+ %p.runner-description
+ = runner.description
+ - if runner.tag_list.present?
+ %p
+ - runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+
diff --git a/app/views/ci/runners/_shared_runners.html.haml b/app/views/ci/runners/_shared_runners.html.haml
new file mode 100644
index 00000000000..944b3fd930d
--- /dev/null
+++ b/app/views/ci/runners/_shared_runners.html.haml
@@ -0,0 +1,23 @@
+%h3 Shared runners
+
+.bs-callout.bs-callout-warning
+ GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X.
+ %hr
+ - if @project.shared_runners_enabled
+ = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-warning', method: :post do
+ Disable shared runners
+ - else
+ = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-success', method: :post do
+ Enable shared runners
+ &nbsp; for this project
+
+- if @shared_runners_count.zero?
+ This application has no shared runners yet.
+ Please use specific runners or ask administrator to create one
+- else
+ %h4.underlined-title Available shared runners - #{@shared_runners_count}
+ %ul.bordered-list.available-shared-runners
+ = render @shared_runners.first(10)
+ - if @shared_runners_count > 10
+ .light
+ and #{@shared_runners_count - 10} more...
diff --git a/app/views/ci/runners/_specific_runners.html.haml b/app/views/ci/runners/_specific_runners.html.haml
new file mode 100644
index 00000000000..0604e7a46c5
--- /dev/null
+++ b/app/views/ci/runners/_specific_runners.html.haml
@@ -0,0 +1,29 @@
+%h3 Specific runners
+
+.bs-callout.help-callout
+ %h4 How to setup a new project specific runner
+
+ %ol
+ %li
+ Install GitLab Runner software.
+ Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+ %li
+ Specify following URL during runner setup:
+ %code #{ci_root_url(only_path: false)}
+ %li
+ Use the following registration token during setup:
+ %code #{@project.token}
+ %li
+ Start runner!
+
+
+- if @runners.any?
+ %h4.underlined-title Runners activated for this project
+ %ul.bordered-list.activated-specific-runners
+ = render @runners
+
+- if @specific_runners.any?
+ %h4.underlined-title Available specific runners
+ %ul.bordered-list.available-specific-runners
+ = render @specific_runners
+ = paginate @specific_runners
diff --git a/app/views/ci/runners/edit.html.haml b/app/views/ci/runners/edit.html.haml
new file mode 100644
index 00000000000..81c8e58ae2b
--- /dev/null
+++ b/app/views/ci/runners/edit.html.haml
@@ -0,0 +1,27 @@
+%h4 Runner ##{@runner.id}
+%hr
+= form_for [:ci, @project, @runner], html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label :active, "Active", class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :active
+ %span.light Paused runners don't accept new builds
+ .form-group
+ = label_tag :token, class: 'control-label' do
+ Token
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', readonly: true
+ .form-group
+ = label_tag :description, class: 'control-label' do
+ Description
+ .col-sm-10
+ = f.text_field :description, class: 'form-control'
+ .form-group
+ = label_tag :tag_list, class: 'control-label' do
+ Tags
+ .col-sm-10
+ = f.text_field :tag_list, class: 'form-control'
+ .help-block You can setup jobs to only use runners with specific tags
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/ci/runners/index.html.haml b/app/views/ci/runners/index.html.haml
new file mode 100644
index 00000000000..529fb9c296d
--- /dev/null
+++ b/app/views/ci/runners/index.html.haml
@@ -0,0 +1,25 @@
+.light
+ %p
+ A 'runner' is a process which runs a build.
+ You can setup as many runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+
+ %p Each runner can be in one of the following states:
+ %div
+ %ul
+ %li
+ %span.label.label-success active
+ \- runner is active and can process any new build
+ %li
+ %span.label.label-danger paused
+ \- runner is paused and will not receive any new build
+
+%hr
+
+%p.lead To start serving your builds you can either add specific runners to your project or use shared runners
+.row
+ .col-sm-6
+ = render 'specific_runners'
+ .col-sm-6
+ = render 'shared_runners'
diff --git a/app/views/ci/runners/show.html.haml b/app/views/ci/runners/show.html.haml
new file mode 100644
index 00000000000..ffec495f85a
--- /dev/null
+++ b/app/views/ci/runners/show.html.haml
@@ -0,0 +1,64 @@
+= content_for :title do
+ %h3.project-title
+ Runner ##{@runner.id}
+ .pull-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
+
+%table.table
+ %thead
+ %tr
+ %th Property Name
+ %th Value
+ %tr
+ %td
+ Tags
+ %td
+ - @runner.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
+ %tr
+ %td
+ Name
+ %td
+ = @runner.name
+ %tr
+ %td
+ Version
+ %td
+ = @runner.version
+ %tr
+ %td
+ Revision
+ %td
+ = @runner.revision
+ %tr
+ %td
+ Platform
+ %td
+ = @runner.platform
+ %tr
+ %td
+ Architecture
+ %td
+ = @runner.architecture
+ %tr
+ %td
+ Description
+ %td
+ = @runner.description
+ %tr
+ %td
+ Last contact
+ %td
+ - if @runner.contacted_at
+ #{time_ago_in_words(@runner.contacted_at)} ago
+ - else
+ Never
+
+
+
diff --git a/app/views/ci/services/_form.html.haml b/app/views/ci/services/_form.html.haml
new file mode 100644
index 00000000000..9110aaa0528
--- /dev/null
+++ b/app/views/ci/services/_form.html.haml
@@ -0,0 +1,57 @@
+%h3.page-title
+ = @service.title
+ = boolean_to_icon @service.activated?
+
+%p= @service.description
+
+.back-link
+ = link_to ci_project_services_path(@project) do
+ &larr; to services
+
+%hr
+
+= form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f|
+ - if @service.errors.any?
+ .alert.alert-danger
+ %ul
+ - @service.errors.full_messages.each do |msg|
+ %li= msg
+
+ - if @service.help.present?
+ .bs-callout
+ = @service.help
+
+ .form-group
+ = f.label :active, "Active", class: "control-label"
+ .col-sm-10
+ = f.check_box :active
+
+ - @service.fields.each do |field|
+ - name = field[:name]
+ - label = field[:label] || name
+ - value = @service.send(name)
+ - type = field[:type]
+ - placeholder = field[:placeholder]
+ - choices = field[:choices]
+ - default_choice = field[:default_choice]
+ - help = field[:help]
+
+ .form-group
+ = f.label label, class: "control-label"
+ .col-sm-10
+ - if type == 'text'
+ = f.text_field name, class: "form-control", placeholder: placeholder
+ - elsif type == 'textarea'
+ = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder
+ - elsif type == 'checkbox'
+ = f.check_box name
+ - elsif type == 'select'
+ = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
+ - if help
+ .light #{help}
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+ &nbsp;
+ - if @service.valid? && @service.activated? && @service.can_test?
+ = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn'
diff --git a/app/views/ci/services/edit.html.haml b/app/views/ci/services/edit.html.haml
new file mode 100644
index 00000000000..bcc5832792f
--- /dev/null
+++ b/app/views/ci/services/edit.html.haml
@@ -0,0 +1 @@
+= render 'form'
diff --git a/app/views/ci/services/index.html.haml b/app/views/ci/services/index.html.haml
new file mode 100644
index 00000000000..37e5723b541
--- /dev/null
+++ b/app/views/ci/services/index.html.haml
@@ -0,0 +1,22 @@
+%h3.page-title Project services
+%p.light Project services allow you to integrate GitLab CI with other applications
+
+%table.table
+ %thead
+ %tr
+ %th
+ %th Service
+ %th Desription
+ %th Last edit
+ - @services.sort_by(&:title).each do |service|
+ %tr
+ %td
+ = boolean_to_icon service.activated?
+ %td
+ = link_to edit_ci_project_service_path(@project, service.to_param) do
+ %strong= service.title
+ %td
+ = service.description
+ %td.light
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml
new file mode 100644
index 00000000000..8a42f29b77c
--- /dev/null
+++ b/app/views/ci/shared/_guide.html.haml
@@ -0,0 +1,15 @@
+.bs-callout.help-callout
+ %h4 How to setup CI for this project
+
+ %ol
+ %li
+ Add at least one runner to the project.
+ Go to #{link_to 'Runners page', ci_project_runners_path(@project), target: :blank} for instructions.
+ %li
+ Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}.
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+ %li
+ Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank}
+ and press the "Test settings" button.
+ %li
+ Return to this page and refresh it, it should show a new build.
diff --git a/app/views/ci/shared/_no_runners.html.haml b/app/views/ci/shared/_no_runners.html.haml
new file mode 100644
index 00000000000..f56c37d9b37
--- /dev/null
+++ b/app/views/ci/shared/_no_runners.html.haml
@@ -0,0 +1,7 @@
+.alert.alert-danger
+ %p
+ Now you need Runners to process your builds.
+ %span
+ Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+
+
diff --git a/app/views/ci/triggers/_trigger.html.haml b/app/views/ci/triggers/_trigger.html.haml
new file mode 100644
index 00000000000..addfbfcb0d4
--- /dev/null
+++ b/app/views/ci/triggers/_trigger.html.haml
@@ -0,0 +1,14 @@
+%tr
+ %td
+ .clearfix
+ %span.monospace= trigger.token
+
+ %td
+ - if trigger.last_trigger_request
+ #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago
+ - else
+ Never
+
+ %td
+ .pull-right
+ = link_to 'Revoke', ci_project_trigger_path(@project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped"
diff --git a/app/views/ci/triggers/index.html.haml b/app/views/ci/triggers/index.html.haml
new file mode 100644
index 00000000000..44374a1a4d5
--- /dev/null
+++ b/app/views/ci/triggers/index.html.haml
@@ -0,0 +1,67 @@
+%h3.page-title
+ Triggers
+
+%p.light
+ Triggers can be used to force a rebuild of a specific branch or tag with an API call.
+
+%hr.clearfix
+
+-if @triggers.any?
+ %table.table
+ %thead
+ %th Token
+ %th Last used
+ %th
+ = render @triggers
+- else
+ %h4 No triggers
+
+= form_for [:ci, @project, @trigger], html: { class: 'form-horizontal' } do |f|
+ .clearfix
+ = f.submit "Add Trigger", class: 'btn btn-success pull-right'
+
+%hr.clearfix
+
+-if @triggers.any?
+ %h3
+ Use CURL
+
+ %p.light
+ Copy the token above and set your branch or tag name. This is the reference that will be rebuild.
+
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ #{ci_build_trigger_url(@project.id, 'REF_NAME')}
+ %h3
+ Use .gitlab-ci.yml
+
+ %p.light
+ Copy the snippet to
+ %i .gitlab-ci.yml
+ of dependent project.
+ At the end of your build it will trigger this project to rebuilt.
+
+ %pre
+ :plain
+ trigger:
+ type: deploy
+ script:
+ - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}"
+ %h3
+ Pass build variables
+
+ %p.light
+ Add
+ %strong variables[VARIABLE]=VALUE
+ to API request.
+ The value of variable could then be used to distinguish triggered build from normal one.
+
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F "variables[RUN_NIGHTLY_BUILD]=true" \
+ #{ci_build_trigger_url(@project.id, 'REF_NAME')}
diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml
new file mode 100644
index 00000000000..308b217ea78
--- /dev/null
+++ b/app/views/ci/user_sessions/new.html.haml
@@ -0,0 +1,8 @@
+.login-block
+ %h2 Login using GitLab account
+ %p.light
+ Make sure you have account on GitLab server
+ = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink
+ %hr
+ = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' )
+
diff --git a/app/views/ci/variables/show.html.haml b/app/views/ci/variables/show.html.haml
new file mode 100644
index 00000000000..ebf68341e08
--- /dev/null
+++ b/app/views/ci/variables/show.html.haml
@@ -0,0 +1,39 @@
+%h3.page-title
+ Secret Variables
+
+%p.light
+ These variables will be set to environment by the runner and will be hidden in the build log.
+ %br
+ So you can use them for passwords, secret keys or whatever you want.
+
+%hr
+
+
+= nested_form_for @project, url: url_for(controller: 'ci/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
+ - if @project.errors.any?
+ #error_explanation
+ %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
+ .alert.alert-error
+ %ul
+ - @project.errors.full_messages.each do |msg|
+ %li= msg
+
+ = f.fields_for :variables do |variable_form|
+ .form-group
+ = variable_form.label :key, 'Key', class: 'control-label'
+ .col-sm-10
+ = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE"
+
+ .form-group
+ = variable_form.label :value, 'Value', class: 'control-label'
+ .col-sm-10
+ = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: ""
+
+ = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10'
+ %hr
+ %p
+ .clearfix
+ = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right'
+
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url
diff --git a/app/views/ci/web_hooks/index.html.haml b/app/views/ci/web_hooks/index.html.haml
new file mode 100644
index 00000000000..78e8203b25e
--- /dev/null
+++ b/app/views/ci/web_hooks/index.html.haml
@@ -0,0 +1,92 @@
+%h3.page-title
+ Web hooks
+
+%p.light
+ Web Hooks can be used for binding events when build completed.
+
+%hr.clearfix
+
+= form_for [:ci, @project, @web_hook], html: { class: 'form-horizontal' } do |f|
+ -if @web_hook.errors.any?
+ .alert.alert-danger
+ - @web_hook.errors.full_messages.each do |msg|
+ %p= msg
+ .form-group
+ = f.label :url, "URL", class: 'control-label'
+ .col-sm-10
+ = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
+ .form-actions
+ = f.submit "Add Web Hook", class: "btn btn-create"
+
+-if @web_hooks.any?
+ %h4 Activated web hooks (#{@web_hooks.count})
+ %table.table
+ - @web_hooks.each do |hook|
+ %tr
+ %td
+ .clearfix
+ %span.monospace= hook.url
+ %td
+ .pull-right
+ - if @project.commits.any?
+ = link_to 'Test Hook', test_ci_project_web_hook_path(@project, hook), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', ci_project_web_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+
+%h4 Web Hook data example
+
+:erb
+ <pre>
+ <code>
+ {
+ "build_id": 2,
+ "build_name":"rspec_linux"
+ "build_status": "failed",
+ "build_started_at": "2014-05-05T18:01:02.563Z",
+ "build_finished_at": "2014-05-05T18:01:07.611Z",
+ "project_id": 1,
+ "project_name": "Brightbox \/ Brightbox Cli",
+ "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli",
+ "ref": "master",
+ "sha": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "push_data": {
+ "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "after": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "ref": "refs\/heads\/master",
+ "user_id": 1,
+ "user_name": "Administrator",
+ "project_id": 5,
+ "repository": {
+ "name": "Brightbox Cli",
+ "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git",
+ "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.",
+ "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli"
+ },
+ "commits": [
+ {
+ "id": "a26cf5de9ed9827746d4970872376b10d9325f40",
+ "message": "Release v1.2.2",
+ "timestamp": "2014-04-22T16:46:42+03:00",
+ "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40",
+ "author": {
+ "name": "Paul Thornthwaite",
+ "email": "tokengeek@gmail.com"
+ }
+ },
+ {
+ "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.",
+ "timestamp": "2014-04-11T18:17:26+03:00",
+ "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c",
+ "author": {
+ "name": "Paul Thornthwaite",
+ "email": "tokengeek@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 2,
+ "ci_yaml_file":"rspec_linux:\r\n script: ls\r\n"
+ }
+ }
+ </code>
+ </pre>
diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml
new file mode 100644
index 00000000000..24c68a6dbf5
--- /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 00000000000..c987ab876a3
--- /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_build.html.haml b/app/views/layouts/ci/_nav_build.html.haml
new file mode 100644
index 00000000000..732882726e7
--- /dev/null
+++ b/app/views/layouts/ci/_nav_build.html.haml
@@ -0,0 +1,3 @@
+= render 'layouts/ci/nav_project',
+ back_title: 'Back to project commit',
+ back_url: ci_project_ref_commits_path(@project, @commit.ref, @commit.sha)
diff --git a/app/views/layouts/ci/_nav_commit.haml b/app/views/layouts/ci/_nav_commit.haml
new file mode 100644
index 00000000000..19c526678d0
--- /dev/null
+++ b/app/views/layouts/ci/_nav_commit.haml
@@ -0,0 +1,3 @@
+= render 'layouts/ci/nav_project',
+ back_title: 'Back to project commits',
+ back_url: ci_project_path(@project)
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 00000000000..fcff405d19d
--- /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 00000000000..10b87e3a2b1
--- /dev/null
+++ b/app/views/layouts/ci/_nav_project.html.haml
@@ -0,0 +1,53 @@
+%ul.nav.nav-sidebar
+ = nav_link do
+ = link_to defined?(back_url) ? back_url : ci_root_path, title: defined?(back_title) ? back_title : 'Back to Dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = icon('caret-square-o-left fw')
+ %span= defined?(back_title) ? back_title : 'Back to Dashboard'
+ %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 00000000000..c598f63c4c8
--- /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 00000000000..c8cb185d28c
--- /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 00000000000..b9f871d5447
--- /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 00000000000..d404ecb894a
--- /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_build'
diff --git a/app/views/layouts/ci/commit.html.haml b/app/views/layouts/ci/commit.html.haml
new file mode 100644
index 00000000000..5727f1b8e3e
--- /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_commit'
diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml
new file mode 100644
index 00000000000..270b206df5e
--- /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 00000000000..15478c3f5bc
--- /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/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb
new file mode 100644
index 00000000000..ebb43570e2a
--- /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 00000000000..3bbb9b4bec7
--- /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 00000000000..0bb83845572
--- /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 a4895cf6586..d4578f6a222 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 00000000000..aab4f60ec60
--- /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 00000000000..e69de29bb2d
--- /dev/null
+++ b/builds/.gitkeep
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 03af7f07864..d7d6aed1602 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 9eb99dae456..b2bd8796004 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
@@ -256,7 +272,7 @@ production: &base
#
- # 3. Advanced settings
+ # 4. Advanced settings
# ==========================
# GitLab Satellites
@@ -315,7 +331,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 689c3f3049d..339419559d1 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
@@ -160,6 +172,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 6540ac839cb..00000000000
--- 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 00000000000..cac8edb32bf
--- /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 00000000000..d831a1838ed
--- /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 00000000000..43adac8b2c6
--- /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
index 8fd27b1d88e..f9f88f95db9 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
index 70ed10e8275..70ed10e8275 100644
--- a/config/initializers/7_omniauth.rb
+++ b/config/initializers/omniauth.rb
diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example
index b1bbcca1d61..2155ea14562 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
index 1d958904e8f..7710eeac453 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 62a54bc8c63..1b518c3becf 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 88651394d1d..04ed9e90df5 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
index e856499732e..e856499732e 100644
--- a/config/initializers/4_sidekiq.rb
+++ b/config/initializers/sidekiq.rb
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index d9042c652bb..e6d5600edb7 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 f3db5b7476e..d8bf0878a3d 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 54e109f34fa..41970d2af8a 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',
diff --git a/config/schedule.rb b/config/schedule.rb
new file mode 100644
index 00000000000..8122f7cc69c
--- /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 00000000000..6b408ac6031
--- /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 00000000000..c691db67c6c
--- /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 00000000000..c4f51363e57
--- /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 00000000000..df3390e8a82
--- /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/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 2b7afae6d7c..73605d4c5e3 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 55cbd8c293e..5fd764bf698 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: 20150914215247) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -72,6 +72,213 @@ 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"], 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 337c4e6a62d..f5f1f56b1e2 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 00000000000..97325069ceb
--- /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 00000000000..e47e5c46732
--- /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 00000000000..3b5008ccdb4
--- /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 00000000000..4df7afc6c52
--- /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 00000000000..8f32e2d3b40
--- /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 00000000000..54584db0938
--- /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 00000000000..e9f88ee066e
--- /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 00000000000..ffd841ca9e7
--- /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 00000000000..84eaf29efd1
--- /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 00000000000..a698fbc8184
--- /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 00000000000..191e3a8144d
--- /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 <<EOF > 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 00000000000..e0b9fa0e25d
--- /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 00000000000..036b03dd6b9
--- /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 00000000000..d2a872f1934
--- /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 00000000000..eaee94a10f1
--- /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 00000000000..d77061c14cd
--- /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 00000000000..a87a1f806fc
--- /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:
+
+![Projects](projects.png)
+
+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:
+
+![](new_commit.png)
+
+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**:
+
+![](runners_activated.png)
+
+### 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**.
+
+![](commit_status.png)
+
+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 00000000000..333259e6acd
--- /dev/null
+++ b/doc/ci/quick_start/build_status.png
Binary files differ
diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/commit_status.png
new file mode 100644
index 00000000000..725b79e6f91
--- /dev/null
+++ b/doc/ci/quick_start/commit_status.png
Binary files differ
diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/new_commit.png
new file mode 100644
index 00000000000..3839e893c17
--- /dev/null
+++ b/doc/ci/quick_start/new_commit.png
Binary files differ
diff --git a/doc/ci/quick_start/projects.png b/doc/ci/quick_start/projects.png
new file mode 100644
index 00000000000..0b3430a69db
--- /dev/null
+++ b/doc/ci/quick_start/projects.png
Binary files differ
diff --git a/doc/ci/quick_start/runners.png b/doc/ci/quick_start/runners.png
new file mode 100644
index 00000000000..25b4046bc00
--- /dev/null
+++ b/doc/ci/quick_start/runners.png
Binary files differ
diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/runners_activated.png
new file mode 100644
index 00000000000..c934bd12f41
--- /dev/null
+++ b/doc/ci/quick_start/runners_activated.png
Binary files differ
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
new file mode 100644
index 00000000000..68dcfe23ffb
--- /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.
+
+![shared token](shared_runner.png)
+
+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.
+
+![project runners in GitLab CI](project_specific.png)
+
+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.
+
+![making a shared runner specific](shared_to_specific_admin.png)
+
+## 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 00000000000..f51ea694e78
--- /dev/null
+++ b/doc/ci/runners/project_specific.png
Binary files differ
diff --git a/doc/ci/runners/shared_runner.png b/doc/ci/runners/shared_runner.png
new file mode 100644
index 00000000000..9755144eb08
--- /dev/null
+++ b/doc/ci/runners/shared_runner.png
Binary files 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 00000000000..44a4bef22f7
--- /dev/null
+++ b/doc/ci/runners/shared_to_specific_admin.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
new file mode 100644
index 00000000000..04c6bf1e3a3
--- /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 00000000000..4caeccacb7f
--- /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 ee13b0f2537..8936697b40e 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -221,6 +221,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/
@@ -234,6 +238,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/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
new file mode 100644
index 00000000000..e12ea9a9ad7
--- /dev/null
+++ b/doc/migrate_ci_to_ce/README.md
@@ -0,0 +1,261 @@
+## 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 <<EOF | bundle exec rails dbconsole production
+ ALTER TABLE application_settings RENAME TO ci_application_settings;
+ ALTER TABLE builds RENAME TO ci_builds;
+ ALTER TABLE commits RENAME TO ci_commits;
+ ALTER TABLE events RENAME TO ci_events;
+ ALTER TABLE jobs RENAME TO ci_jobs;
+ ALTER TABLE projects RENAME TO ci_projects;
+ ALTER TABLE runner_projects RENAME TO ci_runner_projects;
+ ALTER TABLE runners RENAME TO ci_runners;
+ ALTER TABLE services RENAME TO ci_services;
+ ALTER TABLE tags RENAME TO ci_tags;
+ ALTER TABLE taggings RENAME TO ci_taggings;
+ ALTER TABLE trigger_requests RENAME TO ci_trigger_requests;
+ ALTER TABLE triggers RENAME TO ci_triggers;
+ ALTER TABLE variables RENAME TO ci_variables;
+ ALTER TABLE web_hooks RENAME TO ci_web_hooks;
+ EOF
+
+### 4. Dump GitLab CI database [CI]
+
+First check used database and credentials on GitLab CI and GitLab CE/EE:
+
+1. To check it on GitLab CI:
+
+ cat /home/gitlab_ci/gitlab-ci/config/database.yml
+
+1. To check it on GitLab CE/EE:
+
+ cat /home/git/gitlab/config/database.yml
+
+Please first check the database engine used for GitLab CI and GitLab CE/EE.
+
+1. If your GitLab CI uses **mysql2** and GitLab CE/EE uses it too.
+Please follow **Dump MySQL** guide.
+
+1. If your GitLab CI uses **postgres** and GitLab CE/EE uses **postgres**.
+Please follow **Dump PostgreSQL** guide.
+
+1. If your GitLab CI uses **mysql2** and GitLab CE/EE uses **postgres**.
+Please follow **Dump MySQL and migrate to PostgreSQL** guide.
+
+**Remember credentials stored for accessing GitLab CI.
+You will need to put these credentials into commands executed below.**
+
+ $ cat config/database.yml [10:06:55]
+ #
+ # PRODUCTION
+ #
+ production:
+ adapter: postgresql or mysql2
+ encoding: utf8
+ reconnect: false
+ database: GITLAB_CI_DATABASE
+ pool: 5
+ username: DB_USERNAME
+ password: DB_PASSWORD
+ host: DB_HOSTNAME
+ port: DB_PORT
+ # socket: /tmp/mysql.sock
+
+#### a. Dump MySQL
+
+ mysqldump --default-character-set=utf8 --complete-insert --no-create-info \
+ --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
+
+#### 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;
+ }
+
+ # 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 c1ed9e3b80e..c56e99a7005 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 5d36f5121d1..e9187298d79 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
```
+5. Edit the init script configuration at `/etc/default/gitlab` to enable `mail_room`:
-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`:
-
- ```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 00000000000..b8ab07d9fe1
--- /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 3ae0f9616ac..59415e98782 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -91,7 +91,18 @@ 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 gitlab_ci -H cp config/secrets.yml.example config/secrets.yml
+sudo -u gitlab_ci -H chmod 0600 config/secrets.yml
+
+### 7. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -112,7 +123,7 @@ 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`
@@ -122,6 +133,8 @@ There are new configuration options available for [`gitlab.yml`](config/gitlab.y
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.
+
#### New Nginx configuration
Because of the new `gitlab-git-http-server` you need to update your Nginx
@@ -139,12 +152,17 @@ git diff origin/7-14-stable:lib/support/nginx/gitlab-ssl origin/8-0-stable:lib/s
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:
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 8dddcd7ccc3..33b6224a810 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 76c9cc2e3a4..7fada98fcdc 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 7412274b045..63ea2f05438 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 1f2251c9b9c..c2fb36b4143 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 d170b3067ed..6727e80ac1e 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 00000000000..6f56f680bb9
--- /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 13c68d9354f..ac63f89c6ec 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 00000000000..ac6d667cf8d
--- /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("<", "&lt;"))
+ 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 << %{<span class="#{css_classes.join(' ')}">}
+ @n_open_tags += 1
+ end
+
+ def close_open_tags
+ while @n_open_tags > 0
+ @out << %{</span>}
+ @n_open_tags -= 1
+ end
+ end
+
+ def reset
+ @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 00000000000..172c6f22164
--- /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 00000000000..83ca1e6481c
--- /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 00000000000..bac463a5909
--- /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 00000000000..f47bc1236b8
--- /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 00000000000..152883a599f
--- /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 00000000000..9197f917d73
--- /dev/null
+++ b/lib/ci/api/helpers.rb
@@ -0,0 +1,33 @@
+module Ci
+ module API
+ module Helpers
+ 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 00000000000..66bcf65e8c4
--- /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 00000000000..1466fe4356e
--- /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 00000000000..40907d6db54
--- /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 00000000000..e69de29bb2d
--- /dev/null
+++ b/lib/ci/assets/.gitkeep
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
new file mode 100644
index 00000000000..915a4f526a6
--- /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 00000000000..fd78b024970
--- /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 00000000000..7acc3f38edb
--- /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 00000000000..e625e790df8
--- /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 00000000000..74f592dcaea
--- /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 00000000000..f4114c698d2
--- /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 00000000000..c42a0ad36db
--- /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 00000000000..ee0958f4be1
--- /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 00000000000..bb2bdbed495
--- /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 00000000000..2a87c91db5e
--- /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/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
index 8613150894b..bb496135d92 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 5696b4fa585..fcbb2e936a5 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 3d7445a27f1..1e5cb12071e 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 48248f5219d..ecbd263d0e0 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 9e1aab936cb..e2cf89cb1d8 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 1871e52df0e..6f436ea7167 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/url_builder.rb b/lib/gitlab/url_builder.rb
index 779819bc2bf..6f0d02cafd1 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 00000000000..bf05edfd780
--- /dev/null
+++ b/lib/support/nginx/gitlab_ci
@@ -0,0 +1,29 @@
+# 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;
+ }
+
+ # 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 00000000000..e69de29bb2d
--- /dev/null
+++ b/lib/tasks/ci/.gitkeep
diff --git a/lib/tasks/ci/cleanup.rake b/lib/tasks/ci/cleanup.rake
new file mode 100644
index 00000000000..2f4d11bd942
--- /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 00000000000..e7d41874a11
--- /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 00000000000..49435504c67
--- /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 4c73f90bbf2..f20c7f71ba5 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 00000000000..922e28bf696
--- /dev/null
+++ b/public/ci/build-canceled.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">canceled</text><text x="66" y="14">canceled</text></g></svg> \ 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 00000000000..1aefd3f1761
--- /dev/null
+++ b/public/ci/build-failed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="78" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#e05d44" d="M37 0h41v20H37z"/><path fill="url(#b)" d="M0 0h78v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="56.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="56.5" y="14">failed</text></g></svg> \ 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 00000000000..536931af84d
--- /dev/null
+++ b/public/ci/build-pending.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="92" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h55v20H37z"/><path fill="url(#b)" d="M0 0h92v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63.5" y="15" fill="#010101" fill-opacity=".3">pending</text><text x="63.5" y="14">pending</text></g></svg> \ 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 00000000000..0d71eef3c34
--- /dev/null
+++ b/public/ci/build-running.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h53v20H37z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">running</text><text x="62.5" y="14">running</text></g></svg> \ 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 00000000000..43b67e45f42
--- /dev/null
+++ b/public/ci/build-success.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="91" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#4c1" d="M37 0h54v20H37z"/><path fill="url(#b)" d="M0 0h91v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg> \ 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 00000000000..c72a2f5a7f5
--- /dev/null
+++ b/public/ci/build-unknown.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="98" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="98" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h61v20H37z"/><path fill="url(#b)" d="M0 0h98v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66.5" y="15" fill="#010101" fill-opacity=".3">unknown</text><text x="66.5" y="14">unknown</text></g></svg> \ No newline at end of file
diff --git a/public/ci/favicon.ico b/public/ci/favicon.ico
new file mode 100644
index 00000000000..9663d4d00b9
--- /dev/null
+++ b/public/ci/favicon.ico
Binary files differ
diff --git a/scripts/ci/prepare_build.sh b/scripts/ci/prepare_build.sh
new file mode 100755
index 00000000000..864a683a1bd
--- /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 00000000000..b71e7505731
--- /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 00000000000..015788a05e1
--- /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 00000000000..99da5a18776
--- /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 00000000000..70930c789c3
--- /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 00000000000..9638618a400
--- /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 00000000000..e6bd0685f8d
--- /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 00000000000..3aa14ca434d
--- /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 00000000000..db759eca9ac
--- /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 00000000000..db053c610cd
--- /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 00000000000..fd3afdb1ec2
--- /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 00000000000..40d878ecb3c
--- /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 00000000000..88ef9c144af
--- /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 00000000000..a7e75cc4f6b
--- /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 00000000000..b88f55a6807
--- /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 00000000000..b25121f0806
--- /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 00000000000..2f020e524e2
--- /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 00000000000..40a62ca4574
--- /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 00000000000..5b9fd404159
--- /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 00000000000..5d8f56e2cfb
--- /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 00000000000..ff17aeca447
--- /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 00000000000..15147f15eb3
--- /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 00000000000..c6afeb74628
--- /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 00000000000..e387b3be555
--- /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 00000000000..6a216715b7f
--- /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 00000000000..6d0e2d3d1e1
--- /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 00000000000..75c023bbc43
--- /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('<span class="term-fg-red">Hello</span>')
+ end
+
+ it "prints simply red without trailing reset" do
+ expect(subject.convert("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>')
+ end
+
+ it "prints simply yellow" do
+ expect(subject.convert("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>')
+ end
+
+ it "prints default on blue" do
+ expect(subject.convert("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>')
+ end
+
+ it "prints red on blue" do
+ expect(subject.convert("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
+ end
+
+ it "resets colors after red on blue" do
+ expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
+ end
+
+ it "performs color change from red/blue to yellow/blue" do
+ expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
+ end
+
+ it "performs color change from red/blue to yellow/green" do
+ expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
+ 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('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
+ 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('<span class="term-fg-l-red">Hello</span>')
+ end
+
+ it "prints default on light red" do
+ expect(subject.convert("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>')
+ end
+
+ it "performs color change from red/blue to default/blue" do
+ expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ end
+
+ it "performs color change from light red/blue to default/blue" do
+ expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ end
+
+ it "prints bold text" do
+ expect(subject.convert("\e[1mHello")).to eq('<span class="term-bold">Hello</span>')
+ end
+
+ it "resets bold text" do
+ expect(subject.convert("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world')
+ expect(subject.convert("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world')
+ end
+
+ it "prints italic text" do
+ expect(subject.convert("\e[3mHello")).to eq('<span class="term-italic">Hello</span>')
+ end
+
+ it "resets italic text" do
+ expect(subject.convert("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world')
+ end
+
+ it "prints underlined text" do
+ expect(subject.convert("\e[4mHello")).to eq('<span class="term-underline">Hello</span>')
+ end
+
+ it "resets underlined text" do
+ expect(subject.convert("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world')
+ end
+
+ it "prints concealed text" do
+ expect(subject.convert("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>')
+ end
+
+ it "resets concealed text" do
+ expect(subject.convert("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world')
+ end
+
+ it "prints crossed-out text" do
+ expect(subject.convert("\e[9mHello")).to eq('<span class="term-cross">Hello</span>')
+ end
+
+ it "resets crossed-out text" do
+ expect(subject.convert("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world')
+ end
+
+ it "can print 256 xterm fg colors" do
+ expect(subject.convert("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>')
+ end
+
+ it "can print 256 xterm fg colors on normal magenta background" do
+ expect(subject.convert("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
+ end
+
+ it "can print 256 xterm bg colors" do
+ expect(subject.convert("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>')
+ end
+
+ it "can print 256 xterm bg colors on normal magenta foreground" do
+ expect(subject.convert("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
+ end
+
+ it "prints bold colored text vividly" do
+ expect(subject.convert("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ end
+
+ it "prints bold light colored text correctly" do
+ expect(subject.convert("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ end
+end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
new file mode 100644
index 00000000000..24894e81983
--- /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 00000000000..49482ac2b12
--- /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 9c115bbfc6a..48bc60eed16 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/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb
new file mode 100644
index 00000000000..20d8ddcd135
--- /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 00000000000..ce801152042
--- /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 00000000000..586c9dc23a7
--- /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 00000000000..b5f37b349db
--- /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 00000000000..49ac0860259
--- /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 00000000000..063d46b84d4
--- /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 00000000000..f5335903728
--- /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 00000000000..0524f472432
--- /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 00000000000..1025868da6e
--- /dev/null
+++ b/spec/models/ci/project_spec.rb
@@ -0,0 +1,181 @@
+# == 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
+
+ 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 00000000000..0218d484130
--- /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 00000000000..757593a7ab8
--- /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 00000000000..2c575056b08
--- /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 00000000000..19c14ef2da2
--- /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 00000000000..97a3d0081f4
--- /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 00000000000..c4c0b007c11
--- /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 942768fa254..35b3d3e296a 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 00000000000..c25d1823306
--- /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 00000000000..e89b6651499
--- /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 00000000000..37fa1e82f25
--- /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 00000000000..2adae52e79e
--- /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 00000000000..11dc089e1f5
--- /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 00000000000..ff6fdbdd6f1
--- /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 00000000000..998c386ead4
--- /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 00000000000..fb317670339
--- /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 00000000000..38d9943765a
--- /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 00000000000..64041b8d5a2
--- /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 00000000000..d12cd9773dc
--- /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 00000000000..9b330a90ae2
--- /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 00000000000..7565eb8f032
--- /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 00000000000..7b5af6c3dd0
--- /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 00000000000..cebdd145e40
--- /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 0780c4f3203..dfe855926c6 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 f63322776d4..1b3cafb497c 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 755964e9a3d..203117aee70 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 00000000000..3482145404e
--- /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 00000000000..f0a9fce859c
--- /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 00000000000..93d51406d63
--- /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 00000000000..ca42c14c5d8
--- /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 <canvas> tag ","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/charts-js.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/charts-js.git","web_url":"http://demo.gitlab.com/sandbox/charts-js","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Charts.js","name_with_namespace":"Sandbox / Charts.js","path":"charts-js","path_with_namespace":"sandbox/charts-js","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:47:29Z","last_activity_at":"2013-12-02T15:18:11Z","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":13,"description":"","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/afro.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/afro.git","web_url":"http://demo.gitlab.com/sandbox/afro","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Afro","name_with_namespace":"Sandbox / Afro","path":"afro","path_with_namespace":"sandbox/afro","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-11-14T17:45:19Z","last_activity_at":"2013-12-02T17:41:45Z","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"}}] \ No newline at end of file
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
new file mode 100644
index 00000000000..ce8dfe5ae75
--- /dev/null
+++ b/spec/support/gitlab_stubs/session.json
@@ -0,0 +1,20 @@
+{
+ "id":2,
+ "username":"jsmith",
+ "email":"test@test.com",
+ "name":"John Smith",
+ "bio":"",
+ "skype":"aertert",
+ "linkedin":"",
+ "twitter":"",
+ "theme_id":2,"color_scheme_id":2,
+ "state":"active",
+ "created_at":"2012-12-21T13:02:20Z",
+ "extern_uid":null,
+ "provider":null,
+ "is_admin":false,
+ "can_create_group":false,
+ "can_create_project":false,
+ "private_token":"Wvjy2Krpb7y8xi93owUz",
+ "access_token":"Wvjy2Krpb7y8xi93owUz"
+} \ No newline at end of file
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
new file mode 100644
index 00000000000..ce8dfe5ae75
--- /dev/null
+++ b/spec/support/gitlab_stubs/user.json
@@ -0,0 +1,20 @@
+{
+ "id":2,
+ "username":"jsmith",
+ "email":"test@test.com",
+ "name":"John Smith",
+ "bio":"",
+ "skype":"aertert",
+ "linkedin":"",
+ "twitter":"",
+ "theme_id":2,"color_scheme_id":2,
+ "state":"active",
+ "created_at":"2012-12-21T13:02:20Z",
+ "extern_uid":null,
+ "provider":null,
+ "is_admin":false,
+ "can_create_group":false,
+ "can_create_project":false,
+ "private_token":"Wvjy2Krpb7y8xi93owUz",
+ "access_token":"Wvjy2Krpb7y8xi93owUz"
+} \ No newline at end of file
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index ffe30a4246c..cd9fdc6f18e 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -44,4 +44,8 @@ module LoginHelpers
def logout_direct
page.driver.submit :delete, '/users/sign_out', {}
end
+
+ def skip_ci_admin_auth
+ allow_any_instance_of(Ci::Admin::ApplicationController).to receive_messages(authenticate_admin!: true)
+ end
end
diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb
new file mode 100644
index 00000000000..a3e59646187
--- /dev/null
+++ b/spec/support/setup_builds_storage.rb
@@ -0,0 +1,17 @@
+RSpec.configure do |config|
+ def builds_path
+ Rails.root.join('tmp/builds')
+ end
+
+ config.before(:each) do
+ FileUtils.mkdir_p(builds_path)
+ FileUtils.touch(File.join(builds_path, ".gitkeep"))
+ Settings.gitlab_ci['builds_path'] = builds_path
+ end
+
+ config.after(:suite) do
+ Dir.chdir(builds_path) do
+ `ls | grep -v .gitkeep | xargs rm -r`
+ end
+ end
+end
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
new file mode 100644
index 00000000000..5e6744afda1
--- /dev/null
+++ b/spec/support/stub_gitlab_calls.rb
@@ -0,0 +1,77 @@
+module StubGitlabCalls
+ def stub_gitlab_calls
+ stub_session
+ stub_user
+ stub_project_8
+ stub_project_8_hooks
+ stub_projects
+ stub_projects_owned
+ stub_ci_enable
+ end
+
+ def stub_js_gitlab_calls
+ allow_any_instance_of(Network).to receive(:projects) { project_hash_array }
+ end
+
+ private
+
+ def gitlab_url
+ Gitlab.config.gitlab.url
+ end
+
+ def stub_session
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json'))
+
+ stub_request(:post, "#{gitlab_url}api/v3/session.json").
+ with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}",
+ headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 201, body: f, headers: { 'Content-Type'=>'application/json' })
+ end
+
+ def stub_user
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json'))
+
+ stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+
+ stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+ end
+
+ def stub_project_8
+ data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8.json'))
+ allow_any_instance_of(Network).to receive(:project).and_return(JSON.parse(data))
+ end
+
+ def stub_project_8_hooks
+ data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8_hooks.json'))
+ allow_any_instance_of(Network).to receive(:project_hooks).and_return(JSON.parse(data))
+ end
+
+ def stub_projects
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
+
+ stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+ end
+
+ def stub_projects_owned
+ stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: "", headers: {})
+ end
+
+ def stub_ci_enable
+ stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: "", headers: {})
+ end
+
+ def project_hash_array
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
+ JSON.parse f
+ end
+end
diff --git a/spec/support/stub_gitlab_data.rb b/spec/support/stub_gitlab_data.rb
new file mode 100644
index 00000000000..fa402f35b95
--- /dev/null
+++ b/spec/support/stub_gitlab_data.rb
@@ -0,0 +1,5 @@
+module StubGitlabData
+ def gitlab_ci_yaml
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 23f322e0a62..2e63e5f36af 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads}.each do |subtask|
+ %w{db repo uploads builds}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -54,6 +54,7 @@ describe 'gitlab:app namespace rake task' do
and_return({ gitlab_version: gitlab_version })
expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
@@ -111,18 +112,19 @@ describe 'gitlab:app namespace rake task' do
it 'should set correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads repositories}
+ %W{tar -tvf #{@backup_tar} db uploads repositories builds}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
expect(tar_contents).to match('uploads/')
expect(tar_contents).to match('repositories/')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories)\/$/)
+ expect(tar_contents).to match('builds/')
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories|builds)\/$/)
end
it 'should delete temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds}')
)
expect(temp_dirs).to be_empty
@@ -158,11 +160,12 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads repositories}
+ %W{tar -tvf #{@backup_tar} db uploads repositories builds}
)
expect(tar_contents).to match('db/')
expect(tar_contents).to match('uploads/')
+ expect(tar_contents).to match('builds/')
expect(tar_contents).not_to match('repositories/')
end
@@ -173,6 +176,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
+ expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end