Commit ab8f7de0 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'jenkins_gitlab_plugin' into 'master'

Use Jenkins GitLab Plugin

Fixes #106

Deprecate the existing Jenkins service, which uses GitLab Hook plugin.
Adds support for GitLab Plugin which utilizes the GitLab commit status
API. The overall experience is much better.

See merge request !79
parents b8dfe596 3e38e841
......@@ -10,7 +10,8 @@ class Projects::ServicesController < Projects::ApplicationController
:jira_issue_transition_id, :build_events, :notify_only_broken_builds, :add_pusher,
:notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:multiproject_enabled, :pass_unstable]
:multiproject_enabled, :pass_unstable,
:jenkins_url, :project_name]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
......
......@@ -95,6 +95,7 @@ class Project < ActiveRecord::Base
has_one :gemnasium_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
has_one :jenkins_service, dependent: :destroy
has_one :jenkins_deprecated_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy
......
# == 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 'uri'
class JenkinsDeprecatedService < CiService
prop_accessor :project_url
prop_accessor :multiproject_enabled
prop_accessor :pass_unstable
validates :project_url, presence: true, if: :activated?
delegate :execute, to: :service_hook, prefix: nil
after_save :compose_service_hook, if: :activated?
def compose_service_hook
hook = service_hook || build_service_hook
jenkins_url = project_url.sub(/job\/.*/, '')
hook.url = jenkins_url + "/gitlab/build_now"
hook.save
end
def title
'Jenkins CI (Deprecated)'
end
def description
'An extendable open source continuous integration server'
end
def help
'You must have installed GitLab Hook plugin into Jenkins. This service ' \
'is deprecated. Use "Jenkins CI" service instead.'
end
def to_param
'jenkins_deprecated'
end
def fields
[
{ type: 'text', name: 'project_url', placeholder: 'Jenkins project URL like http://jenkins.example.com/job/my-project/' },
{ type: 'checkbox', name: 'multiproject_enabled', title: "Multi-project setup enabled?",
help: "Multi-project mode is configured in Jenkins Gitlab Hook plugin." },
{ type: 'checkbox', name: 'pass_unstable', title: 'Should unstable builds be treated as passing?',
help: 'Unstable builds will be treated as passing.' }
]
end
def multiproject_enabled?
self.multiproject_enabled == '1'
end
def pass_unstable?
self.pass_unstable == '1'
end
def build_page(sha, ref = nil)
if multiproject_enabled? && ref.present?
URI.encode("#{base_project_url}/#{project.name}_#{ref.tr('/', '_')}/scm/bySHA1/#{sha}").to_s
else
"#{project_url}/scm/bySHA1/#{sha}"
end
end
# When multi-project is enabled we need to have a different URL. Rather than
# relying on the user to provide the proper URL depending on multi-project
# we just parse the URL and make sure it's how we want it.
def base_project_url
url = URI.parse(project_url)
URI.join(url, '/job').to_s
end
def commit_status(sha, ref = nil)
parsed_url = URI.parse(build_page(sha, ref))
if parsed_url.userinfo.blank?
response = HTTParty.get(build_page(sha, ref), verify: false)
else
get_url = build_page(sha, ref).gsub("#{parsed_url.userinfo}@", "")
auth = {
username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password),
}
response = HTTParty.get(get_url, verify: false, basic_auth: auth)
end
if response.code == 200
# img.build-caption-status-icon for old jenkins version
src = Nokogiri.parse(response).css('img.build-caption-status-icon,.build-caption>img').first.attributes['src'].value
if src =~ /blue\.png$/ || (src =~ /yellow\.png/ && pass_unstable?)
'success'
elsif src =~ /(red|aborted|yellow)\.png$/
'failed'
elsif src =~ /anime\.gif$/
'running'
else
'pending'
end
else
:error
end
end
end
......@@ -2,113 +2,113 @@
#
# 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
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
# push_events :boolean default(TRUE)
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
#
require 'uri'
class JenkinsService < CiService
prop_accessor :project_url
prop_accessor :multiproject_enabled
prop_accessor :pass_unstable
include HTTParty
prop_accessor :jenkins_url, :project_name, :username, :password
validates :project_url, presence: true, if: :activated?
before_update :reset_password
validates :username,
presence: true,
if: ->(service) { service.activated? && service.password.present? }
delegate :execute, to: :service_hook, prefix: nil
default_value_for :push_events, true
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
after_save :compose_service_hook, if: :activated?
def reset_password
# don't reset the password if a new one is provided
if jenkins_url_changed? && !password_touched?
self.password = nil
end
end
def compose_service_hook
hook = service_hook || build_service_hook
jenkins_url = project_url.sub(/job\/.*/, '')
hook.url = jenkins_url + "/gitlab/build_now"
hook.url = hook_url
hook.save
end
def title
'Jenkins CI'
def execute(data)
self.post(
hook_url,
body: data.to_json,
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Basic #{auth}"
}
)
end
def description
'An extendable open source continuous integration server'
end
def test(data)
begin
result = execute(data)
message = result.message || result unless result.nil?
return { success: false, result: message } if result.code != 200
rescue StandardError => error
return { success: false, result: error }
end
def help
'You must have installed GitLab Hook plugin into Jenkins.'
{ success: true, result: result }
end
def to_param
'jenkins'
def auth
require 'base64'
Base64.urlsafe_encode64("#{username}:#{password}")
end
def fields
[
{ type: 'text', name: 'project_url', placeholder: 'Jenkins project URL like http://jenkins.example.com/job/my-project/' },
{ type: 'checkbox', name: 'multiproject_enabled', title: "Multi-project setup enabled?",
help: "Multi-project mode is configured in Jenkins Gitlab Hook plugin." },
{ type: 'checkbox', name: 'pass_unstable', title: 'Should unstable builds be treated as passing?',
help: 'Unstable builds will be treated as passing.' }
]
def hook_url
File.join(jenkins_url, "project/#{project_name}").to_s
end
def multiproject_enabled?
self.multiproject_enabled == '1'
def supported_events
%w(push merge_request tag_push)
end
def pass_unstable?
self.pass_unstable == '1'
def title
'Jenkins CI'
end
def build_page(sha, ref = nil)
if multiproject_enabled? && ref.present?
URI.encode("#{base_project_url}/#{project.name}_#{ref.tr('/', '_')}/scm/bySHA1/#{sha}").to_s
else
"#{project_url}/scm/bySHA1/#{sha}"
end
def description
'An extendable open source continuous integration server'
end
# When multi-project is enabled we need to have a different URL. Rather than
# relying on the user to provide the proper URL depending on multi-project
# we just parse the URL and make sure it's how we want it.
def base_project_url
url = URI.parse(project_url)
URI.join(url, '/job').to_s
def help
'You must have installed the Git Plugin and GitLab Plugin in Jenkins'
end
def commit_status(sha, ref = nil)
parsed_url = URI.parse(build_page(sha, ref))
if parsed_url.userinfo.blank?
response = HTTParty.get(build_page(sha, ref), verify: false)
else
get_url = build_page(sha, ref).gsub("#{parsed_url.userinfo}@", "")
auth = {
username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password),
}
response = HTTParty.get(get_url, verify: false, basic_auth: auth)
end
def to_param
'jenkins'
end
if response.code == 200
# img.build-caption-status-icon for old jenkins version
src = Nokogiri.parse(response).css('img.build-caption-status-icon,.build-caption>img').first.attributes['src'].value
if src =~ /blue\.png$/ || (src =~ /yellow\.png/ && pass_unstable?)
'success'
elsif src =~ /(red|aborted|yellow)\.png$/
'failed'
elsif src =~ /anime\.gif$/
'running'
else
'pending'
end
else
:error
end
def fields
[
{
type: 'text', name: 'jenkins_url',
placeholder: 'Jenkins URL like http://jenkins.example.com'
},
{
type: 'text', name: 'project_name', placeholder: 'Project Name',
help: 'The URL-friendly project name. Example: my_project_name'
},
{ type: 'text', name: 'username' },
{ type: 'password', name: 'password' },
]
end
end
......@@ -191,6 +191,7 @@ class Service < ActiveRecord::Base
hipchat
irker
jenkins
jenkins_deprecated
jira
pivotaltracker
pushover
......
class RenameJenkinsService < ActiveRecord::Migration
def up
execute "UPDATE services SET type = 'JenkinsDeprecatedService' WHERE type = 'JenkinsService';"
end
def down
execute "UPDATE services SET type = 'JenkinsService' WHERE type = 'JenkinsDeprecatedService';"
end
end
# Jenkins CI integration
GitLab can be configured to interact with Jenkins
In GitLab 8.3, Jenkins integration using the
[GitLab Hook Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Hook+Plugin)
was deprecated in favor of the
[GitLab Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Plugin).
The deprecated integration has been renamed to 'Jenkins CI (Deprecated)' in the
project service settings. We may remove this in a future release and recommend
using the new 'Jenkins CI' project service instead. See
[documentation for 'Jenkins CI (Deprecated)'](#jenkins-ci-deprecated-service)
below.
Integration includes:
## Jenkins CI Service
Integration includes:
* Trigger a Jenkins build after push to a repository and/or when a merge request
is created
* Show build status on Merge Request page, on each commit and on the project
home page
### Requirements:
* [Jenkins GitLab Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Plugin)
* [Jenkins Git Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin)
* Git clone access for Jenkins from the GitLab repository
* GitLab API access to report build status
### Configure GitLab users
Create a user or choose an existing user that Jenkins will use to interact
through the GitLab API. This user will need to be a global Admin or added
as a member to each Group/Project. Reporter permission is all that's required
for cloning, building, and reporting build status. Some features of the GitLab
Plugin may require additional privileges. For example, there is an option to
accept a merge request if the build is successful. Using this feature would
require developer, master or owner-level permission.
Copy the private API token from **Profile Settings -> Account**. You will need this
when configuring the Jenkins server later.
### Configure the Jenkins server
Install [Jenkins GitLab Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Plugin)
and [Jenkins Git Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin).
Go to Manage Jenkins -> Configure System and scroll down to the 'GitLab' section.
Enter the GitLab server URL in the 'GitLab host URL' field and paste the API token
copied earlier in the 'API Token' field.
![Jenkins GitLab plugin configuration](jenkins_gitlab_plugin_config.png)
### Configure a Jenkins project
Follow the GitLab Plugin documentation under the
[Using it With a Job](https://github.com/jenkinsci/gitlab-plugin#using-it-with-a-job)
heading. You *do not* need to complete instructions under the 'GitLab
Configuration (>= 8.0)'. Be sure to check the 'Use GitLab CI features' checkbox
as described under the 'GitLab Configuration (>= 8.1)'.
### Configure a GitLab project
Create a new GitLab project or choose an existing one. Then, go to **Services ->
Jenkins CI**.
Check the 'Active' box. Select whether you want GitLab to trigger a build
on push, Merge Request creation, tag push, or any combination of these. We
recommend unchecking 'Merge Request events' unless you have a specific use-case
that requires re-building a commit when a merge request is created. With 'Push
events' selected, GitLab will build the latest commit on each push and the build
status will be displayed in the merge request.
Enter the Jenkins URL and Project name. The project name should be URL-friendly
where spaces are replaced with underscores. To be safe, copy the project name
from the URL bar of your browser while viewing the Jenkins project.
Optionally, enter a username and password if your Jenkins server requires
authentication.
![GitLab service settings](jenkins_gitlab_service_settings.png)
# Jenkins CI (Deprecated) Service
This service is deprecated and may be removed in a future version of GitLab.
Please see documentation for the new Jenkins CI service above.
Integration includes:
* Trigger Jenkins build after push to repo
* Show build status on Merge Request page
Requirements:
* Jenkins GitLab Hook plugin
* [Jenkins GitLab Hook plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Hook+Plugin)
* git clone access for Jenkins from GitLab repo (via ssh key)
## Jenkins
1. Install GitLab Hook plugin
1. Install [GitLab Hook plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+Hook+Plugin)
2. Setup jenkins project
![screen](jenkins_project.png)
......@@ -23,12 +105,12 @@ Requirements:
## GitLab
### Read access to repository
Jenkins need read access to GitLab repository. We already specified private key to use in Jenkins. Now we need to add public key to GitLab project
![screen](jenkins_gitlab_deploy.png)
### Read access to repository
Jenkins needs read access to the GitLab repository. We already specified a
private key to use in Jenkins, now we need to add a public one to the GitLab
project. For that case we will need a Deploy key. Read the documentation on
[how to setup a Deploy key](../../ssh/README.md#deploy-keys).
### Jenkins service
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
# push_events :boolean default(TRUE)
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
#
require 'spec_helper'
describe JenkinsDeprecatedService do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'commits methods' do
def status_body_for_icon(state)
<<eos
<h1 class="build-caption page-headline"><img style="width: 48px; height: 48px; " alt="Success" class="icon-#{state} icon-xlg" src="/static/855d7c3c/images/48x48/#{state}" tooltip="Success" title="Success">
Build #188
(Oct 15, 2014 9:45:21 PM)
</h1>
eos
end
describe :commit_status do
before do
@service = JenkinsDeprecatedService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '0',
pass_unstable: '0',
token: 'verySecret'
)
end
statuses = { 'blue.png' => 'success', 'yellow.png' => 'failed', 'red.png' => 'failed', 'aborted.png' => 'failed', 'blue-anime.gif' => 'running', 'grey.png' => 'pending' }
statuses.each do |icon, state|
it "should have a status of #{state} when the icon #{icon} exists." do
stub_request(:get, "http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c").to_return(status: 200, body: status_body_for_icon(icon), headers: {})
expect(@service.commit_status("2ab7834c", 'master')).to eq(state)
end
end
end
describe 'commit status with passing unstable' do
before do
@service = JenkinsDeprecatedService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '0',
pass_unstable: '1',
token: 'verySecret'
)
end
it "should have a status of success when the icon yellow exists." do
stub_request(:get, "http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c").to_return(status: 200, body: status_body_for_icon('yellow.png'), headers: {})
expect(@service.commit_status("2ab7834c", 'master')).to eq('success')
end
end
describe 'multiproject enabled' do
let!(:project) { create(:project) }
before do
@service = JenkinsDeprecatedService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '1',
token: 'verySecret',
project: project
)
end
describe :build_page do
it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://jenkins.gitlab.org/job/#{project.name}_master/scm/bySHA1/2ab7834c") }
end
describe :build_page_with_branch do
it { expect(@service.build_page("2ab7834c", 'test_branch')).to eq("http://jenkins.gitlab.org/job/#{project.name}_test_branch/scm/bySHA1/2ab7834c") }
end
end
describe 'multiproject disabled' do
before do
@service = JenkinsDeprecatedService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '0',
token: 'verySecret'
)
end
describe :build_page do
it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c") }
end
describe :build_page_with_branch do
it { expect(@service.build_page("2ab7834c", 'test_branch')).to eq("http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c") }
end
end
end
end
......@@ -24,96 +24,118 @@ describe JenkinsService do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
let(:project) { create(:project) }
describe 'commits methods' do
def status_body_for_icon(state)
<<eos
<h1 class="build-caption page-headline"><img style="width: 48px; height: 48px; " alt="Success" class="icon-#{state} icon-xlg" src="/static/855d7c3c/images/48x48/#{state}" tooltip="Success" title="Success">
Build #188
(Oct 15, 2014 9:45:21 PM)
</h1>
eos
describe 'username validation' do
before do
@jenkins_service = JenkinsService.create(
active: active,
project: project,
properties: {
jenkins_url: 'http://jenkins.example.com/',
password: 'password',
username: nil,
}
)
end
subject { @jenkins_service }
describe :commit_status do
before do
@service = JenkinsService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '0',
pass_unstable: '0',
token: 'verySecret'
)
end
context 'when the service is active' do
let(:active) { true }
it { is_expected.to validate_presence_of :username }
end
statuses = { 'blue.png' => 'success', 'yellow.png' => 'failed', 'red.png' => 'failed', 'aborted.png' => 'failed', 'blue-anime.gif' => 'running', 'grey.png' => 'pending' }
statuses.each do |icon, state|
it "should have a status of #{state} when the icon #{icon} exists." do
stub_request(:get, "http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c").to_return(status: 200, body: status_body_for_icon(icon), headers: {})
expect(@service.commit_status("2ab7834c", 'master')).to eq(state)
end
end
context 'when the service is inactive' do
let(:active) { false }
it { is_expected.not_to validate_presence_of :username }
end
end
describe '#hook_url' do
before do
@jenkins_service = JenkinsService.create(
project: project,
properties: {
jenkins_url: jenkins_url,
project_name: 'my_project'
}
)
end
subject { @jenkins_service.hook_url }
context 'when the jenkins_url has no relative path' do
let(:jenkins_url) { 'http://jenkins.example.com/' }
it { is_expected.to eq('http://jenkins.example.com/project/my_project') }
end
context 'when the jenkins_url has relative path' do
let(:jenkins_url) { 'http://organization.example.com/jenkins' }
it { is_expected.to eq('http://organization.example.com/jenkins/project/my_project') }
end
end
describe 'Stored password invalidation' do
let(:project) { create(:project) }
describe 'commit status with passing unstable' do
context 'when a password was previously set' do
before do
@service = JenkinsService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '0',
pass_unstable: '1',
token: 'verySecret'
@jenkins_service = JenkinsService.create(
project: project,
properties: {
jenkins_url: 'http://jenkins.example.com/',
username: 'jenkins',
password: 'password'
}
)
end
it "should have a status of success when the icon yellow exists." do
stub_request(:get, "http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c").to_return(status: 200, body: status_body_for_icon('yellow.png'), headers: {})
expect(@service.commit_status("2ab7834c", 'master')).to eq('success')
it 'reset password if url changed' do
@jenkins_service.jenkins_url = 'http://jenkins-edited.example.com/'
@jenkins_service.save
expect(@jenkins_service.password).to be_nil
end
end
describe 'multiproject enabled' do
let!(:project) { create(:project) }
before do
@service = JenkinsService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '1',
token: 'verySecret',
project: project
)
it 'does not reset password if username changed' do
@jenkins_service.username = 'some_name'
@jenkins_service.save
expect(@jenkins_service.password).to eq('password')
end
describe :build_page do
it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://jenkins.gitlab.org/job/#{project.name}_master/scm/bySHA1/2ab7834c") }
it 'does not reset password if new url is set together with password, even if it\'s the same password' do
@jenkins_service.jenkins_url = 'http://jenkins_edited.example.com/'
@jenkins_service.password = 'password'
@jenkins_service.save
expect(@jenkins_service.password).to eq('password')
expect(@jenkins_service.jenkins_url).to eq('http://jenkins_edited.example.com/')
end
describe :build_page_with_branch do
it { expect(@service.build_page("2ab7834c", 'test_branch')).to eq("http://jenkins.gitlab.org/job/#{project.name}_test_branch/scm/bySHA1/2ab7834c") }
it 'should reset password if url changed, even if setter called multiple times' do
@jenkins_service.jenkins_url = 'http://jenkins1.example.com/'
@jenkins_service.jenkins_url = 'http://jenkins1.example.com/'
@jenkins_service.save
expect(@jenkins_service.password).to be_nil
end
end
describe 'multiproject disabled' do
context 'when no password was previously set' do
before do
@service = JenkinsService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://jenkins.gitlab.org/job/2',
multiproject_enabled: '0',
token: 'verySecret'
@jenkins_service = JenkinsService.create(
project: create(:project),
properties: {
jenkins_url: 'http://jenkins.example.com/',
username: 'jenkins'
}
)
end
describe :build_page do
it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c") }
it 'saves password if new url is set together with password' do
@jenkins_service.jenkins_url = 'http://jenkins_edited.example.com/'
@jenkins_service.password = 'password'
@jenkins_service.save
expect(@jenkins_service.password).to eq('password')
expect(@jenkins_service.jenkins_url).to eq('http://jenkins_edited.example.com/')
end
describe :build_page_with_branch do
it { expect(@service.build_page("2ab7834c", 'test_branch')).to eq("http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c") }
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment