Commit 6fb5e398 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'mc/feature/manual-job-variables' into 'master'

Allow specifying variables when running manual jobs

Closes #24935

See merge request gitlab-org/gitlab-ce!30485
parents 946f7c06 a5aa40c5
<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 -->
......
<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
......
# 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
# 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
......
---
title: Allow specifying variables when running manual jobs
merge_request: 30485
author:
type: added
# 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
......@@ -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
......
......@@ -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.
......
......@@ -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
......
# 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:
......
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 }
......
# 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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment