diff options
author | Robert Speicher <robert@gitlab.com> | 2015-08-21 17:09:10 +0000 |
---|---|---|
committer | Robert Speicher <robert@gitlab.com> | 2015-08-21 17:09:10 +0000 |
commit | 3f0679ab5ae7a4bd825a937d9601767bb6d681d2 (patch) | |
tree | e5b420e50b31f104c993be7f78168658123e548c | |
parent | ab3ecb348e65a9579a8f5ffbdd70fd55c59b9f17 (diff) | |
parent | e905cbda6e1577d4a9c11da899d11aeb2e637035 (diff) | |
download | gitlab-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
41 files changed, 814 insertions, 113 deletions
@@ -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 |