Commit 6afe25ef authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '32815--Add-Custom-CI-Config-Path' into 'master'

Resolve "Project option to allow customizing CI/CD config path"

Closes #32815 and #33130

See merge request !12509
parents 6c15905c cca92420
...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def update_params def update_params
params.require(:project).permit( params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :auto_cancel_pending_pipelines :public_builds, :auto_cancel_pending_pipelines, :ci_config_path
) )
end end
end end
...@@ -326,10 +326,24 @@ module Ci ...@@ -326,10 +326,24 @@ module Ci
end end
end end
def ci_yaml_file_path
if project.ci_config_path.blank?
'.gitlab-ci.yml'
else
project.ci_config_path
end
end
def ci_yaml_file def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file) return @ci_yaml_file if defined?(@ci_yaml_file)
@ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil @ci_yaml_file = begin
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue Rugged::ReferenceError, GRPC::NotFound, GRPC::Internal
self.yaml_errors =
"Failed to load CI/CD config file at #{ci_yaml_file_path}"
nil
end
end end
def has_yaml_errors? def has_yaml_errors?
...@@ -377,7 +391,8 @@ module Ci ...@@ -377,7 +391,8 @@ module Ci
def predefined_variables def predefined_variables
[ [
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true } { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true }
] ]
end end
......
...@@ -186,6 +186,11 @@ class Project < ActiveRecord::Base ...@@ -186,6 +186,11 @@ class Project < ActiveRecord::Base
# Validations # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true validates :description, length: { maximum: 2000 }, allow_blank: true
validates :ci_config_path,
format: { without: /\.{2}/,
message: 'cannot include directory traversal.' },
length: { maximum: 255 },
allow_blank: true
validates :name, validates :name,
presence: true, presence: true,
length: { maximum: 255 }, length: { maximum: 255 },
...@@ -521,6 +526,11 @@ class Project < ActiveRecord::Base ...@@ -521,6 +526,11 @@ class Project < ActiveRecord::Base
import_data&.destroy import_data&.destroy
end end
def ci_config_path=(value)
# Strip all leading slashes so that //foo -> foo
super(value&.sub(%r{\A/+}, '')&.delete("\0"))
end
def import_url=(value) def import_url=(value)
return super(value) unless Gitlab::UrlSanitizer.valid?(value) return super(value) unless Gitlab::UrlSanitizer.valid?(value)
...@@ -1015,7 +1025,8 @@ class Project < ActiveRecord::Base ...@@ -1015,7 +1025,8 @@ class Project < ActiveRecord::Base
namespace: namespace.name, namespace: namespace.name,
visibility_level: visibility_level, visibility_level: visibility_level,
path_with_namespace: path_with_namespace, path_with_namespace: path_with_namespace,
default_branch: default_branch default_branch: default_branch,
ci_config_path: ci_config_path
} }
# Backward compatibility # Backward compatibility
......
...@@ -931,7 +931,7 @@ class Repository ...@@ -931,7 +931,7 @@ class Repository
def is_ancestor?(ancestor_id, descendant_id) def is_ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil? return false if ancestor_id.nil? || descendant_id.nil?
Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled if is_enabled
raw_repository.is_ancestor?(ancestor_id, descendant_id) raw_repository.is_ancestor?(ancestor_id, descendant_id)
...@@ -1078,8 +1078,8 @@ class Repository ...@@ -1078,8 +1078,8 @@ class Repository
blob_data_at(sha, '.gitlab/route-map.yml') blob_data_at(sha, '.gitlab/route-map.yml')
end end
def gitlab_ci_yml_for(sha) def gitlab_ci_yml_for(sha, path = '.gitlab-ci.yml')
blob_data_at(sha, '.gitlab-ci.yml') blob_data_at(sha, path)
end end
private private
......
...@@ -33,7 +33,7 @@ module Ci ...@@ -33,7 +33,7 @@ module Ci
unless pipeline.config_processor unless pipeline.config_processor
unless pipeline.ci_yaml_file unless pipeline.ci_yaml_file
return error('Missing .gitlab-ci.yml file') return error("Missing #{pipeline.ci_yaml_file_path} file")
end end
return error(pipeline.yaml_errors, save: save_on_errors) return error(pipeline.yaml_errors, save: save_on_errors)
end end
......
...@@ -45,6 +45,14 @@ ...@@ -45,6 +45,14 @@
Per job in minutes. If a job passes this threshold, it will be marked as failed Per job in minutes. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
.form-group
= f.label :ci_config_path, 'Custom CI config path', class: 'label-light'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.help-block
The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr %hr
.form-group .form-group
.checkbox .checkbox
......
---
title: Allow customize CI config path
merge_request: 12509
author: Keith Pope
class AddCiConfigFileToProject < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :projects, :ci_config_path, :string
end
def down
remove_column :projects, :ci_config_path
end
end
...@@ -1086,6 +1086,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do ...@@ -1086,6 +1086,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do
t.string "import_jid" t.string "import_jid"
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.datetime "last_repository_updated_at" t.datetime "last_repository_updated_at"
t.string "ci_config_path"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
...@@ -336,7 +336,7 @@ Parameters: ...@@ -336,7 +336,7 @@ Parameters:
| `snippets_enabled` | boolean | no | Enable snippets for this project | | `snippets_enabled` | boolean | no | Enable snippets for this project |
| `container_registry_enabled` | boolean | no | Enable container registry for this project | | `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `visibility` | String | no | See [project visibility level](#project-visibility-level) | | `visibility` | string | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from | | `import_url` | string | no | URL to import repository from |
| `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members | | `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
...@@ -346,6 +346,7 @@ Parameters: ...@@ -346,6 +346,7 @@ Parameters:
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
| `avatar` | mixed | no | Image file for avatar of the project | | `avatar` | mixed | no | Image file for avatar of the project |
| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line |
| `ci_config_path` | string | no | The path to CI config file |
### Create project for user ### Create project for user
...@@ -382,6 +383,7 @@ Parameters: ...@@ -382,6 +383,7 @@ Parameters:
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
| `avatar` | mixed | no | Image file for avatar of the project | | `avatar` | mixed | no | Image file for avatar of the project |
| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line |
| `ci_config_path` | string | no | The path to CI config file |
### Edit project ### Edit project
...@@ -416,6 +418,7 @@ Parameters: ...@@ -416,6 +418,7 @@ Parameters:
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
| `avatar` | mixed | no | Image file for avatar of the project | | `avatar` | mixed | no | Image file for avatar of the project |
| `ci_config_path` | string | no | The path to CI config file |
### Fork project ### Fork project
......
...@@ -40,6 +40,7 @@ future GitLab releases.** ...@@ -40,6 +40,7 @@ future GitLab releases.**
| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. | | **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built | | **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | | **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | | **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job | | **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | | **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
......
...@@ -27,6 +27,22 @@ The default value is 60 minutes. Decrease the time limit if you want to impose ...@@ -27,6 +27,22 @@ The default value is 60 minutes. Decrease the time limit if you want to impose
a hard limit on your jobs' running time or increase it otherwise. In any case, a hard limit on your jobs' running time or increase it otherwise. In any case,
if the job surpasses the threshold, it is marked as failed. if the job surpasses the threshold, it is marked as failed.
## Custom CI config path
> - [Introduced][ce-12509] in GitLab 9.4.
By default we look for the `.gitlab-ci.yml` file in the project's root
directory. If you require a different location **within** the repository,
you can set a custom filepath that will be used to lookup the config file,
this filepath should be **relative** to the root.
Here are some valid examples:
> * .gitlab-ci.yml
> * .my-custom-file.yml
> * my/path/.gitlab-ci.yml
> * my/path/.my-custom-file.yml
## Test coverage parsing ## Test coverage parsing
If you use test coverage in your code, GitLab can capture its output in the If you use test coverage in your code, GitLab can capture its output in the
...@@ -59,8 +75,8 @@ pipelines** checkbox and save the changes. ...@@ -59,8 +75,8 @@ pipelines** checkbox and save the changes.
> [Introduced][ce-9362] in GitLab 9.1. > [Introduced][ce-9362] in GitLab 9.1.
If you want to auto-cancel all pending non-HEAD pipelines on branch, when If you want to auto-cancel all pending non-HEAD pipelines on branch, when
new pipeline will be created (after your git push or manually from UI), new pipeline will be created (after your git push or manually from UI),
check **Auto-cancel pending pipelines** checkbox and save the changes. check **Auto-cancel pending pipelines** checkbox and save the changes.
## Badges ## Badges
...@@ -115,3 +131,4 @@ into your `README.md`: ...@@ -115,3 +131,4 @@ into your `README.md`:
[var]: ../../../ci/yaml/README.md#git-strategy [var]: ../../../ci/yaml/README.md#git-strategy
[coverage report]: #test-coverage-parsing [coverage report]: #test-coverage-parsing
[ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362 [ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362
[ce-12509]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509
...@@ -112,6 +112,7 @@ module API ...@@ -112,6 +112,7 @@ module API
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? } expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds, as: :public_jobs expose :public_builds, as: :public_jobs
expose :ci_config_path
expose :shared_with_groups do |project, options| expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options) SharedGroup.represent(project.project_group_links.all, options)
end end
......
...@@ -10,6 +10,7 @@ module API ...@@ -10,6 +10,7 @@ module API
helpers do helpers do
params :optional_params_ce do params :optional_params_ce do
optional :description, type: String, desc: 'The description of the project' optional :description, type: String, desc: 'The description of the project'
optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`'
optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled' optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled' optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled' optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
......
...@@ -383,6 +383,7 @@ Project: ...@@ -383,6 +383,7 @@ Project:
- printing_merge_request_link_enabled - printing_merge_request_link_enabled
- build_allow_git_fetch - build_allow_git_fetch
- last_repository_updated_at - last_repository_updated_at
- ci_config_path
Author: Author:
- name - name
ProjectFeature: ProjectFeature:
......
...@@ -1183,6 +1183,7 @@ describe Ci::Build, :models do ...@@ -1183,6 +1183,7 @@ describe Ci::Build, :models do
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true }, { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false }, { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
{ key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false } { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }
...@@ -1473,6 +1474,16 @@ describe Ci::Build, :models do ...@@ -1473,6 +1474,16 @@ describe Ci::Build, :models do
it { is_expected.to include(deployment_variable) } it { is_expected.to include(deployment_variable) }
end end
context 'when project has custom CI config path' do
let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true } }
before do
project.update(ci_config_path: 'custom')
end
it { is_expected.to include(ci_config_path) }
end
context 'returns variables in valid order' do context 'returns variables in valid order' do
let(:build_pre_var) { { key: 'build', value: 'value' } } let(:build_pre_var) { { key: 'build', value: 'value' } }
let(:project_pre_var) { { key: 'project', value: 'value' } } let(:project_pre_var) { { key: 'project', value: 'value' } }
......
...@@ -748,6 +748,39 @@ describe Ci::Pipeline, models: true do ...@@ -748,6 +748,39 @@ describe Ci::Pipeline, models: true do
end end
end end
describe '#ci_yaml_file_path' do
subject { pipeline.ci_yaml_file_path }
it 'returns the path from project' do
allow(pipeline.project).to receive(:ci_config_path) { 'custom/path' }
is_expected.to eq('custom/path')
end
it 'returns default when custom path is nil' do
allow(pipeline.project).to receive(:ci_config_path) { nil }
is_expected.to eq('.gitlab-ci.yml')
end
it 'returns default when custom path is empty' do
allow(pipeline.project).to receive(:ci_config_path) { '' }
is_expected.to eq('.gitlab-ci.yml')
end
end
describe '#ci_yaml_file' do
it 'reports error if the file is not found' do
allow(pipeline.project).to receive(:ci_config_path) { 'custom' }
pipeline.ci_yaml_file
expect(pipeline.yaml_errors)
.to eq('Failed to load CI/CD config file at custom')
end
end
describe '#detailed_status' do describe '#detailed_status' do
subject { pipeline.detailed_status(user) } subject { pipeline.detailed_status(user) }
......
...@@ -143,6 +143,10 @@ describe Project, models: true do ...@@ -143,6 +143,10 @@ describe Project, models: true do
it { is_expected.to validate_length_of(:description).is_at_most(2000) } it { is_expected.to validate_length_of(:description).is_at_most(2000) }
it { is_expected.to validate_length_of(:ci_config_path).is_at_most(255) }
it { is_expected.to allow_value('').for(:ci_config_path) }
it { is_expected.not_to allow_value('test/../foo').for(:ci_config_path) }
it { is_expected.to validate_presence_of(:creator) } it { is_expected.to validate_presence_of(:creator) }
it { is_expected.to validate_presence_of(:namespace) } it { is_expected.to validate_presence_of(:namespace) }
...@@ -1504,6 +1508,28 @@ describe Project, models: true do ...@@ -1504,6 +1508,28 @@ describe Project, models: true do
end end
end end
describe '#ci_config_path=' do
let(:project) { create(:empty_project) }
it 'sets nil' do
project.update!(ci_config_path: nil)
expect(project.ci_config_path).to be_nil
end
it 'sets a string' do
project.update!(ci_config_path: 'foo/.gitlab_ci.yml')
expect(project.ci_config_path).to eq('foo/.gitlab_ci.yml')
end
it 'sets a string but removes all leading slashes and null characters' do
project.update!(ci_config_path: "///f\0oo/\0/.gitlab_ci.yml")
expect(project.ci_config_path).to eq('foo//.gitlab_ci.yml')
end
end
describe 'Project import job' do describe 'Project import job' do
let(:project) { create(:empty_project, import_url: generate(:url)) } let(:project) { create(:empty_project, import_url: generate(:url)) }
......
...@@ -347,7 +347,8 @@ describe API::Projects do ...@@ -347,7 +347,8 @@ describe API::Projects do
wiki_enabled: false, wiki_enabled: false,
only_allow_merge_if_pipeline_succeeds: false, only_allow_merge_if_pipeline_succeeds: false,
request_access_enabled: true, request_access_enabled: true,
only_allow_merge_if_all_discussions_are_resolved: false only_allow_merge_if_all_discussions_are_resolved: false,
ci_config_path: 'a/custom/path'
}) })
post api('/projects', user), project post api('/projects', user), project
...@@ -653,6 +654,7 @@ describe API::Projects do ...@@ -653,6 +654,7 @@ describe API::Projects do
expect(json_response['star_count']).to be_present expect(json_response['star_count']).to be_present
expect(json_response['forks_count']).to be_present expect(json_response['forks_count']).to be_present
expect(json_response['public_jobs']).to be_present expect(json_response['public_jobs']).to be_present
expect(json_response['ci_config_path']).to be_nil
expect(json_response['shared_with_groups']).to be_an Array expect(json_response['shared_with_groups']).to be_an Array
expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'].length).to eq(1)
expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
......
...@@ -74,7 +74,9 @@ module CycleAnalyticsHelpers ...@@ -74,7 +74,9 @@ module CycleAnalyticsHelpers
def dummy_pipeline def dummy_pipeline
@dummy_pipeline ||= @dummy_pipeline ||=
Ci::Pipeline.new(sha: project.repository.commit('master').sha) Ci::Pipeline.new(
sha: project.repository.commit('master').sha,
project: project)
end end
def new_dummy_job(environment) def new_dummy_job(environment)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment