summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Speicher <robert@gitlab.com>2015-08-21 17:09:10 +0000
committerRobert Speicher <robert@gitlab.com>2015-08-21 17:09:10 +0000
commit3f0679ab5ae7a4bd825a937d9601767bb6d681d2 (patch)
treee5b420e50b31f104c993be7f78168658123e548c
parentab3ecb348e65a9579a8f5ffbdd70fd55c59b9f17 (diff)
parente905cbda6e1577d4a9c11da899d11aeb2e637035 (diff)
downloadgitlab-ci-3f0679ab5ae7a4bd825a937d9601767bb6d681d2.tar.gz
Merge branch 'build-triggers' into 'master'
Implement build trigger API This commit implements Build Triggers. There are changes to API request: - Due to security advised method to pass trigger token is to use form data - Advised method to pass variables is to use form data TODO: - [x] Implement API - [x] Implement UI - [x] Dimitriy and Valery review - [x] Write specs - [x] Write documentation - [x] Job documentation review See merge request !229
-rw-r--r--CHANGELOG2
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/controllers/triggers_controller.rb41
-rw-r--r--app/helpers/triggers_helper.rb5
-rw-r--r--app/models/build.rb52
-rw-r--r--app/models/commit.rb48
-rw-r--r--app/models/project.rb7
-rw-r--r--app/models/project_status.rb4
-rw-r--r--app/models/trigger.rb35
-rw-r--r--app/models/trigger_request.rb19
-rw-r--r--app/services/create_commit_service.rb1
-rw-r--r--app/services/create_trigger_request_service.rb15
-rw-r--r--app/views/builds/_build.html.haml2
-rw-r--r--app/views/builds/show.html.haml17
-rw-r--r--app/views/layouts/_nav_project.html.haml4
-rw-r--r--app/views/triggers/_trigger.html.haml14
-rw-r--r--app/views/triggers/index.html.haml67
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20150806091503_add_committed_at_to_commits.rb6
-rw-r--r--db/migrate/20150806091655_update_committed_at_with_created_at.rb5
-rw-r--r--db/migrate/20150806102222_create_trigger.rb12
-rw-r--r--db/migrate/20150806102457_add_trigger_to_builds.rb5
-rw-r--r--db/migrate/20150806105404_create_trigger_request.rb9
-rw-r--r--db/migrate/20150819162227_add_commit_id_to_trigger_requests.rb8
-rw-r--r--db/schema.rb29
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/commits.rb4
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/api/triggers.rb47
-rw-r--r--spec/factories/builds.rb36
-rw-r--r--spec/factories/commits.rb30
-rw-r--r--spec/factories/trigger_requests.rb13
-rw-r--r--spec/factories/triggers.rb9
-rw-r--r--spec/features/triggers_spec.rb26
-rw-r--r--spec/models/build_spec.rb80
-rw-r--r--spec/models/commit_spec.rb91
-rw-r--r--spec/models/project_spec.rb4
-rw-r--r--spec/models/trigger_spec.rb17
-rw-r--r--spec/requests/api/builds_spec.rb18
-rw-r--r--spec/requests/api/triggers_spec.rb78
-rw-r--r--spec/services/create_trigger_request_service_spec.rb52
41 files changed, 814 insertions, 113 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 760c244..d1ad661 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -11,12 +11,14 @@ v7.14.0 (unreleased)
- 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
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8ba0f92..32e5a98 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -40,9 +40,9 @@ class ProjectsController < ApplicationController
def show
@ref = params[:ref]
- @commits = @project.commits
+ @commits = @project.commits.reverse_order
@commits = @commits.where(ref: @ref) if @ref
- @commits = @commits.order('id DESC').page(params[:page]).per(20)
+ @commits = @commits.page(params[:page]).per(20)
end
def integration
diff --git a/app/controllers/triggers_controller.rb b/app/controllers/triggers_controller.rb
new file mode 100644
index 0000000..b942051
--- /dev/null
+++ b/app/controllers/triggers_controller.rb
@@ -0,0 +1,41 @@
+class TriggersController < ApplicationController
+ before_filter :authenticate_user!
+ before_filter :project
+ before_filter :authorize_access_project!
+ before_filter :authorize_manage_project!
+
+ layout 'project'
+
+ def index
+ @triggers = @project.triggers
+ @trigger = Trigger.new
+ end
+
+ def create
+ @trigger = @project.triggers.new
+ @trigger.save
+
+ if @trigger.valid?
+ redirect_to project_triggers_path(@project)
+ else
+ @triggers = @project.triggers.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def destroy
+ trigger.destroy
+
+ redirect_to project_triggers_path(@project)
+ end
+
+ private
+
+ def trigger
+ @trigger ||= @project.triggers.find(params[:id])
+ end
+
+ def project
+ @project = Project.find(params[:project_id])
+ end
+end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
new file mode 100644
index 0000000..ac93bf6
--- /dev/null
+++ b/app/helpers/triggers_helper.rb
@@ -0,0 +1,5 @@
+module TriggersHelper
+ def build_trigger_url(project_id, ref_name)
+ "#{Settings.gitlab_ci.url}/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
+ end
+end
diff --git a/app/models/build.rb b/app/models/build.rb
index da52f0a..913a47c 100644
--- a/app/models/build.rb
+++ b/app/models/build.rb
@@ -2,23 +2,25 @@
#
# 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
+# 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
#
class Build < ActiveRecord::Base
@@ -27,6 +29,7 @@ class Build < ActiveRecord::Base
belongs_to :commit
belongs_to :project
belongs_to :runner
+ belongs_to :trigger_request
serialize :options
@@ -78,6 +81,7 @@ class Build < ActiveRecord::Base
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
@@ -113,7 +117,7 @@ class Build < ActiveRecord::Base
end
if build.commit.success?
- build.commit.create_next_builds
+ build.commit.create_next_builds(build.trigger_request)
end
project.execute_services(build)
@@ -165,7 +169,7 @@ class Build < ActiveRecord::Base
end
def variables
- yaml_variables + project_variables
+ yaml_variables + project_variables + trigger_variables
end
def duration
@@ -264,4 +268,14 @@ class Build < ActiveRecord::Base
{ 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
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 68057b7..98bd35c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -2,21 +2,23 @@
#
# 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
+# 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
#
class Commit < ActiveRecord::Base
belongs_to :project
has_many :builds, dependent: :destroy
+ has_many :trigger_requests, dependent: :destroy
serialize :push_data
@@ -99,8 +101,8 @@ class Commit < ActiveRecord::Base
config_processor.stages.find { |stage| stages.include? stage }
end
- def create_builds_for_stage(stage)
- return if skip_ci?
+ 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)
@@ -112,28 +114,29 @@ class Commit < ActiveRecord::Base
tag_list: build_attrs[:tags],
options: build_attrs[:options],
allow_failure: build_attrs[:allow_failure],
- stage: build_attrs[:stage]
+ stage: build_attrs[:stage],
+ trigger_request: trigger_request,
})
end
end
- def create_next_builds
- return if skip_ci?
+ def create_next_builds(trigger_request)
+ return if skip_ci? && trigger_request.blank?
return unless config_processor
- stages = builds.group_by(&:stage)
+ stages = builds.where(trigger_request: trigger_request).group_by(&:stage)
config_processor.stages.any? do |stage|
- !stages.include?(stage) && create_builds_for_stage(stage).present?
+ !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present?
end
end
- def create_builds
- return if skip_ci?
+ 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).present?
+ create_builds_for_stage(stage, trigger_request).present?
end
end
@@ -241,10 +244,15 @@ class Commit < ActiveRecord::Base
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)
diff --git a/app/models/project.rb b/app/models/project.rb
index 55d8445..4879f24 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -28,13 +28,14 @@
class Project < ActiveRecord::Base
include ProjectStatus
- has_many :commits, dependent: :destroy
+ has_many :commits, ->() { order(:committed_at) }, dependent: :destroy
has_many :builds, through: :commits, dependent: :destroy
has_many :runner_projects, dependent: :destroy
has_many :runners, through: :runner_projects
has_many :web_hooks, dependent: :destroy
has_many :events, dependent: :destroy
has_many :variables, dependent: :destroy
+ has_many :triggers, dependent: :destroy
# Project services
has_many :services, dependent: :destroy
@@ -110,9 +111,9 @@ ls -la
end
def ordered_by_last_commit_date
- last_commit_subquery = "(SELECT project_id, MAX(created_at) created_at FROM commits GROUP BY project_id)"
+ last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM commits GROUP BY project_id)"
joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON projects.id = last_commit.project_id").
- order("CASE WHEN last_commit.created_at IS NULL THEN 1 ELSE 0 END, last_commit.created_at DESC")
+ order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
end
def search(query)
diff --git a/app/models/project_status.rb b/app/models/project_status.rb
index b14cb4f..7c22785 100644
--- a/app/models/project_status.rb
+++ b/app/models/project_status.rb
@@ -30,7 +30,7 @@ module ProjectStatus
# only check for toggling build status within same ref.
def last_commit_changed_status?
ref = last_commit.ref
- last_commits = commits.where(ref: ref).order('id DESC').limit(2)
+ last_commits = commits.where(ref: ref).last(2)
if last_commits.size < 2
false
@@ -40,6 +40,6 @@ module ProjectStatus
end
def last_commit_for_ref(ref)
- commits.where(ref: ref).order('id DESC').first
+ commits.where(ref: ref).last
end
end
diff --git a/app/models/trigger.rb b/app/models/trigger.rb
new file mode 100644
index 0000000..26d9893
--- /dev/null
+++ b/app/models/trigger.rb
@@ -0,0 +1,35 @@
+# == 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
+#
+
+class Trigger < ActiveRecord::Base
+ acts_as_paranoid
+
+ belongs_to :project
+ has_many :trigger_requests, dependent: :destroy
+
+ 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
diff --git a/app/models/trigger_request.rb b/app/models/trigger_request.rb
new file mode 100644
index 0000000..180c23f
--- /dev/null
+++ b/app/models/trigger_request.rb
@@ -0,0 +1,19 @@
+# == 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
+#
+
+class TriggerRequest < ActiveRecord::Base
+ belongs_to :trigger
+ belongs_to :commit
+ has_many :builds
+
+ serialize :variables
+end
diff --git a/app/services/create_commit_service.rb b/app/services/create_commit_service.rb
index 4ae1852..912117a 100644
--- a/app/services/create_commit_service.rb
+++ b/app/services/create_commit_service.rb
@@ -40,6 +40,7 @@ class CreateCommitService
commit = project.commits.create(data)
end
+ commit.update_committed!
commit.create_builds unless commit.builds.any?
commit
diff --git a/app/services/create_trigger_request_service.rb b/app/services/create_trigger_request_service.rb
new file mode 100644
index 0000000..a60f6f6
--- /dev/null
+++ b/app/services/create_trigger_request_service.rb
@@ -0,0 +1,15 @@
+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
diff --git a/app/views/builds/_build.html.haml b/app/views/builds/_build.html.haml
index 4fa90c1..58a8727 100644
--- a/app/views/builds/_build.html.haml
+++ b/app/views/builds/_build.html.haml
@@ -16,6 +16,8 @@
- 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
diff --git a/app/views/builds/show.html.haml b/app/views/builds/show.html.haml
index 55ac8af..95f5992 100644
--- a/app/views/builds/show.html.haml
+++ b/app/views/builds/show.html.haml
@@ -109,6 +109,23 @@
- 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
diff --git a/app/views/layouts/_nav_project.html.haml b/app/views/layouts/_nav_project.html.haml
index 3a3ac08..dbcdc40 100644
--- a/app/views/layouts/_nav_project.html.haml
+++ b/app/views/layouts/_nav_project.html.haml
@@ -20,6 +20,10 @@
= link_to project_web_hooks_path(@project) do
%i.icon-link
Web Hooks
+ = nav_link path: 'triggers#index' do
+ = link_to project_triggers_path(@project) do
+ %i.icon-retweet
+ Triggers
= nav_link path: 'services#index' do
= link_to project_services_path(@project) do
%i.icon-share
diff --git a/app/views/triggers/_trigger.html.haml b/app/views/triggers/_trigger.html.haml
new file mode 100644
index 0000000..72f7a17
--- /dev/null
+++ b/app/views/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', 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/triggers/index.html.haml b/app/views/triggers/index.html.haml
new file mode 100644
index 0000000..3401ef3
--- /dev/null
+++ b/app/views/triggers/index.html.haml
@@ -0,0 +1,67 @@
+%h3
+ 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 [@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 \
+ #{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 #{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" \
+ #{build_trigger_url(@project.id, 'REF_NAME')}
diff --git a/config/routes.rb b/config/routes.rb
index cd0054f..cbb5231 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -56,6 +56,8 @@ Rails.application.routes.draw do
end
end
+ resources :triggers, only: [:index, :create, :destroy]
+
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
member do
get :resume
diff --git a/db/migrate/20150806091503_add_committed_at_to_commits.rb b/db/migrate/20150806091503_add_committed_at_to_commits.rb
new file mode 100644
index 0000000..2825b99
--- /dev/null
+++ b/db/migrate/20150806091503_add_committed_at_to_commits.rb
@@ -0,0 +1,6 @@
+class AddCommittedAtToCommits < ActiveRecord::Migration
+ def up
+ add_column :commits, :committed_at, :timestamp
+ add_index :commits, [:project_id, :committed_at]
+ end
+end
diff --git a/db/migrate/20150806091655_update_committed_at_with_created_at.rb b/db/migrate/20150806091655_update_committed_at_with_created_at.rb
new file mode 100644
index 0000000..a2646c3
--- /dev/null
+++ b/db/migrate/20150806091655_update_committed_at_with_created_at.rb
@@ -0,0 +1,5 @@
+class UpdateCommittedAtWithCreatedAt < ActiveRecord::Migration
+ def up
+ execute('UPDATE commits SET committed_at=created_at WHERE committed_at IS NULL')
+ end
+end
diff --git a/db/migrate/20150806102222_create_trigger.rb b/db/migrate/20150806102222_create_trigger.rb
new file mode 100644
index 0000000..0f141b0
--- /dev/null
+++ b/db/migrate/20150806102222_create_trigger.rb
@@ -0,0 +1,12 @@
+class CreateTrigger < ActiveRecord::Migration
+ def up
+ create_table :triggers do |t|
+ t.string :token, null: true
+ t.integer :project_id, null: false
+ t.datetime :deleted_at
+ t.timestamps
+ end
+
+ add_index :triggers, :deleted_at
+ end
+end
diff --git a/db/migrate/20150806102457_add_trigger_to_builds.rb b/db/migrate/20150806102457_add_trigger_to_builds.rb
new file mode 100644
index 0000000..ad2fd78
--- /dev/null
+++ b/db/migrate/20150806102457_add_trigger_to_builds.rb
@@ -0,0 +1,5 @@
+class AddTriggerToBuilds < ActiveRecord::Migration
+ def up
+ add_column :builds, :trigger_request_id, :integer
+ end
+end
diff --git a/db/migrate/20150806105404_create_trigger_request.rb b/db/migrate/20150806105404_create_trigger_request.rb
new file mode 100644
index 0000000..b58ff31
--- /dev/null
+++ b/db/migrate/20150806105404_create_trigger_request.rb
@@ -0,0 +1,9 @@
+class CreateTriggerRequest < ActiveRecord::Migration
+ def change
+ create_table :trigger_requests do |t|
+ t.integer :trigger_id, null: false
+ t.text :variables
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20150819162227_add_commit_id_to_trigger_requests.rb b/db/migrate/20150819162227_add_commit_id_to_trigger_requests.rb
new file mode 100644
index 0000000..0e55537
--- /dev/null
+++ b/db/migrate/20150819162227_add_commit_id_to_trigger_requests.rb
@@ -0,0 +1,8 @@
+class AddCommitIdToTriggerRequests < ActiveRecord::Migration
+ def up
+ add_column :trigger_requests, :commit_id, :integer
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0e99323..6f630ab 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: 20150803142346) do
+ActiveRecord::Schema.define(version: 20150819162227) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -37,10 +37,11 @@ ActiveRecord::Schema.define(version: 20150803142346) do
t.text "commands"
t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
+ t.boolean "allow_failure", default: false, null: false
t.string "stage"
+ t.integer "trigger_request_id"
end
add_index "builds", ["commit_id"], name: "index_builds_on_commit_id", using: :btree
@@ -56,10 +57,12 @@ ActiveRecord::Schema.define(version: 20150803142346) do
t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "tag", default: false
+ t.boolean "tag", default: false
t.text "yaml_errors"
+ t.datetime "committed_at"
end
+ add_index "commits", ["project_id", "committed_at"], name: "index_commits_on_project_id_and_committed_at", using: :btree
add_index "commits", ["project_id", "sha"], name: "index_commits_on_project_id_and_sha", using: :btree
add_index "commits", ["project_id"], name: "index_commits_on_project_id", using: :btree
add_index "commits", ["sha"], name: "index_commits_on_sha", using: :btree
@@ -184,6 +187,24 @@ ActiveRecord::Schema.define(version: 20150803142346) do
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
+ create_table "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 "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 "triggers", ["deleted_at"], name: "index_triggers_on_deleted_at", using: :btree
+
create_table "variables", force: true do |t|
t.integer "project_id", null: false
t.string "key"
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d1127ed..6645ff1 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -30,5 +30,6 @@ module API
mount Runners
mount Projects
mount Forks
+ mount Triggers
end
end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 6aa060c..363e5d7 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -15,7 +15,7 @@ module API
authenticate_project_token!(project)
commits = project.commits.page(params[:page]).per(params[:per_page] || 20)
- present commits, with: Entities::Commit
+ present commits, with: Entities::CommitWithBuilds
end
# Create a commit
@@ -53,7 +53,7 @@ module API
commit = CreateCommitService.new.execute(project, params[:data])
if commit.persisted?
- present commit, with: Entities::Commit
+ present commit, with: Entities::CommitWithBuilds
else
errors = commit.errors.full_messages.join(", ")
render_api_error!(errors, 400)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 464667f..3aaef0a 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -4,6 +4,9 @@ module API
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
@@ -30,5 +33,10 @@ module API
class WebHook < Grape::Entity
expose :id, :project_id, :url
end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ expose :commit, using: Commit
+ end
end
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
new file mode 100644
index 0000000..1e4935a
--- /dev/null
+++ b/lib/api/triggers.rb
@@ -0,0 +1,47 @@
+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 = Project.find(params[:id])
+ trigger = 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 = 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
diff --git a/spec/factories/builds.rb b/spec/factories/builds.rb
index af63bbd..346e000 100644
--- a/spec/factories/builds.rb
+++ b/spec/factories/builds.rb
@@ -2,23 +2,25 @@
#
# 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
+# 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
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index 4a411ee..6fdd46f 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -2,16 +2,17 @@
#
# 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
+# 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
@@ -50,15 +51,24 @@ FactoryGirl.define do
}
end
+ factory :commit_without_jobs do
+ after(:create) do |commit, evaluator|
+ commit.push_data[:ci_yaml_file] = YAML.dump({})
+ commit.save
+ end
+ end
+
factory :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 :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
diff --git a/spec/factories/trigger_requests.rb b/spec/factories/trigger_requests.rb
new file mode 100644
index 0000000..c85d102
--- /dev/null
+++ b/spec/factories/trigger_requests.rb
@@ -0,0 +1,13 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :trigger_request do
+ factory :trigger_request_with_variables do
+ variables do
+ {
+ TRIGGER_KEY: 'TRIGGER_VALUE'
+ }
+ end
+ end
+ end
+end
diff --git a/spec/factories/triggers.rb b/spec/factories/triggers.rb
new file mode 100644
index 0000000..a5af47b
--- /dev/null
+++ b/spec/factories/triggers.rb
@@ -0,0 +1,9 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :trigger_without_token, class: Trigger do
+ factory :trigger do
+ token 'token'
+ end
+ end
+end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
new file mode 100644
index 0000000..2076429
--- /dev/null
+++ b/spec/features/triggers_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'Variables' do
+ before do
+ login_as :user
+ @project = FactoryGirl.create :project
+ stub_js_gitlab_calls
+ visit project_triggers_path(@project)
+ end
+
+ context 'create a trigger' do
+ before do
+ click_on 'Add Trigger'
+ @project.triggers.count.should == 1
+ end
+
+ it 'contains trigger token' do
+ page.should have_content(@project.triggers.first.token)
+ end
+
+ it 'revokes the trigger' do
+ click_on 'Revoke'
+ @project.triggers.count.should == 0
+ end
+ end
+end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 7e1c7e9..7333981 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -2,23 +2,25 @@
#
# 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
+# 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'
@@ -301,4 +303,48 @@ describe Build do
it { should eq(98.29) }
end
end
+
+ describe :variables do
+ context 'returns variables' do
+ subject { build.variables }
+
+ let(:variables) {
+ [
+ {key: :DB_NAME, value: 'postgres', public: true}
+ ]
+ }
+
+ it { should eq(variables) }
+
+ context 'and secure variables' do
+ let(:secure_variables) {
+ [
+ {key: 'SECRET_KEY', value: 'secret_value', public: false}
+ ]
+ }
+
+ before do
+ build.project.variables << Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ end
+
+ it { should eq(variables + secure_variables) }
+
+ context 'and trigger variables' do
+ let(:trigger) { FactoryGirl.create :trigger, project: project }
+ let(:trigger_request) { FactoryGirl.create :trigger_request_with_variables, commit: commit, trigger: trigger }
+ let(:trigger_variables) {
+ [
+ {key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false}
+ ]
+ }
+
+ before do
+ build.trigger_request = trigger_request
+ end
+
+ it { should eq(variables + secure_variables + trigger_variables) }
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 2074781..6f644d2 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -2,16 +2,17 @@
#
# 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
+# 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'
@@ -20,6 +21,7 @@ describe Commit do
let(:project) { FactoryGirl.create :project }
let(:commit) { FactoryGirl.create :commit, project: project }
let(:commit_with_project) { FactoryGirl.create :commit, project: project }
+ let(:config_processor) { GitlabCiYamlProcessor.new(gitlab_ci_yaml) }
it { should belong_to(:project) }
it { should have_many(:builds) }
@@ -133,23 +135,82 @@ describe Commit do
end
describe :create_next_builds do
- it "creates builds for next type" do
- config_processor = GitlabCiYamlProcessor.new(gitlab_ci_yaml)
+ before do
commit.stub(:config_processor).and_return(config_processor)
+ end
+ it "creates builds for next type" do
commit.create_builds.should be_true
commit.builds.reload
commit.builds.size.should == 2
- commit.create_next_builds.should be_true
+ commit.create_next_builds(nil).should be_true
commit.builds.reload
commit.builds.size.should == 4
- commit.create_next_builds.should be_true
+ commit.create_next_builds(nil).should be_true
commit.builds.reload
commit.builds.size.should == 5
- commit.create_next_builds.should be_false
+ commit.create_next_builds(nil).should be_false
+ end
+ end
+
+ describe :create_builds do
+ before do
+ commit.stub(:config_processor).and_return(config_processor)
+ end
+
+ it 'creates builds' do
+ commit.create_builds.should be_true
+ commit.builds.reload
+ commit.builds.size.should == 2
+ end
+
+ context 'for build triggers' do
+ let(:trigger) { FactoryGirl.create :trigger, project: project }
+ let(:trigger_request) { FactoryGirl.create :trigger_request, commit: commit, trigger: trigger }
+
+ it 'creates builds' do
+ commit.create_builds(trigger_request).should be_true
+ commit.builds.reload
+ commit.builds.size.should == 2
+ end
+
+ it 'rebuilds commit' do
+ commit.create_builds.should be_true
+ commit.builds.reload
+ commit.builds.size.should == 2
+
+ commit.create_builds(trigger_request).should be_true
+ commit.builds.reload
+ commit.builds.size.should == 4
+ end
+
+ it 'creates next builds' do
+ commit.create_builds(trigger_request).should be_true
+ commit.builds.reload
+ commit.builds.size.should == 2
+
+ commit.create_next_builds(trigger_request).should be_true
+ commit.builds.reload
+ commit.builds.size.should == 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
+ commit.status.should == 'skipped'
+ commit.create_builds(trigger_request).should be_true
+ commit.builds.reload
+ commit.builds.size.should == 2
+ commit.status.should == 'pending'
+ end
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c15fa2b..aa76b99 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -54,8 +54,8 @@ describe Project do
oldest_project = FactoryGirl.create :project
project_without_commits = FactoryGirl.create :project
- FactoryGirl.create :commit, created_at: 1.hour.ago, project: newest_project
- FactoryGirl.create :commit, created_at: 2.hour.ago, project: oldest_project
+ FactoryGirl.create :commit, committed_at: 1.hour.ago, project: newest_project
+ FactoryGirl.create :commit, committed_at: 2.hour.ago, project: oldest_project
Project.ordered_by_last_commit_date.should == [newest_project, oldest_project, project_without_commits]
end
diff --git a/spec/models/trigger_spec.rb b/spec/models/trigger_spec.rb
new file mode 100644
index 0000000..bba638e
--- /dev/null
+++ b/spec/models/trigger_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Trigger do
+ let(:project) { FactoryGirl.create :project }
+
+ describe 'before_validation' do
+ it 'should set an random token if none provided' do
+ trigger = FactoryGirl.create :trigger_without_token, project: project
+ trigger.token.should_not be_nil
+ end
+
+ it 'should not set an random token if one provided' do
+ trigger = FactoryGirl.create :trigger, project: project
+ trigger.token.should == 'token'
+ end
+ end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index a169f8e..be55e9f 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -74,6 +74,24 @@ describe API::API do
{"key" => "SECRET_KEY", "value" => "secret_value", "public" => false},
]
end
+
+ it "returns variables for triggers" do
+ trigger = FactoryGirl.create(:trigger, project: project)
+ commit = FactoryGirl.create(:commit, project: project)
+
+ trigger_request = FactoryGirl.create(:trigger_request_with_variables, commit: commit, trigger: trigger)
+ commit.create_builds(trigger_request)
+ project.variables << Variable.new(key: "SECRET_KEY", value: "secret_value")
+
+ post api("/builds/register"), token: runner.token, info: {platform: :darwin}
+
+ response.status.should == 201
+ json_response["variables"].should == [
+ {"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
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
new file mode 100644
index 0000000..6e56c4b
--- /dev/null
+++ b/spec/requests/api/triggers_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe API::API do
+ include ApiHelpers
+
+ describe 'POST /projects/:project_id/refs/:ref/trigger' do
+ let!(:trigger_token) { 'secure token' }
+ let!(:project) { FactoryGirl.create(:project) }
+ let!(:project2) { FactoryGirl.create(:project) }
+ let!(:trigger) { FactoryGirl.create(:trigger, project: project, token: trigger_token) }
+ let(:options) {
+ {
+ token: trigger_token
+ }
+ }
+
+ context 'Handles errors' do
+ it 'should return bad request if token is missing' do
+ post api("/projects/#{project.id}/refs/master/trigger")
+ response.status.should == 400
+ end
+
+ it 'should return not found if project is not found' do
+ post api('/projects/0/refs/master/trigger'), options
+ response.status.should == 404
+ end
+
+ it 'should return unauthorized if token is for different project' do
+ post api("/projects/#{project2.id}/refs/master/trigger"), options
+ response.status.should == 401
+ end
+ end
+
+ context 'Have a commit' do
+ before do
+ @commit = FactoryGirl.create(:commit, project: project)
+ end
+
+ it 'should create builds' do
+ post api("/projects/#{project.id}/refs/master/trigger"), options
+ response.status.should == 201
+ @commit.builds.reload
+ @commit.builds.size.should == 2
+ end
+
+ it 'should return bad request with no builds created if there\'s no commit for that ref' do
+ post api("/projects/#{project.id}/refs/other-branch/trigger"), options
+ response.status.should == 400
+ json_response['message'].should == 'No builds created'
+ end
+
+ context 'Validates variables' do
+ let(:variables) {
+ {'TRIGGER_KEY' => 'TRIGGER_VALUE'}
+ }
+
+ it 'should validate variables to be a hash' do
+ post api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: 'value')
+ response.status.should == 400
+ json_response['message'].should == 'variables needs to be a hash'
+ end
+
+ it 'should validate variables needs to be a map of key-valued strings' do
+ post api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: {key: %w(1 2)})
+ response.status.should == 400
+ json_response['message'].should == 'variables needs to be a map of key-valued strings'
+ end
+
+ it 'create trigger request with variables' do
+ post api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: variables)
+ response.status.should == 201
+ @commit.builds.reload
+ @commit.builds.first.trigger_request.variables.should == variables
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/create_trigger_request_service_spec.rb b/spec/services/create_trigger_request_service_spec.rb
new file mode 100644
index 0000000..41db01c
--- /dev/null
+++ b/spec/services/create_trigger_request_service_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe CreateTriggerRequestService do
+ let(:service) { CreateTriggerRequestService.new }
+ let(:project) { FactoryGirl.create :project }
+ let(:trigger) { FactoryGirl.create :trigger, project: project }
+
+ describe :execute do
+ context 'valid params' do
+ subject { service.execute(project, trigger, 'master') }
+
+ before do
+ @commit = FactoryGirl.create :commit, project: project
+ end
+
+ it { subject.should be_kind_of(TriggerRequest) }
+ it { subject.commit.should == @commit }
+ end
+
+ context 'no commit for ref' do
+ subject { service.execute(project, trigger, 'other-branch') }
+
+ it { subject.should be_nil }
+ end
+
+ context 'no builds created' do
+ subject { service.execute(project, trigger, 'master') }
+
+ before do
+ FactoryGirl.create :commit_without_jobs, project: project
+ end
+
+ it { subject.should be_nil }
+ end
+
+ context 'for multiple commits' do
+ subject { service.execute(project, trigger, 'master') }
+
+ before do
+ @commit1 = FactoryGirl.create :commit, committed_at: 2.hour.ago, project: project
+ @commit2 = FactoryGirl.create :commit, committed_at: 1.hour.ago, project: project
+ @commit3 = FactoryGirl.create :commit, committed_at: 3.hour.ago, project: project
+ end
+
+ context 'retries latest one' do
+ it { subject.should be_kind_of(TriggerRequest) }
+ it { subject.should be_persisted }
+ it { subject.commit.should == @commit2 }
+ end
+ end
+ end
+end