-
+
+
+
{{ title }}
-
{{ content }}
-
+
{{ content }}
+
+
+
{{ action.button_title }}
- {{ action.button_title }}
-
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 79fb67d38cd92d90e0ecd523ee6535176d15e10a..ef9fb6d08d136662ae4b753e2dd63fec4d46e2f9 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -45,6 +45,11 @@ export default {
required: false,
default: null,
},
+ variablesSettingsUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
runnerHelpUrl: {
type: String,
required: false,
@@ -313,6 +318,9 @@ export default {
:title="emptyStateTitle"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
+ :playable="job.playable"
+ :scheduled="job.scheduled"
+ :variables-settings-url="variablesSettingsUrl"
/>
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c32a3cac7be33727af6f498c8edcaf4dc32de456
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -0,0 +1,179 @@
+
+
+
+
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 25132449458b5dd7551772e52531c34cb049d7b8..06514fcce1dee04f763fe5187f9484e3119c20f9 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -15,6 +15,7 @@ export default () => {
deploymentHelpUrl: element.dataset.deploymentHelpUrl,
runnerHelpUrl: element.dataset.runnerHelpUrl,
runnerSettingsUrl: element.dataset.runnerSettingsUrl,
+ variablesSettingsUrl: element.dataset.variablesSettingsUrl,
endpoint: element.dataset.endpoint,
pagePath: element.dataset.buildOptionsPagePath,
logState: element.dataset.buildOptionsLogState,
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 12d67a43599962ddb480503944ef2a80c9e3bb79..a2daef96a2d186523d792534299223c48d90289c 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -209,5 +209,19 @@ export const receiveJobsForStageError = ({ commit }) => {
flash(__('An error occurred while fetching the jobs.'));
};
+export const triggerManualJob = ({ state }, variables) => {
+ const parsedVariables = variables.map(variable => {
+ const copyVar = Object.assign({}, variable);
+ delete copyVar.id;
+ return copyVar;
+ });
+
+ axios
+ .post(state.job.status.action.path, {
+ job_variables_attributes: parsedVariables,
+ })
+ .catch(() => flash(__('An error occurred while triggering the job.')));
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 02ff6e872c9dd1d43c190bb71879fcc9892ec827..adbc01593582e853ef89ee8643d74f0ef2cf7f0b 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -94,7 +94,7 @@ class Projects::JobsController < Projects::ApplicationController
def play
return respond_422 unless @build.playable?
- build = @build.play(current_user)
+ build = @build.play(current_user, play_params[:job_variables_attributes])
redirect_to build_path(build)
end
@@ -190,6 +190,10 @@ class Projects::JobsController < Projects::ApplicationController
{ query: { 'response-content-type' => 'text/plain; charset=utf-8', 'response-content-disposition' => 'inline' } }
end
+ def play_params
+ params.permit(job_variables_attributes: %i[key secret_value])
+ end
+
def trace_artifact_file
@trace_artifact_file ||= build.job_artifacts_trace&.file
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index da70cb9a9a750306e9786cef78889c35dbac267b..07813e03f3a84bdf5dcbf886087274f4cc4cec99 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -40,6 +40,7 @@ module Ci
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
+ has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
Ci::JobArtifact.file_types.each do |key, value|
has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
@@ -48,6 +49,7 @@ module Ci
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
accepts_nested_attributes_for :runner_session
+ accepts_nested_attributes_for :job_variables
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
@@ -331,10 +333,10 @@ module Ci
end
# rubocop: disable CodeReuse/ServiceClass
- def play(current_user)
+ def play(current_user, job_variables_attributes = nil)
Ci::PlayBuildService
.new(project, current_user)
- .execute(self)
+ .execute(self, job_variables_attributes)
end
# rubocop: enable CodeReuse/ServiceClass
@@ -432,6 +434,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new
.concat(persisted_variables)
.concat(scoped_variables)
+ .concat(job_variables)
.concat(persisted_environment_variables)
.to_runner_variables
end
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..862a0bc1299653df44ca0effdd49cb76cccb5ef9
--- /dev/null
+++ b/app/models/ci/job_variable.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ci
+ class JobVariable < ApplicationRecord
+ extend Gitlab::Ci::Model
+ include NewHasVariable
+
+ belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
+
+ alias_attribute :secret_value, :value
+
+ validates :key, uniqueness: { scope: :job_id }
+ end
+end
diff --git a/app/models/concerns/new_has_variable.rb b/app/models/concerns/new_has_variable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..429bf496872d82ba5cc1dd365f6aaff269ff17b5
--- /dev/null
+++ b/app/models/concerns/new_has_variable.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module NewHasVariable
+ extend ActiveSupport::Concern
+ include HasVariable
+
+ included do
+ attr_encrypted :value,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_32,
+ insecure_mode: false
+ end
+end
diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb
index eb0b070657d578e404efdbc112ed9986dcb33c80..9f922ffde815b1b3f4138ea1075614478f95371e 100644
--- a/app/services/ci/play_build_service.rb
+++ b/app/services/ci/play_build_service.rb
@@ -2,7 +2,7 @@
module Ci
class PlayBuildService < ::BaseService
- def execute(build)
+ def execute(build, job_variables_attributes = nil)
unless can?(current_user, :update_build, build)
raise Gitlab::Access::AccessDeniedError
end
@@ -10,7 +10,7 @@ module Ci
# Try to enqueue the build, otherwise create a duplicate.
#
if build.enqueue
- build.tap { |action| action.update(user: current_user) }
+ build.tap { |action| action.update(user: current_user, job_variables_attributes: job_variables_attributes || []) }
else
Ci::Build.retry(build, current_user)
end
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 81a53f22f67b3df844cc56cbcd679f187a1bf98c..c7fab87a593b9c6bf14b51fd74a16bbaca61f8eb 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -11,4 +11,5 @@
deployment_help_url: help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'),
runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'),
runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'),
+ variables_settings_url: project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'),
build_options: javascript_build_options } }
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 293f0a241ebf48926eaa3c808052fdf7b8802873..87000e8270b6cc10d2c543bbc98e72159700d35a 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -41,7 +41,7 @@
.settings-content
= render 'projects/runners/index'
-%section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'variables_settings_content' } }
+%section.qa-variables-settings.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { qa_selector: 'variables_settings_content' } }
.settings-header
= render 'ci/variables/header', expanded: expanded
.settings-content
diff --git a/changelogs/unreleased/mc-feature-manual-job-variables.yml b/changelogs/unreleased/mc-feature-manual-job-variables.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a71cabfe303c76ac44aa2bf1a76e667d4de4588a
--- /dev/null
+++ b/changelogs/unreleased/mc-feature-manual-job-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Allow specifying variables when running manual jobs
+merge_request: 30485
+author:
+type: added
diff --git a/db/migrate/20190711124721_create_job_variables.rb b/db/migrate/20190711124721_create_job_variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a860522f39ef5182d4192506ac7daa525b1ffd12
--- /dev/null
+++ b/db/migrate/20190711124721_create_job_variables.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateJobVariables < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :ci_job_variables do |t|
+ t.string :key, null: false
+ t.text :encrypted_value
+ t.string :encrypted_value_iv
+ t.references :job, null: false, index: true, foreign_key: { to_table: :ci_builds, on_delete: :cascade }
+ t.integer :variable_type, null: false, limit: 2, default: 1
+ end
+
+ add_index :ci_job_variables, [:key, :job_id], unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 67479937b47cca6abda2d095e879cb23c1beb269..1b5272179f5546bd98d8018dfe6c23dd57ac52ec 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -605,6 +605,16 @@ ActiveRecord::Schema.define(version: 2019_07_25_012225) do
t.index ["project_id"], name: "index_ci_job_artifacts_on_project_id"
end
+ create_table "ci_job_variables", force: :cascade do |t|
+ t.string "key", null: false
+ t.text "encrypted_value"
+ t.string "encrypted_value_iv"
+ t.bigint "job_id", null: false
+ t.integer "variable_type", limit: 2, default: 1, null: false
+ t.index ["job_id"], name: "index_ci_job_variables_on_job_id"
+ t.index ["key", "job_id"], name: "index_ci_job_variables_on_key_and_job_id", unique: true
+ end
+
create_table "ci_pipeline_chat_data", force: :cascade do |t|
t.integer "pipeline_id", null: false
t.integer "chat_name_id", null: false
@@ -3637,6 +3647,7 @@ ActiveRecord::Schema.define(version: 2019_07_25_012225) do
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
+ add_foreign_key "ci_job_variables", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_pipeline_chat_data", "chat_names", on_delete: :cascade
add_foreign_key "ci_pipeline_chat_data", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade
diff --git a/doc/ci/img/manual_job_variables.png b/doc/ci/img/manual_job_variables.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7d62477cdda4e97f47e80ece7c766c6fa128320
Binary files /dev/null and b/doc/ci/img/manual_job_variables.png differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index be8f66c741f35b1964c3addb1b0a9985ff6706fa..ed8d0e3bc351254750f6b569d99df0d95fc4ea67 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -323,6 +323,20 @@ stage has a job with a manual action.
![Pipelines example](img/pipelines.png)
+### Specifying variables when running manual jobs
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/30485) in GitLab 12.2.
+
+When running manual jobs you can supply additional job specific variables.
+
+You can do this from the job page of the manual job you want to run with
+additional variables.
+
+This is useful when you want to alter the execution of a job by using
+environment variables.
+
+![Manual job variables](img/manual_job_variables.png)
+
### Delay a job in a pipeline graph
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4.
diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb
index d01b09f1398c1764ee14d90afa0a6c384bd56995..df572188194bdee06d94ea7dabc7dd6d3845bed1 100644
--- a/lib/gitlab/ci/status/build/manual.rb
+++ b/lib/gitlab/ci/status/build/manual.rb
@@ -10,7 +10,7 @@ module Gitlab
image: 'illustrations/manual_action.svg',
size: 'svg-394',
title: _('This job requires a manual action'),
- content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
+ content: _('This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.')
}
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 58c75ee23195af61e435c9435db5e5d7543b25aa..5397404b6301014071f2acaa9984ff1f671e72e8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1080,6 +1080,9 @@ msgstr ""
msgid "An error occurred while saving assignees"
msgstr ""
+msgid "An error occurred while triggering the job."
+msgstr ""
+
msgid "An error occurred while validating username"
msgstr ""
@@ -2221,6 +2224,9 @@ msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
+msgid "CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default"
+msgstr ""
+
msgid "CiVariables|State"
msgstr ""
@@ -2230,6 +2236,9 @@ msgstr ""
msgid "CiVariables|Value"
msgstr ""
+msgid "CiVariables|Variables"
+msgstr ""
+
msgid "CiVariable|* (All environments)"
msgstr ""
@@ -7697,6 +7706,9 @@ msgstr ""
msgid "Pipeline|Existing branch name or tag"
msgstr ""
+msgid "Pipeline|Key"
+msgstr ""
+
msgid "Pipeline|Pipeline"
msgstr ""
@@ -7727,6 +7739,9 @@ msgstr ""
msgid "Pipeline|Triggerer"
msgstr ""
+msgid "Pipeline|Value"
+msgstr ""
+
msgid "Pipeline|Variables"
msgstr ""
@@ -11056,9 +11071,6 @@ msgstr ""
msgid "This issue is locked."
msgstr ""
-msgid "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
-msgstr ""
-
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
@@ -11113,6 +11125,9 @@ msgstr ""
msgid "This job requires a manual action"
msgstr ""
+msgid "This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes."
+msgstr ""
+
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
msgstr ""
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index e428aa3c7b7bf6dd36ce8e975625e09d161f16e2..39ebf02dcf5da24fb8d739d2a08723fed7513794 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -676,6 +676,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
describe 'POST play' do
+ let(:variable_attributes) { [] }
+
before do
project.add_developer(user)
@@ -698,6 +700,14 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
it 'transits to pending' do
expect(job.reload).to be_pending
end
+
+ context 'when job variables are specified' do
+ let(:variable_attributes) { [{ key: 'first', secret_value: 'first' }] }
+
+ it 'assigns the job variables' do
+ expect(job.reload.job_variables.map(&:key)).to contain_exactly('first')
+ end
+ end
end
context 'when job is not playable' do
@@ -712,7 +722,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
post :play, params: {
namespace_id: project.namespace,
project_id: project,
- id: job.id
+ id: job.id,
+ job_variables_attributes: variable_attributes
}
end
end
diff --git a/spec/factories/ci/job_variables.rb b/spec/factories/ci/job_variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d664b763abd9cce0b57861c9061aa24dac8c98ea
--- /dev/null
+++ b/spec/factories/ci/job_variables.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_job_variable, class: Ci::JobVariable do
+ sequence(:key) { |n| "VARIABLE_#{n}" }
+ value 'VARIABLE_VALUE'
+
+ job factory: :ci_build
+ end
+end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index f4ed89adc0f0fa335a4a6f0c900ab703fa1fee03..8ed420300afc60a4f41e537dfa9ea9f810ef3ccf 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -701,12 +701,12 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'shows manual action empty state', :js do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).to have_content('This job requires a manual action')
- expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
- expect(page).to have_link('Trigger this manual action')
+ expect(page).to have_content('This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.')
+ expect(page).to have_button('Trigger this manual action')
end
it 'plays manual action and shows pending status', :js do
- click_link 'Trigger this manual action'
+ click_button 'Trigger this manual action'
wait_for_requests
expect(page).to have_content('This job has not started yet')
@@ -734,8 +734,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
wait_for_requests
expect(page).to have_content('This job requires a manual action')
- expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
- expect(page).to have_link('Trigger this manual action')
+ expect(page).to have_content('This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.')
+ expect(page).to have_button('Trigger this manual action')
end
end
diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js
index a2df79bdda0a535361777fffbda80c0c695b1a66..dfba5a936eeb7cd5e2f76c172ceb93b2f920eb55 100644
--- a/spec/frontend/jobs/components/empty_state_spec.js
+++ b/spec/frontend/jobs/components/empty_state_spec.js
@@ -10,6 +10,8 @@ describe('Empty State', () => {
illustrationPath: 'illustrations/pending_job_empty.svg',
illustrationSizeClass: 'svg-430',
title: 'This job has not started yet',
+ playable: false,
+ variablesSettingsUrl: '',
};
const content = 'This job is in pending state and is waiting to be picked by a runner';
@@ -90,4 +92,44 @@ describe('Empty State', () => {
expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull();
});
});
+
+ describe('without playbale action', () => {
+ it('does not render manual variables form', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ content,
+ });
+
+ expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull();
+ });
+ });
+
+ describe('with playbale action and not scheduled job', () => {
+ it('renders manual variables form', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ content,
+ playable: true,
+ scheduled: false,
+ action: {
+ path: 'runner',
+ button_title: 'Check runner',
+ method: 'post',
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-manual-vars-form')).not.toBeNull();
+ });
+ });
+
+ describe('with playbale action and scheduled job', () => {
+ it('does not render manual variables form', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ content,
+ });
+
+ expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index f28d2c2a882cd4301a60b1a253125f6088ffd416..c58d59b4b16eb99079040e4e53b7b3728f2cff91 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -19,6 +19,7 @@ describe('Job App ', () => {
runnerHelpUrl: 'help/runner',
deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners',
+ variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal',
pagePath: `${gl.TEST_HOST}jobs/123`,
logState:
diff --git a/spec/javascripts/jobs/components/manual_variables_form_spec.js b/spec/javascripts/jobs/components/manual_variables_form_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..093aa9051857017c3ad8e463894f3f86247a843c
--- /dev/null
+++ b/spec/javascripts/jobs/components/manual_variables_form_spec.js
@@ -0,0 +1,88 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import Form from '~/jobs/components/manual_variables_form.vue';
+
+describe('Manual Variables Form', () => {
+ let wrapper;
+ const requiredProps = {
+ action: {
+ path: '/play',
+ method: 'post',
+ button_title: 'Trigger this manual action',
+ },
+ variablesSettingsUrl: '/settings',
+ };
+
+ const factory = (props = {}) => {
+ wrapper = shallowMount(Form, {
+ propsData: props,
+ });
+ };
+
+ beforeEach(() => {
+ factory(requiredProps);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders empty form with correct placeholders', () => {
+ expect(wrapper.find({ ref: 'inputKey' }).attributes('placeholder')).toBe('Input variable key');
+ expect(wrapper.find({ ref: 'inputSecretValue' }).attributes('placeholder')).toBe(
+ 'Input variable value',
+ );
+ });
+
+ it('renders help text with provided link', () => {
+ expect(wrapper.find('p').text()).toBe(
+ 'Specify variable values to be used in this run. The values specified in CI/CD settings will be used as default',
+ );
+
+ expect(wrapper.find('a').attributes('href')).toBe(requiredProps.variablesSettingsUrl);
+ });
+
+ describe('when adding a new variable', () => {
+ it('creates a new variable when user types a new key and resets the form', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => wrapper.find({ ref: 'inputKey' }).setValue('new key'))
+ .then(() => {
+ expect(wrapper.vm.variables.length).toBe(1);
+ expect(wrapper.vm.variables[0].key).toBe('new key');
+ expect(wrapper.find({ ref: 'inputKey' }).attributes('value')).toBe(undefined);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('creates a new variable when user types a new value and resets the form', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => wrapper.find({ ref: 'inputSecretValue' }).setValue('new value'))
+ .then(() => {
+ expect(wrapper.vm.variables.length).toBe(1);
+ expect(wrapper.vm.variables[0].secret_value).toBe('new value');
+ expect(wrapper.find({ ref: 'inputSecretValue' }).attributes('value')).toBe(undefined);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when deleting a variable', () => {
+ it('removes the variable row', () => {
+ wrapper.vm.variables = [
+ {
+ key: 'new key',
+ secret_value: 'value',
+ id: '1',
+ },
+ ];
+
+ wrapper.find(GlButton).vm.$emit('click');
+
+ expect(wrapper.vm.variables.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index c30cb70e1c191125ab05154e0ebef4dd5af57720..17c7c05324a326e7b14829ee01e2666cd4106d05 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -21,7 +21,8 @@ describe Ci::Build do
it { is_expected.to belong_to(:erased_by) }
it { is_expected.to have_many(:trace_sections)}
it { is_expected.to have_one(:deployment) }
- it { is_expected.to have_one(:runner_session)}
+ it { is_expected.to have_one(:runner_session) }
+ it { is_expected.to have_many(:job_variables) }
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
@@ -2258,6 +2259,16 @@ describe Ci::Build do
it { is_expected.to include(manual_variable) }
end
+ context 'when job variable is defined' do
+ let(:job_variable) { { key: 'first', value: 'first', public: false, masked: false } }
+
+ before do
+ create(:ci_job_variable, job_variable.slice(:key, :value).merge(job: build))
+ end
+
+ it { is_expected.to include(job_variable) }
+ end
+
context 'when build is for tag' do
let(:tag_variable) do
{ key: 'CI_COMMIT_TAG', value: 'master', public: true, masked: false }
diff --git a/spec/models/ci/job_variable_spec.rb b/spec/models/ci/job_variable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b94a914c78435d0372204af9058b9a3cdb240fde
--- /dev/null
+++ b/spec/models/ci/job_variable_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::JobVariable do
+ subject { build(:ci_job_variable) }
+
+ it_behaves_like "CI variable"
+
+ it { is_expected.to belong_to(:job) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:job_id) }
+end
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index 1e68b7956eaf1e0c61b74ad45835dd0cfa939841..cf39f3da4fe22f84160eb8db99e780fcd31c4d4e 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -60,6 +60,19 @@ describe Ci::PlayBuildService, '#execute' do
expect(build.reload.user).to eq user
end
+
+ context 'when variables are supplied' do
+ let(:job_variables) do
+ [{ key: 'first', secret_value: 'first' },
+ { key: 'second', secret_value: 'second' }]
+ end
+
+ it 'assigns the variables to the build' do
+ service.execute(build, job_variables)
+
+ expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
+ end
+ end
end
context 'when build is not a playable manual action' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 11b06ef50196ac1bf4426251fe4c2601e9c52802..915288cd9163b56c3c8497a828c1573cfc3208c8 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -30,7 +30,8 @@ describe Ci::RetryBuildService do
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_performance
- job_artifacts_codequality job_artifacts_metrics scheduled_at].freeze
+ job_artifacts_codequality job_artifacts_metrics scheduled_at
+ job_variables].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
@@ -65,6 +66,8 @@ describe Ci::RetryBuildService do
file_type: file_type, job: build, expire_at: build.artifacts_expire_at)
end
+ create(:ci_job_variable, job: build)
+
build.reload
end