diff options
45 files changed, 658 insertions, 50 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 687f09882a7..16c5d0fa344 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => { propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); pipelineTableViewEl.appendChild(table.$el); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index dd751ec97a8..c931e1e0ea5 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -13,6 +13,10 @@ type: String, required: true, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, mixins: [ pipelinesMixin, @@ -95,6 +99,7 @@ <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsHelpPath" /> </div> </div> diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3b3620fe61b..0c3c877ff15 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -243,6 +243,7 @@ import bp from './breakpoints'; propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }).$mount(); diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 2ca5ac2912f..f0b44dfa6d8 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -1,29 +1,45 @@ <script> -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + import popover from '../../vue_shared/directives/popover'; -export default { - props: { - pipeline: { - type: Object, - required: true, + export default { + props: { + pipeline: { + type: Object, + required: true, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, - }, - components: { - userAvatarLink, - }, - directives: { - tooltip, - }, - computed: { - user() { - return this.pipeline.user; + components: { + userAvatarLink, }, - }, -}; + directives: { + tooltip, + popover, + }, + computed: { + user() { + return this.pipeline.user; + }, + popoverOptions() { + return { + html: true, + delay: { hide: 600 }, + trigger: 'hover', + placement: 'top', + title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', + content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, + }; + }, + }, + }; </script> <template> - <div class="table-section section-15 hidden-xs hidden-sm"> + <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -57,6 +73,13 @@ export default { :title="pipeline.yaml_errors"> yaml invalid </span> + <a + v-if="pipeline.flags.auto_devops" + class="js-pipeline-url-autodevops label label-info autodevops-badge" + v-popover="popoverOptions" + role="button"> + Auto DevOps + </a> <span v-if="pipeline.flags.stuck" class="js-pipeline-url-stuck label label-warning"> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 010063a0240..5ce4fe58e55 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -27,6 +27,7 @@ endpoint: pipelinesData.endpoint, cssClass: pipelinesData.cssClass, helpPagePath: pipelinesData.helpPagePath, + autoDevopsPath: pipelinesData.helpAutoDevopsPath, newPipelinePath: pipelinesData.newPipelinePath, canCreatePipeline: pipelinesData.canCreatePipeline, allPath: pipelinesData.allPath, @@ -200,6 +201,7 @@ <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsPath" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 5088d92209f..7aa0c0e8a7f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -17,6 +17,10 @@ required: false, default: false, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, components: { pipelinesTableRowComponent, @@ -54,6 +58,7 @@ :key="model.id" :pipeline="model" :update-graph-dropdown="updateGraphDropdown" + :auto-devops-help-path="autoDevopsHelpPath" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index c3f1c426d8a..5b9bb6c3750 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -25,6 +25,10 @@ export default { required: false, default: false, }, + autoDevopsHelpPath: { + type: String, + required: true, + }, }, components: { asyncButtonComponent, @@ -218,7 +222,10 @@ export default { </div> </div> - <pipeline-url :pipeline="pipeline" /> + <pipeline-url + :pipeline="pipeline" + :auto-devops-help-path="autoDevopsHelpPath" + /> <div class="table-section section-25"> <div diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js new file mode 100644 index 00000000000..05fa563cbd0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/popover.js @@ -0,0 +1,20 @@ +/** + * Helper to user bootstrap popover in vue.js. + * Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover + * + * @example + * import popover from 'vue_shared/directives/popover.js'; + * { + * directives: [popover] + * } + * <a v-popover="{options}">popover</a> + */ +export default { + bind(el, binding) { + $(el).popover(binding.value); + }, + + unbind(el) { + $(el).popover('destroy'); + }, +}; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 587a202d6dd..994707422bb 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -226,6 +226,14 @@ vertical-align: baseline; } + a.autodevops-badge { + color: $white-light; + } + + a.autodevops-link { + color: $gl-link-color; + } + .commit-row-description { font-size: 14px; padding: 10px 15px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cb8815e4775..296b6310552 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -202,6 +202,10 @@ .btn-group.open .dropdown-toggle { box-shadow: none; } + + .pipeline-tags .label-container { + white-space: normal; + } } .stage-cell { @@ -932,3 +936,8 @@ button.mini-pipeline-graph-dropdown-toggle { .pipelines-container .top-area .nav-controls > .btn:last-child { float: none; } + +.autodevops-title { + font-weight: $gl-font-weight-normal; + line-height: 1.5; +} diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 9d24ebe2138..abab2e2f0c9 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -6,7 +6,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController end def update - if @project.update_attributes(update_params) + if @project.update(update_params) flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." redirect_to project_settings_ci_cd_path(@project) else @@ -16,14 +16,12 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController private - def create_params - params.require(:pipeline).permit(:ref) - end - def update_params params.require(:project).permit( - :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds, :auto_cancel_pending_pipelines, :ci_config_path + :runners_token, :builds_enabled, :build_allow_git_fetch, + :build_timeout_in_minutes, :build_coverage_regex, :public_builds, + :auto_cancel_pending_pipelines, :ci_config_path, + auto_devops_attributes: [:id, :domain, :enabled] ) end end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 15a2ff56b92..b029b31f9af 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -8,6 +8,7 @@ module Projects define_secret_variables define_triggers_variables define_badges_variables + define_auto_devops_variables end private @@ -42,6 +43,10 @@ module Projects badge.new(@project, @ref).metadata end end + + def define_auto_devops_variables + @auto_devops = @project.auto_devops || ProjectAutoDevops.new + end end end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index b93f5f0af1c..7bd34df5c95 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -115,6 +115,7 @@ module ApplicationSettingsHelper :after_sign_up_text, :akismet_api_key, :akismet_enabled, + :auto_devops_enabled, :clientside_sentry_dsn, :clientside_sentry_enabled, :container_registry_token_expire_delay, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 64c93966dff..5ebe6f180e6 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -216,6 +216,7 @@ module Ci variables += runner.predefined_variables if runner variables += project.container_registry_variables variables += project.deployment_variables if has_environment? + variables += project.auto_devops_variables variables += yaml_variables variables += user_variables variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 46e5c344fdc..871c76fbad3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -38,6 +38,7 @@ module Ci validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? + after_initialize :set_config_source, if: :new_record? after_create :keep_around_commits, unless: :importing? enum source: { @@ -50,6 +51,12 @@ module Ci external: 6 } + enum config_source: { + unknown_source: nil, + repository_source: 1, + auto_devops_source: 2 + } + state_machine :status, initial: :created do event :enqueue do transition created: :pending @@ -316,6 +323,14 @@ module Ci builds.latest.failed_but_allowed.any? end + def set_config_source + if ci_yaml_from_repo + self.config_source = :repository_source + elsif implied_ci_yaml_file + self.config_source = :auto_devops_source + end + end + def config_processor return unless ci_yaml_file return @config_processor if defined?(@config_processor) @@ -342,11 +357,17 @@ module Ci def ci_yaml_file return @ci_yaml_file if defined?(@ci_yaml_file) - @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}" + @ci_yaml_file = + if auto_devops_source? + implied_ci_yaml_file + else + ci_yaml_from_repo + end + + if @ci_yaml_file + @ci_yaml_file + else + self.yaml_errors = "Failed to load CI/CD config file for #{sha}" nil end end @@ -434,6 +455,23 @@ module Ci private + def ci_yaml_from_repo + return unless project + return unless sha + + project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path) + rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal + nil + end + + def implied_ci_yaml_file + return unless project + + if project.auto_devops_enabled? + Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content + end + end + def pipeline_data Gitlab::DataBuilder::Pipeline.build(self) end diff --git a/app/models/project.rb b/app/models/project.rb index 18800921c6c..039dacf1945 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -187,9 +187,12 @@ class Project < ActiveRecord::Base has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' + has_one :auto_devops, class_name: 'ProjectAutoDevops' + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature accepts_nested_attributes_for :import_data + accepts_nested_attributes_for :auto_devops delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true @@ -466,6 +469,14 @@ class Project < ActiveRecord::Base self[:lfs_enabled] && Gitlab.config.lfs.enabled end + def auto_devops_enabled? + if auto_devops&.enabled.nil? + current_application_settings.auto_devops_enabled? + else + auto_devops.enabled? + end + end + def repository_storage_path Gitlab.config.repositories.storages[repository_storage].try(:[], 'path') end @@ -1378,6 +1389,10 @@ class Project < ActiveRecord::Base Gitlab::Utils.slugify(full_path.to_s) end + def has_ci? + repository.gitlab_ci_yml || auto_devops_enabled? + end + def predefined_variables [ { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, @@ -1423,6 +1438,12 @@ class Project < ActiveRecord::Base deployment_service.predefined_variables end + def auto_devops_variables + return [] unless auto_devops_enabled? + + auto_devops&.variables || [] + end + def append_or_update_attribute(name, value) old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb new file mode 100644 index 00000000000..53731579e87 --- /dev/null +++ b/app/models/project_auto_devops.rb @@ -0,0 +1,11 @@ +class ProjectAutoDevops < ActiveRecord::Base + belongs_to :project + + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } + + def variables + variables = [] + variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present? + variables + end +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index c4f000b0ca3..357fc71f877 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -16,6 +16,7 @@ class PipelineEntity < Grape::Entity expose :flags do expose :latest?, as: :latest expose :stuck?, as: :stuck + expose :auto_devops_source?, as: :auto_devops expose :has_yaml_errors?, as: :yaml_errors expose :can_retry?, as: :retryable expose :can_cancel?, as: :cancelable diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a010b4691bf..dbaed1d09fb 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -226,7 +226,17 @@ .help-block 0 for unlimited %fieldset - %legend Continuous Integration + %legend Continuous Integration and Deployment + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :auto_devops_enabled do + = f.check_box :auto_devops_enabled + Enabled Auto DevOps (Beta) for projects by default + .help-block + It will automatically build, test, and deploy applications based on a predefined CI/CD configuration + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md') + .form-group .col-sm-offset-2.col-sm-10 .checkbox diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 09e3a775d1c..bef96786b73 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -2,6 +2,7 @@ #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, "help-page-path" => help_page_path('ci/quick_start/README'), + "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), } } - content_for :page_specific_javascripts do diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index c1729850cf4..579f681abaa 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,6 +5,7 @@ #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), "css-class" => container_class, "help-page-path" => help_page_path('ci/quick_start/README'), + "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "new-pipeline-path" => new_project_pipeline_path(@project), "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "all-path" => project_pipelines_path(@project), @@ -13,7 +14,7 @@ "finished-path" => project_pipelines_path(@project, scope: :finished), "branches-path" => project_pipelines_path(@project, scope: :branches), "tags-path" => project_pipelines_path(@project, scope: :tags), - "has-ci" => @repository.gitlab_ci_yml, + "has-ci" => @project.has_ci?, "ci-lint-path" => ci_lint_path } } = page_specific_javascript_bundle_tag('common_vue') diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 8bf76b646f7..324cd423ede 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -2,11 +2,42 @@ .col-lg-12 = form_for @project, url: project_pipelines_settings_path(@project) do |f| %fieldset.builds-feature - - unless @repository.gitlab_ci_yml - .form-group - %p Pipelines need to be configured before you can begin using Continuous Integration. - = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' - %hr + .form-group + %p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery. + %h5 Auto DevOps (Beta) + %p + Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration. + = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md') + = f.fields_for :auto_devops_attributes, @auto_devops do |form| + .radio + = form.label :enabled_true do + = form.radio_button :enabled, 'true' + %strong Enable Auto DevOps + %br + %span.descr + The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml + in the project. + .radio + = form.label :enabled_false do + = form.radio_button :enabled, 'false' + %strong Disable Auto DevOps + %br + %span.descr + A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery. + .radio + = form.label :enabled do + = form.radio_button :enabled, nil + %strong + Instance default (status: #{current_application_settings.auto_devops_enabled?}) + %br + %span.descr + Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified. + %br + %p + Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed. + = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' + + %hr .form-group.append-bottom-default = f.label :runners_token, "Runner token", class: 'label-light' = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' diff --git a/changelogs/unreleased/zj-auto-devops-table.yml b/changelogs/unreleased/zj-auto-devops-table.yml new file mode 100644 index 00000000000..f1a004ebd19 --- /dev/null +++ b/changelogs/unreleased/zj-auto-devops-table.yml @@ -0,0 +1,5 @@ +--- +title: Allow users and administrator to configure Auto-DevOps +merge_request: 13923 +author: +type: added diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb index f977104ff9d..1ad9ddca877 100644 --- a/config/initializers/0_inflections.rb +++ b/config/initializers/0_inflections.rb @@ -10,5 +10,10 @@ # end # ActiveSupport::Inflector.inflections do |inflect| - inflect.uncountable %w(award_emoji project_statistics system_note_metadata) + inflect.uncountable %w( + award_emoji + project_statistics + system_note_metadata + project_auto_devops + ) end diff --git a/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb new file mode 100644 index 00000000000..da518d8215c --- /dev/null +++ b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb @@ -0,0 +1,15 @@ +class AddAutoDevopsEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :auto_devops_enabled, :boolean, default: false) + end + + def down + remove_column(:application_settings, :auto_devops_enabled, :boolean) + end +end diff --git a/db/migrate/20170828093725_create_project_auto_dev_ops.rb b/db/migrate/20170828093725_create_project_auto_dev_ops.rb new file mode 100644 index 00000000000..c1bb4f20c1d --- /dev/null +++ b/db/migrate/20170828093725_create_project_auto_dev_ops.rb @@ -0,0 +1,19 @@ +class CreateProjectAutoDevOps < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + create_table :project_auto_devops do |t| + t.belongs_to :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + t.datetime_with_timezone :created_at, null: false + t.datetime_with_timezone :updated_at, null: false + t.boolean :enabled, default: nil, null: true + t.string :domain + end + end + + def down + drop_table(:project_auto_devops) + end +end diff --git a/db/migrate/20170831092813_add_config_source_to_pipelines.rb b/db/migrate/20170831092813_add_config_source_to_pipelines.rb new file mode 100644 index 00000000000..ff51e968abd --- /dev/null +++ b/db/migrate/20170831092813_add_config_source_to_pipelines.rb @@ -0,0 +1,7 @@ +class AddConfigSourceToPipelines < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:ci_pipelines, :config_source, :integer, allow_null: true) + end +end diff --git a/db/schema.rb b/db/schema.rb index 86c1dda537e..bcb750184db 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -133,6 +133,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "performance_bar_allowed_group_id" t.boolean "hashed_storage_enabled", default: false, null: false t.boolean "project_export_enabled", default: true, null: false + t.boolean "auto_devops_enabled", default: false, null: false end create_table "audit_events", force: :cascade do |t| @@ -340,6 +341,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "auto_canceled_by_id" t.integer "pipeline_schedule_id" t.integer "source" + t.integer "config_source" t.boolean "protected" end @@ -1108,6 +1110,16 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree + create_table "project_auto_devops", force: :cascade do |t| + t.integer "project_id", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.boolean "enabled" + t.string "domain" + end + + add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree + create_table "project_features", force: :cascade do |t| t.integer "project_id" t.integer "merge_requests_access_level" @@ -1724,6 +1736,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_foreign_key "personal_access_tokens", "users" add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade + add_foreign_key "project_auto_devops", "projects", on_delete: :cascade add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index ec73846d844..2171c6c7bbb 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -50,6 +50,7 @@ project_tree: - :push_event_payload - :stages - :statuses + - :auto_devops - :triggers - :pipeline_schedules - :services @@ -142,4 +143,4 @@ methods: events: - :action push_event_payload: - - :action
\ No newline at end of file + - :action diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index d563a87dcfd..380b336395d 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -14,6 +14,7 @@ module Gitlab create_access_levels: 'ProtectedTag::CreateAccessLevel', labels: :project_labels, priorities: :label_priorities, + auto_devops: :project_auto_devops, label: :project_label }.freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb new file mode 100644 index 00000000000..8d124dc2381 --- /dev/null +++ b/spec/factories/project_auto_devops.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :project_auto_devops do + project + enabled true + domain "example.com" + end +end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 232d796a200..975d204e75e 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -41,5 +41,15 @@ feature "Pipelines settings" do checkbox = find_field('project_auto_cancel_pending_pipelines') expect(checkbox).to be_checked end + + scenario 'update auto devops settings' do + fill_in('project_auto_devops_attributes_domain', with: 'test.com') + page.choose('project_auto_devops_attributes_enabled_false') + click_on 'Save changes' + + expect(page.status_code).to eq(200) + expect(project.auto_devops).to be_present + expect(project.auto_devops).not_to be_enabled + end end end diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index a34cadec0ab..454f187ccbc 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -29,6 +29,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); }); @@ -64,6 +65,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); }); @@ -115,6 +117,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); element.appendChild(this.component.$el); @@ -136,6 +139,7 @@ describe('Pipelines table in Commits and Merge requests', () => { propsData: { endpoint: 'endpoint', helpPagePath: 'foo', + autoDevopsHelpPath: 'foo', }, }).$mount(); }); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 3c4b20a5f06..256fdbe743c 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -16,6 +16,7 @@ describe('Pipeline Url Component', () => { path: 'foo', flags: {}, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -30,6 +31,7 @@ describe('Pipeline Url Component', () => { path: 'foo', flags: {}, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -50,6 +52,7 @@ describe('Pipeline Url Component', () => { path: '/', }, }, + autoDevopsHelpPath: 'foo', }; const component = new PipelineUrlComponent({ @@ -73,6 +76,7 @@ describe('Pipeline Url Component', () => { path: 'foo', flags: {}, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -91,6 +95,7 @@ describe('Pipeline Url Component', () => { stuck: true, }, }, + autoDevopsHelpPath: 'foo', }, }).$mount(); @@ -98,4 +103,26 @@ describe('Pipeline Url Component', () => { expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid'); expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); }); + + it('should render a badge for autodevops', () => { + const component = new PipelineUrlComponent({ + propsData: { + pipeline: { + id: 1, + path: 'foo', + flags: { + latest: true, + yaml_errors: true, + stuck: true, + auto_devops: true, + }, + }, + autoDevopsHelpPath: 'foo', + }, + }).$mount(); + + expect( + component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(), + ).toEqual('Auto DevOps'); + }); }); diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 7ce39dca112..d7456a48bc1 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -9,7 +9,7 @@ describe('Pipelines Table Row', () => { el: document.querySelector('.test-dom-element'), propsData: { pipeline, - service: {}, + autoDevopsHelpPath: 'foo', }, }).$mount(); }; diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index 3afe89c8db4..4f5eb42eb35 100644 --- a/spec/javascripts/pipelines/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -22,6 +22,7 @@ describe('Pipelines Table', () => { component = new PipelinesTableComponent({ propsData: { pipelines: [], + autoDevopsHelpPath: 'foo', }, }).$mount(); }); @@ -47,6 +48,7 @@ describe('Pipelines Table', () => { const component = new PipelinesTableComponent({ propsData: { pipelines: [], + autoDevopsHelpPath: 'foo', }, }).$mount(); expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0); @@ -58,6 +60,7 @@ describe('Pipelines Table', () => { const component = new PipelinesTableComponent({ propsData: { pipelines: [pipeline], + autoDevopsHelpPath: 'foo', }, }).$mount(); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index beed4e77e8b..3fb8edeb701 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -256,6 +256,7 @@ project: - environments - deployments - project_feature +- auto_devops - pages_domains - authorized_users - project_authorizations diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 1613b968bb6..899d17d97c2 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -223,6 +223,7 @@ Ci::Pipeline: - lock_version - auto_canceled_by_id - pipeline_schedule_id +- config_source - protected Ci::Stage: - id @@ -466,3 +467,10 @@ Timelog: - user_id - created_at - updated_at +ProjectAutoDevops: +- id +- enabled +- domain +- project_id +- created_at +- updated_at diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index f921545668d..c7a9eabdf06 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -5,6 +5,7 @@ describe ApplicationSetting do it { expect(setting).to be_valid } it { expect(setting.uuid).to be_present } + it { expect(setting).to have_db_column(:auto_devops_enabled) } describe 'validations' do let(:http) { 'http://example.com' } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 08d22f166e4..c2c9f1c12d1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1688,6 +1688,30 @@ describe Ci::Build do { key: 'secret', value: 'value', public: false }]) end end + + context 'when using auto devops' do + context 'and is enabled' do + before do + project.create_auto_devops!(enabled: true, domain: 'example.com') + end + + it "includes AUTO_DEVOPS_DOMAIN" do + is_expected.to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + + context 'and is disabled' do + before do + project.create_auto_devops!(enabled: false, domain: 'example.com') + end + + it "includes AUTO_DEVOPS_DOMAIN" do + is_expected.not_to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + end end describe 'state transition: any => [:pending]' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 84656ffe0b9..95da97b7bc5 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -799,14 +799,118 @@ describe Ci::Pipeline, :mailer do end end + describe '#set_config_source' do + context 'on object initialisation' do + context 'when pipelines does not contain needed data' do + let(:pipeline) do + Ci::Pipeline.new + end + + it 'defines source to be unknown' do + expect(pipeline).to be_unknown_source + end + end + + context 'when pipeline contains all needed data' do + let(:pipeline) do + Ci::Pipeline.new( + project: project, + sha: '1234', + ref: 'master', + source: :push) + end + + context 'when the repository has a config file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') + end + + it 'defines source to be from repository' do + expect(pipeline).to be_repository_source + end + + context 'when loading an object' do + let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) } + + it 'does not redefine the source' do + # force to overwrite the source + pipeline.unknown_source! + + expect(new_pipeline).to be_unknown_source + end + end + end + + context 'when the repository does not have a config file' do + let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } + + context 'auto devops enabled' do + before do + stub_application_setting(auto_devops_enabled: true) + allow(project).to receive(:ci_config_path) { 'custom' } + end + + it 'defines source to be auto devops' do + subject + + expect(pipeline).to be_auto_devops_source + end + end + end + end + 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' } + let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } + + context 'the source is unknown' do + before do + pipeline.unknown_source! + end + + it 'returns the configuration if found' do + allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') + + expect(pipeline.ci_yaml_file).to be_a(String) + expect(pipeline.ci_yaml_file).not_to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil + end + + it 'sets yaml errors if not found' do + expect(pipeline.ci_yaml_file).to be_nil + expect(pipeline.yaml_errors) + .to start_with('Failed to load CI/CD config file') + end + end + + context 'the source is the repository' do + before do + pipeline.repository_source! + end - pipeline.ci_yaml_file + it 'returns the configuration if found' do + allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') - expect(pipeline.yaml_errors) - .to eq('Failed to load CI/CD config file at custom') + expect(pipeline.ci_yaml_file).to be_a(String) + expect(pipeline.ci_yaml_file).not_to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil + end + end + + context 'when the source is auto_devops_source' do + before do + stub_application_setting(auto_devops_enabled: true) + pipeline.auto_devops_source! + end + + it 'finds the implied config' do + expect(pipeline.ci_yaml_file).to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil + end end end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb new file mode 100644 index 00000000000..ca13af4d73e --- /dev/null +++ b/spec/models/project_auto_devops_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe ProjectAutoDevops do + set(:project) { build(:project) } + + it { is_expected.to belong_to(:project) } + + it { is_expected.to respond_to(:created_at) } + it { is_expected.to respond_to(:updated_at) } + + describe 'variables' do + let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) } + + context 'when domain is defined' do + let(:domain) { 'example.com' } + + it 'returns AUTO_DEVOPS_DOMAIN' do + expect(auto_devops.variables).to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1f7c6a82b91..75c99b62150 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -53,6 +53,7 @@ describe Project do it { is_expected.to have_one(:import_data).class_name('ProjectImportData') } it { is_expected.to have_one(:last_event).class_name('Event') } it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) } + it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') } it { is_expected.to have_many(:commit_statuses) } it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:builds) } @@ -2508,4 +2509,133 @@ describe Project do end end end + + describe '#has_ci?' do + set(:project) { create(:project) } + let(:repository) { double } + + before do + expect(project).to receive(:repository) { repository } + end + + context 'when has .gitlab-ci.yml' do + before do + expect(repository).to receive(:gitlab_ci_yml) { 'content' } + end + + it "CI is available" do + expect(project).to have_ci + end + end + + context 'when there is no .gitlab-ci.yml' do + before do + expect(repository).to receive(:gitlab_ci_yml) { nil } + end + + it "CI is not available" do + expect(project).not_to have_ci + end + + context 'when auto devops is enabled' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it "CI is available" do + expect(project).to have_ci + end + end + end + end + + describe '#auto_devops_enabled?' do + set(:project) { create(:project) } + + subject { project.auto_devops_enabled? } + + context 'when enabled in settings' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it 'auto devops is implicitly enabled' do + expect(project.auto_devops).to be_nil + expect(project).to be_auto_devops_enabled + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it "auto devops is enabled" do + expect(project).to be_auto_devops_enabled + end + end + + context 'when explicitly disabled' do + before do + create(:project_auto_devops, project: project, enabled: false) + end + + it "auto devops is disabled" do + expect(project).not_to be_auto_devops_enabled + end + end + end + + context 'when disabled in settings' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it 'auto devops is implicitly disabled' do + expect(project.auto_devops).to be_nil + expect(project).not_to be_auto_devops_enabled + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it "auto devops is enabled" do + expect(project).to be_auto_devops_enabled + end + end + end + end + + context '#auto_devops_variables' do + set(:project) { create(:project) } + + subject { project.auto_devops_variables } + + context 'when enabled in settings' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + context 'when domain is empty' do + before do + create(:project_auto_devops, project: project, domain: nil) + end + + it 'variables are empty' do + is_expected.to be_empty + end + end + + context 'when domain is configured' do + before do + create(:project_auto_devops, project: project, domain: 'example.com') + end + + it "variables are not empty" do + is_expected.not_to be_empty + end + end + end + end end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 881f2b6bfd8..f8df461bc81 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -36,7 +36,7 @@ describe PipelineEntity do it 'contains flags' do expect(subject).to include :flags expect(subject[:flags]) - .to include :latest, :stuck, + .to include :latest, :stuck, :auto_devops, :yaml_errors, :retryable, :cancelable end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 009d67a3fbe..4c2ff08039c 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreatePipelineService do - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } let(:user) { create(:admin) } let(:ref_name) { 'refs/heads/master' } |