diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index b424e7f205d43825ecb1a1477330a8e7a6c08776..50c725aa3d54954d5446c976195532b9c9fc075f 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate'; import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; +import { setupPipelineVariableList } from './setup_pipeline_variable_list'; Vue.use(Translate); @@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => { gl.timezoneDropdown = new TimezoneDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); + + setupPipelineVariableList($('.js-pipeline-variable-list')); }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js new file mode 100644 index 0000000000000000000000000000000000000000..644efd10509b081e21a8d6451f54d2c0e44fb8f4 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -0,0 +1,71 @@ +function insertRow($row) { + const $rowClone = $row.clone(); + $rowClone.removeAttr('data-is-persisted'); + $rowClone.find('input, textarea').val(''); + $row.after($rowClone); +} + +function removeRow($row) { + const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted')); + + if (isPersisted) { + $row.hide(); + $row + .find('.js-destroy-input') + .val(1); + } else { + $row.remove(); + } +} + +function checkIfRowTouched($row) { + return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0); +} + +function setupPipelineVariableList(parent = document) { + const $parent = $(parent); + + $parent.on('click', '.js-row-remove-button', (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + removeRow($row); + + e.preventDefault(); + }); + + // Remove any empty rows except the last r + $parent.on('blur', '.js-user-input', (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + + const isTouched = checkIfRowTouched($row); + if ($row.is(':not(:last-child)') && !isTouched) { + removeRow($row); + } + }); + + // Always make sure there is an empty last row + $parent.on('input', '.js-user-input', () => { + const $lastRow = $parent.find('.js-row').last(); + + const isTouched = checkIfRowTouched($lastRow); + if (isTouched) { + insertRow($lastRow); + } + }); + + // Clear out the empty last row so it + // doesn't get submitted and throw validation errors + $parent.closest('form').on('submit', () => { + const $lastRow = $parent.find('.js-row').last(); + + const isTouched = checkIfRowTouched($lastRow); + if (!isTouched) { + $lastRow.find('input, textarea').attr('name', ''); + } + }); +} + +export { + setupPipelineVariableList, + insertRow, + removeRow, +}; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d2dd156633b733ab0f712b4a05bef2591618c378..fb889af3175c6cf82e54cce03a32a5de893eaf82 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -579,6 +579,12 @@ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; $action-icon-color: #d6d6d6; +/* +Pipeline Schedules +*/ +$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); + + /* Filtered Search */ diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 595eb40fec71d6149ff96640b68f009cf999dcd6..b3743a7c88dd1cd292692837af3d96560edc117b 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -74,3 +74,65 @@ margin-right: 3px; } } + +.pipeline-variable-list { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; +} + +.pipeline-variable-row { + display: flex; + + &:not(:last-child) { + margin-bottom: $gl-btn-padding; + } + + @media (max-width: $screen-xs-max) { + flex-wrap: wrap; + } + + &:last-child { + & > .pipeline-variable-row-remove-button { + display: none; + } + + & > .pipeline-variable-value-input { + margin-right: $pipeline-variable-remove-button-width; + } + } +} + +.pipeline-variable-key-input { + margin-right: $gl-btn-padding; + + @media (max-width: $screen-xs-max) { + margin-right: $pipeline-variable-remove-button-width; + margin-bottom: $gl-btn-padding; + } +} + +.pipeline-variable-value-input { + @media (max-width: $screen-xs-max) { + flex: 1; + } +} + +.pipeline-variable-row-remove-button { + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + width: $pipeline-variable-remove-button-width; + padding: 0; + background: transparent; + border: 0; + color: $gl-text-color-secondary; + @include transition(color); + + &:hover, + &:focus { + outline: none; + color: $gl-text-color; + } +} diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 60db179277b574404372ba8da6995b728bf05fc6..aa71f6066577b49d4ac9ad2519bbd8db9962ee15 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -1,11 +1,11 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController + before_action :schedule, except: [:index, :new, :create] + before_action :authorize_read_pipeline_schedule! before_action :authorize_create_pipeline_schedule!, only: [:new, :create] - before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update] + before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create] before_action :authorize_admin_pipeline_schedule!, only: [:destroy] - before_action :schedule, only: [:edit, :update, :destroy, :take_ownership] - def index @scope = params[:scope] @all_schedules = PipelineSchedulesFinder.new(@project).execute @@ -53,7 +53,7 @@ def destroy redirect_to pipeline_schedules_path(@project), status: 302 else redirect_to pipeline_schedules_path(@project), - status: 302, + status: :forbidden, alert: _("Failed to remove the pipeline schedule") end end @@ -66,6 +66,11 @@ def schedule def schedule_params params.require(:schedule) - .permit(:description, :cron, :cron_timezone, :ref, :active) + .permit(:description, :cron, :cron_timezone, :ref, :active, + variables_attributes: [:id, :key, :value, :_destroy] ) + end + + def authorize_update_pipeline_schedule! + return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule) end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 179a17c1545546f6b4c086f4718ebc524fb7369a..24fa5b8ab5eff721e9d3b6fce8ab6f2cbd2638b5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -204,6 +204,7 @@ def variables(environment: persisted_environment) variables += secret_variables(environment: environment) variables += trigger_request.user_variables if trigger_request variables += persisted_environment_variables if environment + variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule variables end diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 45d8cd34359b8e1811e97bcf2df9b71f7b364813..e4ae1b35f66f6e1782b7dde22ec31d3dea5a3ed4 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -9,17 +9,21 @@ class PipelineSchedule < ActiveRecord::Base belongs_to :owner, class_name: 'User' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_many :pipelines + has_many :variables, class_name: 'Ci::PipelineScheduleVariable' validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :description, presence: true + validates :variables, variable_duplicates: true before_save :set_next_run_at scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } + accepts_nested_attributes_for :variables, allow_destroy: true + def owned_by?(current_user) owner == current_user end @@ -56,5 +60,9 @@ def real_next_run( Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone) .next_time_from(next_run_at) end + + def job_variables + variables&.map(&:to_runner_variable) || [] + end end end diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb new file mode 100644 index 0000000000000000000000000000000000000000..ee5b8733facee7906a378ad6add9b5d172e7704c --- /dev/null +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -0,0 +1,10 @@ +module Ci + class PipelineScheduleVariable < ActiveRecord::Base + extend Ci::Model + include HasVariable + + belongs_to :pipeline_schedule + + validates :key, uniqueness: { scope: :pipeline_schedule_id } + end +end diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb index 1877e89bb23f1cc2c998ce46ae35c5cadd0734e3..db561dafbd34a12600ee055acdb4a0831497848d 100644 --- a/app/policies/ci/pipeline_schedule_policy.rb +++ b/app/policies/ci/pipeline_schedule_policy.rb @@ -1,4 +1,25 @@ module Ci class PipelineSchedulePolicy < PipelinePolicy + alias_method :pipeline_schedule, :subject + + condition(:protected_action) do + owned_by_developer? && owned_by_another? + end + + rule { protected_action }.prevent :update_pipeline_schedule + + private + + def owned_by_developer? + return false unless @user + + pipeline_schedule.project.team.developer?(@user) + end + + def owned_by_another? + return false unless @user + + !pipeline_schedule.owned_by?(@user) + end end end diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb new file mode 100644 index 0000000000000000000000000000000000000000..8a9d8892e9bd4026ed3afa3609a078f07e9d2a9e --- /dev/null +++ b/app/validators/variable_duplicates_validator.rb @@ -0,0 +1,13 @@ +# VariableDuplicatesValidator +# +# This validtor is designed for especially the following condition +# - Use `accepts_nested_attributes_for :xxx` in a parent model +# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model +class VariableDuplicatesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + duplicates = value.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first) + if duplicates.any? + record.errors.add(attribute, "Duplicate variables: #{duplicates.join(", ")}") + end + end +end diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index fc7fa5c1876792b560e968dc7dd8415766557476..bafb0013f91cfff0b38a36fe0b4d74c09122e93d 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -22,6 +22,14 @@ = f.label :ref, _('Target Branch'), class: 'label-light' = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true + .form-group + .col-md-9 + %label.label-light + #{ s_('PipelineSchedules|Variables') } + %ul.js-pipeline-variable-list.pipeline-variable-list + - @schedule.variables.each do |variable| + = render 'variable_row', id: variable.id, key: variable.key, value: variable.value + = render 'variable_row' .form-group .col-md-9 = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..68713561f258ab1fb83f3a9646b0a18dd600d043 --- /dev/null +++ b/app/views/projects/pipeline_schedules/_variable_row.html.haml @@ -0,0 +1,16 @@ +- id = local_assigns.fetch(:id, nil) +- key = local_assigns.fetch(:key, "") +- value = local_assigns.fetch(:value, "") +%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } } + %input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id } + %input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" } + %input.js-user-input.pipeline-variable-key-input.form-control{ type: "text", + name: "schedule[variables_attributes][][key]", + value: key, + placeholder: s_('PipelineSchedules|Input variable key') } + %textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1, + name: "schedule[variables_attributes][][value]", + placeholder: s_('PipelineSchedules|Input variable value') } + = value + %button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': s_('PipelineSchedules|Remove variable row') } + %i.fa.fa-minus-circle{ 'aria-hidden': "true" } diff --git a/changelogs/unreleased/feature-intermediate-32568-adding-variables-to-pipelines-schedules.yml b/changelogs/unreleased/feature-intermediate-32568-adding-variables-to-pipelines-schedules.yml new file mode 100644 index 0000000000000000000000000000000000000000..d497575b7f3c78fbe16d804c1a6df9452befa062 --- /dev/null +++ b/changelogs/unreleased/feature-intermediate-32568-adding-variables-to-pipelines-schedules.yml @@ -0,0 +1,4 @@ +--- +title: Add variables to pipelines schedules +merge_request: 12372 +author: diff --git a/db/migrate/20170620064728_create_ci_pipeline_schedule_variables.rb b/db/migrate/20170620064728_create_ci_pipeline_schedule_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..92833765a827457bf36f9648f5a385bc96e6f243 --- /dev/null +++ b/db/migrate/20170620064728_create_ci_pipeline_schedule_variables.rb @@ -0,0 +1,25 @@ +class CreateCiPipelineScheduleVariables < ActiveRecord::Migration + DOWNTIME = false + + def up + create_table :ci_pipeline_schedule_variables do |t| + t.string :key, null: false + t.text :value + t.text :encrypted_value + t.string :encrypted_value_salt + t.string :encrypted_value_iv + t.integer :pipeline_schedule_id, null: false + + t.timestamps_with_timezone null: true + end + + add_index :ci_pipeline_schedule_variables, + [:pipeline_schedule_id, :key], + name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", + unique: true + end + + def down + drop_table :ci_pipeline_schedule_variables + end +end diff --git a/db/migrate/20170620065449_add_foreign_key_to_ci_pipeline_schedule_variables.rb b/db/migrate/20170620065449_add_foreign_key_to_ci_pipeline_schedule_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..7bbf66e0ac392a4708ec0c29af89923d89491690 --- /dev/null +++ b/db/migrate/20170620065449_add_foreign_key_to_ci_pipeline_schedule_variables.rb @@ -0,0 +1,15 @@ +class AddForeignKeyToCiPipelineScheduleVariables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_foreign_key(:ci_pipeline_schedule_variables, :ci_pipeline_schedules, column: :pipeline_schedule_id) + end + + def down + remove_foreign_key(:ci_pipeline_schedule_variables, column: :pipeline_schedule_id) + end +end diff --git a/db/schema.rb b/db/schema.rb index 98034d638c788dfa91de557e57095ed3e8276634..784c7d175bbadc5153fbf4904fe76df259d0ae45 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -136,12 +136,12 @@ t.integer "cached_markdown_version" t.boolean "clientside_sentry_enabled", default: false, null: false t.string "clientside_sentry_dsn" - t.boolean "prometheus_metrics_enabled", default: false, null: false t.boolean "check_namespace_plan", default: false, null: false t.integer "mirror_max_delay", default: 5, null: false t.integer "mirror_max_capacity", default: 100, null: false t.integer "mirror_capacity_threshold", default: 50, null: false t.boolean "authorized_keys_enabled", default: true, null: false + t.boolean "prometheus_metrics_enabled", default: false, null: false t.boolean "help_page_hide_commercial_content", default: false t.string "help_page_support_url" end @@ -288,9 +288,9 @@ t.string "coverage_regex" t.integer "auto_canceled_by_id" t.boolean "retried" - t.integer "stage_id" t.integer "artifacts_file_store", default: 1, null: false t.integer "artifacts_metadata_store", default: 1, null: false + t.integer "stage_id" end add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree @@ -307,6 +307,19 @@ add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree + create_table "ci_pipeline_schedule_variables", force: :cascade do |t| + t.string "key", null: false + t.text "value" + t.text "encrypted_value" + t.string "encrypted_value_salt" + t.string "encrypted_value_iv" + t.integer "pipeline_schedule_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree + create_table "ci_pipeline_schedules", force: :cascade do |t| t.string "description" t.string "ref" @@ -615,8 +628,8 @@ t.text "deleted_path", null: false t.text "deleted_wiki_path" t.text "deleted_project_name", null: false - t.datetime_with_timezone "created_at", null: false - t.datetime_with_timezone "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "geo_repository_deleted_events", ["project_id"], name: "index_geo_repository_deleted_events_on_project_id", using: :btree @@ -1805,8 +1818,8 @@ t.string "token" t.boolean "pipeline_events", default: false, null: false t.boolean "confidential_issues_events", default: false, null: false - t.boolean "repository_update_events", default: false, null: false t.boolean "job_events", default: false, null: false + t.boolean "repository_update_events", default: false, null: false end add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree @@ -1817,6 +1830,7 @@ add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade + add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 328b2b70f2f2213d9bd4206b1881c1c745fb04fc..25360ac574a890d4a6c0e24cd57e0ff4b9998d0a 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -9,7 +9,7 @@ and a list of **user-defined variables**. The variables can be overwritten and they take precedence over each other in this order: -1. [Trigger variables][triggers] (take precedence over all) +1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all) 1. [Secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) 1. YAML-defined [job-level variables](../yaml/README.md#job-variables) 1. YAML-defined [global variables](../yaml/README.md#variables) diff --git a/doc/user/project/pipelines/img/pipeline_schedule_variables.png b/doc/user/project/pipelines/img/pipeline_schedule_variables.png new file mode 100644 index 0000000000000000000000000000000000000000..47a0c6f3697337ddf90e78a3ec834f6e736083f0 Binary files /dev/null and b/doc/user/project/pipelines/img/pipeline_schedule_variables.png differ diff --git a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png index ea5394fa8a685e582f0c649edb6cd9309f4e2845..5a0e596599234934ad272e0cba10771e7981f51e 100644 Binary files a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png and b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png differ diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 17cc21238ff3917cc8c534fa493b4468b154c168..258b3a2f9555f771d8746fc5001e5e97a9f1d174 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -31,6 +31,15 @@ is installed on. ![Schedules list](img/pipeline_schedules_list.png) +### Making use of scheduled pipeline variables + +> [Introduced][ce-12328] in GitLab 9.4. + +You can pass any number of arbitrary variables and they will be available in +GitLab CI so that they can be used in your `.gitlab-ci.yml` file. + +![Scheduled pipeline variables](img/pipeline_schedule_variables.png) + ## Using only and except To configure that a job can be executed only when the pipeline has been @@ -79,4 +88,5 @@ don't have admin access to the server, ask your administrator. [ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533 [ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853 +[ce-12328]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12328 [settings]: https://about.gitlab.com/gitlab-com/settings/#cron-jobs diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index afb8fb3176fdad86704b72c9a14814e9f974cd4c..7af4d9db9155c9896ed787d9bb2c184526b1c59d 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -17,6 +17,16 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "\n" +msgid "%d additional commit has been omitted to prevent performance issues." +msgid_plural "%d additional commits have been omitted to prevent performance issues." +msgstr[0] "" +msgstr[1] "" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "" +msgstr[1] "" + msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" @@ -61,9 +71,24 @@ msgstr[1] "" msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "" +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "" + msgid "Branches" msgstr "" +msgid "Browse Directory" +msgstr "" + +msgid "Browse File" +msgstr "" + +msgid "Browse Files" +msgstr "" + msgid "Browse files" msgstr "" @@ -171,6 +196,9 @@ msgstr "" msgid "Commits" msgstr "" +msgid "Commits feed" +msgstr "" + msgid "Commits|History" msgstr "" @@ -195,6 +223,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a personal access token on your account to pull or push via %{protocol}." +msgstr "" + msgid "Create directory" msgstr "" @@ -213,6 +244,9 @@ msgstr "" msgid "CreateTag|Tag" msgstr "" +msgid "CreateTokenToCloneLink|create a personal access token" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -323,6 +357,9 @@ msgstr "" msgid "Files" msgstr "" +msgid "Filter by commit message" +msgstr "" + msgid "Find by path" msgstr "" @@ -547,6 +584,12 @@ msgstr "" msgid "PipelineSchedules|Inactive" msgstr "" +msgid "PipelineSchedules|Input variable key" +msgstr "" + +msgid "PipelineSchedules|Input variable value" +msgstr "" + msgid "PipelineSchedules|Next Run" msgstr "" @@ -556,12 +599,18 @@ msgstr "" msgid "PipelineSchedules|Provide a short description for this pipeline" msgstr "" +msgid "PipelineSchedules|Remove variable row" +msgstr "" + msgid "PipelineSchedules|Take ownership" msgstr "" msgid "PipelineSchedules|Target" msgstr "" +msgid "PipelineSchedules|Variables" +msgstr "" + msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" @@ -688,7 +737,7 @@ msgstr "" msgid "Select target branch" msgstr "" -msgid "Set a password on your account to pull or push via %{protocol}" +msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" msgid "Set up CI" @@ -714,10 +763,7 @@ msgstr "" msgid "StarProject|Star" msgstr "" -msgid "Start a %{new_merge_request} with these changes" -msgstr "" - -msgid "Start a new merge request with these changes" +msgid "Start a %{new_merge_request} with these changes" msgstr "" msgid "Switch branch/tag" @@ -948,9 +994,15 @@ msgstr "" msgid "Upload file" msgstr "" +msgid "UploadLink|click to upload" +msgstr "" + msgid "Use your global notification setting" msgstr "" +msgid "View open merge request" +msgstr "" + msgid "VisibilityLevel|Internal" msgstr "" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 07f9efeb4957d7ec06385d8bfaaf2ecb40ca55b8..68bedf1e3482f604af26a33932a1e32c7474edb6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-19 15:50-0500\n" -"PO-Revision-Date: 2017-06-19 15:50-0500\n" +"POT-Creation-Date: 2017-07-05 14:24-0500\n" +"PO-Revision-Date: 2017-07-05 14:24-0500\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -msgid "%d additional commit have been omitted to prevent performance issues." +msgid "%d additional commit has been omitted to prevent performance issues." msgid_plural "%d additional commits have been omitted to prevent performance issues." msgstr[0] "" msgstr[1] "" @@ -224,6 +224,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a personal access token on your account to pull or push via %{protocol}." +msgstr "" + msgid "Create directory" msgstr "" @@ -242,6 +245,9 @@ msgstr "" msgid "CreateTag|Tag" msgstr "" +msgid "CreateTokenToCloneLink|create a personal access token" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -579,6 +585,12 @@ msgstr "" msgid "PipelineSchedules|Inactive" msgstr "" +msgid "PipelineSchedules|Input variable key" +msgstr "" + +msgid "PipelineSchedules|Input variable value" +msgstr "" + msgid "PipelineSchedules|Next Run" msgstr "" @@ -588,12 +600,18 @@ msgstr "" msgid "PipelineSchedules|Provide a short description for this pipeline" msgstr "" +msgid "PipelineSchedules|Remove variable row" +msgstr "" + msgid "PipelineSchedules|Take ownership" msgstr "" msgid "PipelineSchedules|Target" msgstr "" +msgid "PipelineSchedules|Variables" +msgstr "" + msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" @@ -720,7 +738,7 @@ msgstr "" msgid "Select target branch" msgstr "" -msgid "Set a password on your account to pull or push via %{protocol}" +msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" msgid "Set up CI" diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index a8c44d5c313ec4a678272a4d25e8ddab98e0e9ee..d28dd1e3e48bf53b61e4b611abf1a609b9872ae8 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::PipelineSchedulesController do + include AccessMatchersForController + set(:project) { create(:empty_project, :public) } let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } @@ -17,6 +19,14 @@ expect(response).to render_template(:index) end + it 'avoids N + 1 queries' do + control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count + + create_list(:ci_pipeline_schedule, 2, project: project) + + expect { visit_pipelines_schedules }.not_to exceed_query_limit(control_count) + end + context 'when the scope is set to active' do let(:scope) { 'active' } @@ -36,104 +46,322 @@ def visit_pipelines_schedules end end - describe 'GET edit' do - let(:user) { create(:user) } + describe 'GET #new' do + set(:user) { create(:user) } before do - project.add_master(user) - + project.add_developer(user) sign_in(user) end - it 'loads the pipeline schedule' do - get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + it 'initializes a pipeline schedule model' do + get :new, namespace_id: project.namespace.to_param, project_id: project expect(response).to have_http_status(:ok) - expect(assigns(:schedule)).to eq(pipeline_schedule) + expect(assigns(:schedule)).to be_a_new(Ci::PipelineSchedule) end end - describe 'DELETE #destroy' do - set(:user) { create(:user) } + describe 'POST #create' do + describe 'functionality' do + set(:user) { create(:user) } - context 'when a developer makes the request' do before do project.add_developer(user) sign_in(user) + end - delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + let(:basic_param) do + attributes_for(:ci_pipeline_schedule) end - it 'does not delete the pipeline schedule' do - expect(response).not_to have_http_status(:ok) + context 'when variables_attributes has one variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }] + }) + end + + it 'creates a new schedule' do + expect { go } + .to change { Ci::PipelineSchedule.count }.by(1) + .and change { Ci::PipelineScheduleVariable.count }.by(1) + + expect(response).to have_http_status(:found) + + Ci::PipelineScheduleVariable.last.tap do |v| + expect(v.key).to eq("AAA") + expect(v.value).to eq("AAA123") + end + end + end + + context 'when variables_attributes has two variables and duplicted' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }] + }) + end + + it 'returns an error that the keys of variable are duplicated' do + expect { go } + .to change { Ci::PipelineSchedule.count }.by(0) + .and change { Ci::PipelineScheduleVariable.count }.by(0) + + expect(assigns(:schedule).errors['variables']).not_to be_empty + end end end - context 'when a master makes the request' do + describe 'security' do + let(:schedule) { attributes_for(:ci_pipeline_schedule) } + + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + it { expect { go }.to be_denied_for(:visitor) } + end + + def go + post :create, namespace_id: project.namespace.to_param, project_id: project, schedule: schedule + end + end + + describe 'PUT #update' do + describe 'functionality' do + set(:user) { create(:user) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + before do - project.add_master(user) + project.add_developer(user) sign_in(user) end - it 'destroys the pipeline schedule' do - expect do - delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id - end.to change { project.pipeline_schedules.count }.by(-1) + context 'when a pipeline schedule has no variables' do + let(:basic_param) do + { description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'patch-x', active: true } + end - expect(response).to have_http_status(302) + context 'when params include one variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }] + }) + end + + it 'inserts new variable to the pipeline schedule' do + expect { go }.to change { Ci::PipelineScheduleVariable.count }.by(1) + + pipeline_schedule.reload + expect(response).to have_http_status(:found) + expect(pipeline_schedule.variables.last.key).to eq('AAA') + expect(pipeline_schedule.variables.last.value).to eq('AAA123') + end + end + + context 'when params include two duplicated variables' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }] + }) + end + + it 'returns an error that variables are duplciated' do + go + + expect(assigns(:schedule).errors['variables']).not_to be_empty + end + end + end + + context 'when a pipeline schedule has one variable' do + let(:basic_param) do + { description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'patch-x', active: true } + end + + let!(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, + key: 'CCC', pipeline_schedule: pipeline_schedule) + end + + context 'when params include one variable' do + context 'when adds a new variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ key: 'AAA', value: 'AAA123' }] + }) + end + + it 'adds the new variable' do + expect { go }.to change { Ci::PipelineScheduleVariable.count }.by(1) + + pipeline_schedule.reload + expect(pipeline_schedule.variables.last.key).to eq('AAA') + end + end + + context 'when updates a variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ id: pipeline_schedule_variable.id, value: 'new_value' }] + }) + end + + it 'updates the variable' do + expect { go }.not_to change { Ci::PipelineScheduleVariable.count } + + pipeline_schedule_variable.reload + expect(pipeline_schedule_variable.value).to eq('new_value') + end + end + + context 'when deletes a variable' do + let(:schedule) do + basic_param.merge({ + variables_attributes: [{ id: pipeline_schedule_variable.id, _destroy: true }] + }) + end + + it 'delete the existsed variable' do + expect { go }.to change { Ci::PipelineScheduleVariable.count }.by(-1) + end + end + end end end - end - describe 'security' do - include AccessMatchersForController + describe 'security' do + let(:schedule) { { description: 'updated_desc' } } - describe 'GET edit' do it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) } it { expect { go }.to be_denied_for(:reporter).of(project) } it { expect { go }.to be_denied_for(:guest).of(project) } it { expect { go }.to be_denied_for(:user) } it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:visitor) } - def go + context 'when a developer created a pipeline schedule' do + let(:developer_1) { create(:user) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer_1) } + + before do + project.add_developer(developer_1) + end + + it { expect { go }.to be_allowed_for(developer_1) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:master).of(project) } + end + + context 'when a master created a pipeline schedule' do + let(:master_1) { create(:user) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master_1) } + + before do + project.add_master(master_1) + end + + it { expect { go }.to be_allowed_for(master_1) } + it { expect { go }.to be_allowed_for(:master).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + end + end + + def go + put :update, namespace_id: project.namespace.to_param, + project_id: project, id: pipeline_schedule, + schedule: schedule + end + end + + describe 'GET #edit' do + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + it 'loads the pipeline schedule' do get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + + expect(response).to have_http_status(:ok) + expect(assigns(:schedule)).to eq(pipeline_schedule) end end - describe 'GET take_ownership' do + describe 'security' do it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) } it { expect { go }.to be_denied_for(:reporter).of(project) } it { expect { go }.to be_denied_for(:guest).of(project) } it { expect { go }.to be_denied_for(:user) } it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:visitor) } + end - def go - post :take_ownership, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id - end + def go + get :edit, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id end + end - describe 'PUT update' do + describe 'GET #take_ownership' do + describe 'security' do it { expect { go }.to be_allowed_for(:admin) } it { expect { go }.to be_allowed_for(:owner).of(project) } it { expect { go }.to be_allowed_for(:master).of(project) } - it { expect { go }.to be_allowed_for(:developer).of(project) } + it { expect { go }.to be_allowed_for(:developer).of(project).own(pipeline_schedule) } it { expect { go }.to be_denied_for(:reporter).of(project) } it { expect { go }.to be_denied_for(:guest).of(project) } it { expect { go }.to be_denied_for(:user) } it { expect { go }.to be_denied_for(:external) } it { expect { go }.to be_denied_for(:visitor) } + end - def go - put :update, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id, - schedule: { description: 'a' } + def go + post :take_ownership, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + end + + describe 'DELETE #destroy' do + set(:user) { create(:user) } + + context 'when a developer makes the request' do + before do + project.add_developer(user) + sign_in(user) + + delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end + + it 'does not delete the pipeline schedule' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when a master makes the request' do + before do + project.add_master(user) + sign_in(user) + end + + it 'destroys the pipeline schedule' do + expect do + delete :destroy, namespace_id: project.namespace.to_param, project_id: project, id: pipeline_schedule.id + end.to change { project.pipeline_schedules.count }.by(-1) + + expect(response).to have_http_status(302) end end end diff --git a/spec/factories/ci/pipeline_schedule_variables.rb b/spec/factories/ci/pipeline_schedule_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..ca64d1aada0bcd7ef388d4c56f913f3c4738a36b --- /dev/null +++ b/spec/factories/ci/pipeline_schedule_variables.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :ci_pipeline_schedule_variable, class: Ci::PipelineScheduleVariable do + sequence(:key) { |n| "VARIABLE_#{n}" } + value 'VARIABLE_VALUE' + + pipeline_schedule factory: :ci_pipeline_schedule + end +end diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index dfb973c37e557519e405d96ae12a555df3f51d7a..7ab6b3f0b754eca78045b204b80ecb548538954f 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Pipeline Schedules', :feature do +feature 'Pipeline Schedules', :feature, js: true do include PipelineSchedulesHelper let!(:project) { create(:project) } @@ -11,27 +11,20 @@ before do project.add_master(user) - gitlab_sign_in(user) - visit_page end describe 'GET /projects/pipeline_schedules' do - let(:visit_page) { visit_pipelines_schedules } - - it 'avoids N + 1 queries' do - control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count - - create_list(:ci_pipeline_schedule, 2, project: project) - - expect { visit_pipelines_schedules }.not_to exceed_query_limit(control_count) + before do + visit_pipelines_schedules end describe 'The view' do it 'displays the required information description' do page.within('.pipeline-schedule-table-row') do expect(page).to have_content('pipeline schedule') - expect(page).to have_content(pipeline_schedule.real_next_run.strftime('%b %d, %Y')) + expect(find(".next-run-cell time")['data-original-title']) + .to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y')) expect(page).to have_link('master') expect(page).to have_link("##{pipeline.id}") end @@ -62,7 +55,7 @@ it 'deletes the pipeline' do click_link 'Delete' - expect(page).not_to have_content('pipeline schedule') + expect(page).not_to have_css(".pipeline-schedule-table-row") end end @@ -78,8 +71,10 @@ end end - describe 'POST /projects/pipeline_schedules/new', js: true do - let(:visit_page) { visit_new_pipeline_schedule } + describe 'POST /projects/pipeline_schedules/new' do + before do + visit_new_pipeline_schedule + end it 'sets defaults for timezone and target branch' do expect(page).to have_button('master') @@ -100,8 +95,8 @@ end end - describe 'PATCH /projects/pipelines_schedules/:id/edit', js: true do - let(:visit_page) do + describe 'PATCH /projects/pipelines_schedules/:id/edit' do + before do edit_pipeline_schedule end @@ -134,6 +129,72 @@ end end + context 'when user creates a new pipeline schedule with variables' do + background do + visit_pipelines_schedules + click_link 'New schedule' + fill_in_schedule_form + all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA') + all('[name="schedule[variables_attributes][][value]"]')[0].set('AAA123') + all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB') + all('[name="schedule[variables_attributes][][value]"]')[1].set('BBB123') + save_pipeline_schedule + end + + scenario 'user sees the new variable in edit window' do + find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click + page.within('.pipeline-variable-list') do + expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('AAA') + expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('AAA123') + expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-key-input").value).to eq('BBB') + expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-value-input").value).to eq('BBB123') + end + end + end + + context 'when user edits a variable of a pipeline schedule' do + background do + create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule| + create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule) + end + + visit_pipelines_schedules + find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click + all('[name="schedule[variables_attributes][][key]"]')[0].set('foo') + all('[name="schedule[variables_attributes][][value]"]')[0].set('bar') + click_button 'Save pipeline schedule' + end + + scenario 'user sees the updated variable in edit window' do + find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click + page.within('.pipeline-variable-list') do + expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('foo') + expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('bar') + end + end + end + + context 'when user removes a variable of a pipeline schedule' do + background do + create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule| + create(:ci_pipeline_schedule_variable, key: 'AAA', value: 'AAA123', pipeline_schedule: pipeline_schedule) + end + + visit_pipelines_schedules + find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click + find('.pipeline-variable-list .pipeline-variable-row-remove-button').click + click_button 'Save pipeline schedule' + end + + scenario 'user does not see the removed variable in edit window' do + find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click + page.within('.pipeline-variable-list') do + expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('') + expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('') + end + end + end + def visit_new_pipeline_schedule visit new_namespace_project_pipeline_schedule_path(project.namespace, project, pipeline_schedule) end diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..5b316b319a56155b15c16bc522c8852fbca2b458 --- /dev/null +++ b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js @@ -0,0 +1,145 @@ +import { + setupPipelineVariableList, + insertRow, + removeRow, +} from '~/pipeline_schedules/setup_pipeline_variable_list'; + +describe('Pipeline Variable List', () => { + let $markup; + + describe('insertRow', () => { + it('should insert another row', () => { + $markup = $(`
+
  • + + +
  • +
    `); + + insertRow($markup.find('.js-row')); + + expect($markup.find('.js-row').length).toBe(2); + }); + + it('should clear `data-is-persisted` on cloned row', () => { + $markup = $(`
    +
  • +
    `); + + insertRow($markup.find('.js-row')); + + const $lastRow = $markup.find('.js-row').last(); + expect($lastRow.attr('data-is-persisted')).toBe(undefined); + }); + + it('should clear inputs on cloned row', () => { + $markup = $(`
    +
  • + + +
  • +
    `); + + insertRow($markup.find('.js-row')); + + const $lastRow = $markup.find('.js-row').last(); + expect($lastRow.find('input').val()).toBe(''); + expect($lastRow.find('textarea').val()).toBe(''); + }); + }); + + describe('removeRow', () => { + it('should remove dynamic row', () => { + $markup = $(`
    +
  • + + +
  • +
    `); + + removeRow($markup.find('.js-row')); + + expect($markup.find('.js-row').length).toBe(0); + }); + + it('should hide and mark to destroy with already persisted rows', () => { + $markup = $(`
    +
  • + +
  • +
    `); + + const $row = $markup.find('.js-row'); + removeRow($row); + + expect($row.find('.js-destroy-input').val()).toBe('1'); + expect($markup.find('.js-row').length).toBe(1); + }); + }); + + describe('setupPipelineVariableList', () => { + beforeEach(() => { + $markup = $(`
    +
  • + + + + +
  • +
    `); + + setupPipelineVariableList($markup); + }); + + it('should remove the row when clicking the remove button', () => { + $markup.find('.js-row-remove-button').trigger('click'); + + expect($markup.find('.js-row').length).toBe(0); + }); + + it('should add another row when editing the last rows key input', () => { + const $row = $markup.find('.js-row'); + $row.find('input.js-user-input') + .val('foo') + .trigger('input'); + + expect($markup.find('.js-row').length).toBe(2); + }); + + it('should add another row when editing the last rows value textarea', () => { + const $row = $markup.find('.js-row'); + $row.find('textarea.js-user-input') + .val('foo') + .trigger('input'); + + expect($markup.find('.js-row').length).toBe(2); + }); + + it('should remove empty row after blurring', () => { + const $row = $markup.find('.js-row'); + $row.find('input.js-user-input') + .val('foo') + .trigger('input'); + + expect($markup.find('.js-row').length).toBe(2); + + $row.find('input.js-user-input') + .val('') + .trigger('input') + .trigger('blur'); + + expect($markup.find('.js-row').length).toBe(1); + }); + + it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => { + const $row = $markup.find('.js-row'); + expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]'); + expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]'); + + $markup.filter('form').submit(); + + expect($row.find('input').attr('name')).toBe(''); + expect($row.find('textarea').attr('name')).toBe(''); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index bc705b0c8388910fe80e5c3dce20351cd28c122e..fa7bc1987ca4054edfb0cebdee8194d1556801aa 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -138,8 +138,11 @@ pipeline_schedules: - owner - pipelines - last_pipeline +- variables pipeline_schedule: - pipelines +pipeline_schedule_variables: +- pipeline_schedule deploy_keys: - user - deploy_keys_projects diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 22d2ed10ac6767f23bfe5cf83b27a8b8e18ad162..40abf4854606290f0a980e1a129409c43908a109 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1414,6 +1414,23 @@ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) it { is_expected.to include(predefined_trigger_variable) } end + context 'when a job was triggered by a pipeline schedule' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + + let!(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, + key: 'SCHEDULE_VARIABLE_KEY', + pipeline_schedule: pipeline_schedule) + end + + before do + pipeline_schedule.pipelines << pipeline + pipeline_schedule.reload + end + + it { is_expected.to include(pipeline_schedule_variable.to_runner_variable) } + end + context 'when yaml_variables are undefined' do before do build.yaml_variables = nil diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 56817baf79d65d679a0da8b1e9c729e2c6e0f7b5..4c3aa986bf91af9b385c92584a37134415418d84 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -5,6 +5,7 @@ it { is_expected.to belong_to(:owner) } it { is_expected.to have_many(:pipelines) } + it { is_expected.to have_many(:variables) } it { is_expected.to respond_to(:ref) } it { is_expected.to respond_to(:cron) } @@ -117,4 +118,20 @@ end end end + + describe '#job_variables' do + let!(:pipeline_schedule) { create(:ci_pipeline_schedule) } + + let!(:pipeline_schedule_variables) do + create_list(:ci_pipeline_schedule_variable, 2, pipeline_schedule: pipeline_schedule) + end + + subject { pipeline_schedule.job_variables } + + before do + pipeline_schedule.reload + end + + it { is_expected.to eq(pipeline_schedule_variables.map(&:to_runner_variable)) } + end end diff --git a/spec/models/ci/pipeline_schedule_variable_spec.rb b/spec/models/ci/pipeline_schedule_variable_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9c0b0153e038dc5924c739dceb807f05890e7b62 --- /dev/null +++ b/spec/models/ci/pipeline_schedule_variable_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +describe Ci::PipelineScheduleVariable, models: true do + subject { build(:ci_pipeline_schedule_variable) } + + it { is_expected.to include_module(HasVariable) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:pipeline_schedule_id) } +end diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb index fb43f51c70c48b84fce249add51fa75cefc1f1e5..ff60bd0c0aeae05c57660adc023c5dc284fb5ba3 100644 --- a/spec/support/matchers/access_matchers_for_controller.rb +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -50,9 +50,24 @@ def description_for(role, type, expected, result) "be #{type} for #{role}. Expected: #{expected.join(',')} Got: #{result}" end + def update_owner(objects, user) + return unless objects + + objects.each do |object| + if object.respond_to?(:owner) + object.update_attribute(:owner, user) + elsif object.respond_to?(:user) + object.update_attribute(:user, user) + else + raise ArgumentError, "cannot own this object #{object}" + end + end + end + matcher :be_allowed_for do |role| match do |action| - emulate_user(role, @membership) + user = emulate_user(role, @membership) + update_owner(@objects, user) action.call EXPECTED_STATUS_CODE_ALLOWED.include?(response.status) @@ -62,13 +77,18 @@ def description_for(role, type, expected, result) @membership = membership end + chain :own do |*objects| + @objects = objects + end + description { description_for(role, 'allowed', EXPECTED_STATUS_CODE_ALLOWED, response.status) } supports_block_expectations end matcher :be_denied_for do |role| match do |action| - emulate_user(role, @membership) + user = emulate_user(role, @membership) + update_owner(@objects, user) action.call EXPECTED_STATUS_CODE_DENIED.include?(response.status) @@ -78,6 +98,10 @@ def description_for(role, type, expected, result) @membership = membership end + chain :own do |*objects| + @objects = objects + end + description { description_for(role, 'denied', EXPECTED_STATUS_CODE_DENIED, response.status) } supports_block_expectations end