Skip to content
Snippets Groups Projects
Commit 3f0679ab authored by Robert Speicher's avatar Robert Speicher
Browse files

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
parents ab3ecb34 e905cbda
No related branches found
No related tags found
No related merge requests found
Showing
with 304 additions and 46 deletions
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
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
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
Loading
Loading
@@ -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
Loading
Loading
@@ -27,6 +29,7 @@ class Build < ActiveRecord::Base
belongs_to :commit
belongs_to :project
belongs_to :runner
belongs_to :trigger_request
 
serialize :options
 
Loading
Loading
@@ -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
Loading
Loading
@@ -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)
Loading
Loading
@@ -165,7 +169,7 @@ class Build < ActiveRecord::Base
end
 
def variables
yaml_variables + project_variables
yaml_variables + project_variables + trigger_variables
end
 
def duration
Loading
Loading
@@ -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
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
# == 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
# == 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
Loading
Loading
@@ -40,6 +40,7 @@ class CreateCommitService
commit = project.commits.create(data)
end
 
commit.update_committed!
commit.create_builds unless commit.builds.any?
 
commit
Loading
Loading
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
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
%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"
%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')}
Loading
Loading
@@ -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
Loading
Loading
class AddCommittedAtToCommits < ActiveRecord::Migration
def up
add_column :commits, :committed_at, :timestamp
add_index :commits, [:project_id, :committed_at]
end
end
class UpdateCommittedAtWithCreatedAt < ActiveRecord::Migration
def up
execute('UPDATE commits SET committed_at=created_at WHERE committed_at IS NULL')
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment