Add Job specific variables
Adds Job specific variables to facilitate specifying variables when running manual jobs.
This commit is contained in:
parent
946f7c0687
commit
a5aa40c5fe
|
|
@ -1,9 +1,11 @@
|
|||
<script>
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import ManualVariablesForm from './manual_variables_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLink,
|
||||
ManualVariablesForm,
|
||||
},
|
||||
props: {
|
||||
illustrationPath: {
|
||||
|
|
@ -23,6 +25,21 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
playable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
scheduled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
variablesSettingsUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
action: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
|
@ -37,28 +54,40 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldRenderManualVariables() {
|
||||
return this.playable && !this.scheduled;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="row empty-state">
|
||||
<div class="col-12">
|
||||
<div :class="illustrationSizeClass" class="svg-content"><img :src="illustrationPath" /></div>
|
||||
<div :class="illustrationSizeClass" class="svg-content">
|
||||
<img :src="illustrationPath" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="text-content">
|
||||
<h4 class="js-job-empty-state-title text-center">{{ title }}</h4>
|
||||
|
||||
<p v-if="content" class="js-job-empty-state-content text-center">{{ content }}</p>
|
||||
|
||||
<p v-if="content" class="js-job-empty-state-content">{{ content }}</p>
|
||||
</div>
|
||||
<manual-variables-form
|
||||
v-if="shouldRenderManualVariables"
|
||||
:action="action"
|
||||
:variables-settings-url="variablesSettingsUrl"
|
||||
/>
|
||||
<div class="text-content">
|
||||
<div v-if="action" class="text-center">
|
||||
<gl-link
|
||||
:href="action.path"
|
||||
:data-method="action.method"
|
||||
class="js-job-empty-state-action btn btn-primary"
|
||||
>{{ action.button_title }}</gl-link
|
||||
>
|
||||
{{ action.button_title }}
|
||||
</gl-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
<!-- EO empty state -->
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions } from 'vuex';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'ManualVariablesForm',
|
||||
components: {
|
||||
GlButton,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
validator(value) {
|
||||
return (
|
||||
value === null ||
|
||||
(_.has(value, 'path') && _.has(value, 'method') && _.has(value, 'button_title'))
|
||||
);
|
||||
},
|
||||
},
|
||||
variablesSettingsUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
inputTypes: {
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
},
|
||||
i18n: {
|
||||
keyPlaceholder: s__('CiVariables|Input variable key'),
|
||||
valuePlaceholder: s__('CiVariables|Input variable value'),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
variables: [],
|
||||
key: '',
|
||||
secretValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
helpText() {
|
||||
return sprintf(
|
||||
s__(
|
||||
'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
|
||||
),
|
||||
{
|
||||
linkStart: `<a href="${this.variablesSettingsUrl}">`,
|
||||
linkEnd: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
key(newVal) {
|
||||
this.handleValueChange(newVal, this.$options.inputTypes.key);
|
||||
},
|
||||
secretValue(newVal) {
|
||||
this.handleValueChange(newVal, this.$options.inputTypes.value);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['triggerManualJob']),
|
||||
handleValueChange(newValue, type) {
|
||||
if (newValue !== '') {
|
||||
this.createNewVariable(type);
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
createNewVariable(type) {
|
||||
const newVariable = {
|
||||
key: this.key,
|
||||
secret_value: this.secretValue,
|
||||
id: _.uniqueId(),
|
||||
};
|
||||
|
||||
this.variables.push(newVariable);
|
||||
|
||||
return this.$nextTick().then(() => {
|
||||
this.$refs[`${this.$options.inputTypes[type]}-${newVariable.id}`][0].focus();
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
this.key = '';
|
||||
this.secretValue = '';
|
||||
},
|
||||
deleteVariable(id) {
|
||||
this.variables.splice(this.variables.findIndex(el => el.id === id), 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="js-manual-vars-form col-12">
|
||||
<label>{{ s__('CiVariables|Variables') }}</label>
|
||||
|
||||
<div class="ci-table">
|
||||
<div class="gl-responsive-table-row table-row-header pb-0 pt-0 border-0" role="row">
|
||||
<div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Key') }}</div>
|
||||
<div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Value') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-for="variable in variables" :key="variable.id" class="gl-responsive-table-row">
|
||||
<div class="table-section section-50">
|
||||
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
|
||||
<div class="table-mobile-content append-right-10">
|
||||
<input
|
||||
:ref="`${$options.inputTypes.key}-${variable.id}`"
|
||||
v-model="variable.key"
|
||||
:placeholder="$options.i18n.keyPlaceholder"
|
||||
class="ci-variable-body-item form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section section-50">
|
||||
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
|
||||
<div class="table-mobile-content append-right-10">
|
||||
<input
|
||||
:ref="`${$options.inputTypes.value}-${variable.id}`"
|
||||
v-model="variable.secret_value"
|
||||
:placeholder="$options.i18n.valuePlaceholder"
|
||||
class="ci-variable-body-item form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section section-10">
|
||||
<div class="table-mobile-header" role="rowheader"></div>
|
||||
<div class="table-mobile-content justify-content-end">
|
||||
<gl-button class="btn-transparent btn-blank w-25" @click="deleteVariable(variable.id)">
|
||||
<icon name="clear" />
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-responsive-table-row">
|
||||
<div class="table-section section-50">
|
||||
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
|
||||
<div class="table-mobile-content append-right-10">
|
||||
<input
|
||||
ref="inputKey"
|
||||
v-model="key"
|
||||
class="js-input-key form-control"
|
||||
:placeholder="$options.i18n.keyPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-section section-50">
|
||||
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
|
||||
<div class="table-mobile-content append-right-10">
|
||||
<input
|
||||
ref="inputSecretValue"
|
||||
v-model="secretValue"
|
||||
class="ci-variable-body-item form-control"
|
||||
:placeholder="$options.i18n.valuePlaceholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex prepend-top-default justify-content-center">
|
||||
<p class="text-muted" v-html="helpText"></p>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<gl-button variant="primary" @click="triggerManualJob(variables)">
|
||||
{{ action.button_title }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 () => {};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow specifying variables when running manual jobs
|
||||
merge_request: 30485
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -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
|
||||
11
db/schema.rb
11
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
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 420 KiB |
|
|
@ -323,6 +323,20 @@ stage has a job with a manual action.
|
|||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### Delay a job in a pipeline graph
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue