diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
index 3ea8e0df02279cd280c316a82229f57b8d806f0b..40f7abaa914586a5962cc161d933a4f9fcff8a5a 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
@@ -13,6 +13,10 @@ const parseJsonArray = (triggers) => {
export default (containerId = 'js-ci-pipeline-triggers-list') => {
const containerEl = document.getElementById(containerId);
+ if (!containerEl) {
+ return null;
+ }
+
const triggers = parseJsonArray(containerEl.dataset.triggers);
return new Vue({
diff --git a/app/assets/javascripts/deploy_freeze/index.js b/app/assets/javascripts/deploy_freeze/index.js
index fd3f52b6da1b87407dfba9e5247b609aa190d502..85e515a1493e4d4027457fb1d00580311b33fa76 100644
--- a/app/assets/javascripts/deploy_freeze/index.js
+++ b/app/assets/javascripts/deploy_freeze/index.js
@@ -5,6 +5,10 @@ import createStore from './store';
export default () => {
const el = document.getElementById('js-deploy-freeze-table');
+ if (!el) {
+ return null;
+ }
+
const { projectId, timezoneData } = el.dataset;
const store = createStore({
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index ad6f84fbc07366b3e9a4e5fe74101117b6b51435..66845e03963eca00057933a11d9b5144bff71985 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -22,7 +22,9 @@ initInheritedGroupCiVariables();
// hide extra auto devops settings based checkbox state
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
-document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => {
+const extraSettingsToggle = document.querySelector('.js-toggle-extra-settings');
+
+extraSettingsToggle?.addEventListener('click', (event) => {
const { target } = event;
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 9a128adb926e192bc5b99d6dff4e022313a79f9f..94fc5bf0c75e71d9571a2f53dd98f36fa75e1dda 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -8,7 +8,8 @@ class CiCdController < Projects::ApplicationController
NUMBER_OF_RUNNERS_PER_PAGE = 20
layout 'project_settings'
- before_action :authorize_admin_pipeline!
+ before_action :authorize_admin_pipeline!, except: :show
+ before_action :authorize_admin_cicd_variables!, only: :show
before_action :check_builds_available!
before_action :define_variables
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 29ecca1b7e0060ee02015f6fd246d2234784d854..217d00c7d547ea11eb6c1ce95ffdc7e4b5286bf9 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
class Projects::VariablesController < Projects::ApplicationController
- before_action :authorize_admin_build!
+ before_action :authorize_admin_build!, except: :update
+ before_action :authorize_admin_cicd_variables!, only: :update
feature_category :secrets_management
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index db850f60910450546df646bf6f4ff2dc6db2ba69..abab206087230bc9e72ee2d1b75108507eb6b3f5 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -382,13 +382,13 @@ class ProjectType < BaseObject
field :ci_variables, Types::Ci::ProjectVariableType.connection_type,
null: true,
description: "List of the project's CI/CD variables.",
- authorize: :admin_build,
+ authorize: :admin_cicd_variables,
resolver: Resolvers::Ci::VariablesResolver
field :inherited_ci_variables, Types::Ci::InheritedCiVariableType.connection_type,
null: true,
description: "List of CI/CD variables the project inherited from its parent group and ancestors.",
- authorize: :admin_build,
+ authorize: :admin_cicd_variables,
resolver: Resolvers::Ci::InheritedVariablesResolver
field :ci_cd_settings, Types::Ci::CiCdSettingType,
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a686a94b7c66d93bdd1febb57fb6e9af04bbfa56..bc162f1215795e3cab8c1625fd506481888267cd 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -593,6 +593,7 @@ class ProjectPolicy < BasePolicy
enable :admin_incident_management_timeline_event_tag
enable :stop_environment
enable :read_import_error
+ enable :admin_cicd_variables
end
rule { can?(:admin_build) }.enable :manage_trigger
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index ab052a73ce02b0b316c90c029baeae49c01d6cb4..290756e5aad2c0c18cb872a87a13ff04f7a4a5a2 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -5,118 +5,121 @@
- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
-%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _("General pipelines")
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- = _("Customize your pipeline configuration.")
- .settings-content
- = render 'form'
-
-%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'autodevops-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = s_('CICD|Auto DevOps')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- - auto_devops_url = help_page_path('topics/autodevops/index')
- - quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- - auto_devops_link = link_to('', auto_devops_url, target: '_blank', rel: 'noopener noreferrer')
- - quickstart_link = link_to('', quickstart_url, target: '_blank', rel: 'noopener noreferrer')
- = safe_format(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}'), tag_pair(auto_devops_link, :auto_devops_start, :auto_devops_end), tag_pair(quickstart_link, :quickstart_start, :quickstart_end))
- .settings-content
- = render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?
-
-= render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded
-
-- expand_runners = expanded || params[:expand_runners]
-%section.settings.no-animate#js-runners-settings{ class: ('expanded' if expand_runners), data: { testid: 'runners-settings-content' } }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _("Runners")
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expand_runners ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
- = link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
- .settings-content
- = render 'projects/runners/settings'
+- if can?(current_user, :admin_pipeline, @project)
+ %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _("General pipelines")
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = _("Customize your pipeline configuration.")
+ .settings-content
+ = render 'form'
-- if Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact?
- %section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) }
+ %section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded), data: { testid: 'autodevops-settings-content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _("Artifacts")
+ = s_('CICD|Auto DevOps')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- = _("A job artifact is an archive of files and directories saved by a job when it finishes.")
+ - auto_devops_url = help_page_path('topics/autodevops/index')
+ - quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
+ - auto_devops_link = link_to('', auto_devops_url, target: '_blank', rel: 'noopener noreferrer')
+ - quickstart_link = link_to('', quickstart_url, target: '_blank', rel: 'noopener noreferrer')
+ = safe_format(s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}'), tag_pair(auto_devops_link, :auto_devops_start, :auto_devops_end), tag_pair(quickstart_link, :quickstart_start, :quickstart_end))
.settings-content
- #js-artifacts-settings-app{ data: { full_path: @project.full_path, help_page_path: help_page_path('ci/jobs/job_artifacts', anchor: 'keep-artifacts-from-most-recent-successful-jobs') } }
+ = render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?
-%section.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { testid: 'variables-settings-content' } }
- .settings-header
- = render 'ci/variables/header', expanded: expanded
- .settings-content
- = render 'ci/variables/index', save_endpoint: project_variables_path(@project)
+ = render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded
-%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _("Pipeline trigger tokens")
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- = _("Trigger a pipeline for a branch or tag by generating a trigger token and using it with an API call. The token impersonates a user's project access and permissions.")
- = link_to _('Learn more.'), help_page_path('ci/triggers/index'), target: '_blank', rel: 'noopener noreferrer'
- .settings-content
- = render 'projects/triggers/index'
+ - expand_runners = expanded || params[:expand_runners]
+ %section.settings.no-animate#js-runners-settings{ class: ('expanded' if expand_runners), data: { testid: 'runners-settings-content' } }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _("Runners")
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expand_runners ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
+ = link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = render 'projects/runners/settings'
-= render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded
+ - if Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact?
+ %section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _("Artifacts")
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = _("A job artifact is an archive of files and directories saved by a job when it finishes.")
+ .settings-content
+ #js-artifacts-settings-app{ data: { full_path: @project.full_path, help_page_path: help_page_path('ci/jobs/job_artifacts', anchor: 'keep-artifacts-from-most-recent-successful-jobs') } }
+
+- if can?(current_user, :admin_cicd_variables, @project)
+ %section.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { testid: 'variables-settings-content' } }
+ .settings-header
+ = render 'ci/variables/header', expanded: expanded
+ .settings-content
+ = render 'ci/variables/index', save_endpoint: project_variables_path(@project)
-- if can?(current_user, :create_freeze_period, @project)
- %section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) }
+- if can?(current_user, :admin_pipeline, @project)
+ %section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _("Deploy freezes")
+ = _("Pipeline trigger tokens")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- - freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze')
- - freeze_period_link_start = ''.html_safe % { url: freeze_period_docs }
- = html_escape(s_('DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}')) % { freeze_period_link_start: freeze_period_link_start, freeze_period_link_end: ''.html_safe, filename: tag.code('.gitlab-ci.yml') }
+ = _("Trigger a pipeline for a branch or tag by generating a trigger token and using it with an API call. The token impersonates a user's project access and permissions.")
+ = link_to _('Learn more.'), help_page_path('ci/triggers/index'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = render 'projects/triggers/index'
- - cron_syntax_url = 'https://crontab.guru/'
- - cron_syntax_link_start = ''.html_safe % { url: cron_syntax_url }
- = s_('DeployFreeze|Specify deploy freezes using %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}.').html_safe % { cron_syntax_link_start: cron_syntax_link_start, cron_syntax_link_end: "".html_safe }
+ = render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded
- .settings-content
- = render 'ci/deploy_freeze/index'
+ - if can?(current_user, :create_freeze_period, @project)
+ %section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _("Deploy freezes")
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ - freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze')
+ - freeze_period_link_start = ''.html_safe % { url: freeze_period_docs }
+ = html_escape(s_('DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}')) % { freeze_period_link_start: freeze_period_link_start, freeze_period_link_end: ''.html_safe, filename: tag.code('.gitlab-ci.yml') }
-%section.settings.no-animate#js-token-access{ class: ('expanded' if expanded) }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _("Token Access")
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded ? _('Collapse') : _('Expand')
- %p.gl-text-secondary
- = _("Control how the CI_JOB_TOKEN CI/CD variable is used for API access between projects.")
- .settings-content
- = render 'ci/token_access/index'
+ - cron_syntax_url = 'https://crontab.guru/'
+ - cron_syntax_link_start = ''.html_safe % { url: cron_syntax_url }
+ = s_('DeployFreeze|Specify deploy freezes using %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}.').html_safe % { cron_syntax_link_start: cron_syntax_link_start, cron_syntax_link_end: "".html_safe }
-- if show_secure_files_setting(@project, current_user)
- %section.settings.no-animate#js-secure-files{ class: ('expanded' if expanded) }
+ .settings-content
+ = render 'ci/deploy_freeze/index'
+
+ %section.settings.no-animate#js-token-access{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = _("Secure Files")
+ = _("Token Access")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
%p.gl-text-secondary
- = _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.")
- = link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer'
+ = _("Control how the CI_JOB_TOKEN CI/CD variable is used for API access between projects.")
.settings-content
- #js-ci-secure-files{ data: { project_id: @project.id, admin: can?(current_user, :admin_secure_files, @project).to_s, file_size_limit: Ci::SecureFile::FILE_SIZE_LIMIT.to_mb } }
+ = render 'ci/token_access/index'
+
+ - if show_secure_files_setting(@project, current_user)
+ %section.settings.no-animate#js-secure-files{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _("Secure Files")
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded ? _('Collapse') : _('Expand')
+ %p.gl-text-secondary
+ = _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.")
+ = link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ #js-ci-secure-files{ data: { project_id: @project.id, admin: can?(current_user, :admin_secure_files, @project).to_s, file_size_limit: Ci::SecureFile::FILE_SIZE_LIMIT.to_mb } }
diff --git a/ee/app/views/projects/settings/ci_cd/show.html.haml b/ee/app/views/projects/settings/ci_cd/show.html.haml
index a0987888cd8bf4bdc1ba69cdaa4329c6810c8ca6..b68d488d5a8b6566d02d46293e9f1d9f59369fba 100644
--- a/ee/app/views/projects/settings/ci_cd/show.html.haml
+++ b/ee/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,3 +1,4 @@
= render_ce 'projects/settings/ci_cd/show'
-= render 'projects/settings/ci_cd/pipeline_subscriptions'
+- if can?(current_user, :admin_pipeline, @project)
+ = render 'projects/settings/ci_cd/pipeline_subscriptions'
diff --git a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
index 429f0221d5ee79f69982d69f37119a6eedee6181..725defa85b30de6a4958c01bcc39947f21f051a2 100644
--- a/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
+++ b/ee/lib/ee/sidebars/projects/menus/settings_menu.rb
@@ -44,6 +44,7 @@ def custom_roles_menu_items
items << general_menu_item if custom_roles_general_menu_item?
items << access_tokens_menu_item if custom_roles_access_token_menu_item?
+ items << ci_cd_menu_item if custom_roles_ci_cd_menu_item?
items
end
@@ -55,6 +56,10 @@ def custom_roles_general_menu_item?
def custom_roles_access_token_menu_item?
can?(context.current_user, :manage_resource_access_tokens, context.project)
end
+
+ def custom_roles_ci_cd_menu_item?
+ can?(context.current_user, :admin_cicd_variables, context.project)
+ end
end
end
end
diff --git a/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb b/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb
index 65ce668661ff8742c1ea1d85bd6135ca439b0809..0f3bc0067a3ace196db7390172cf2aa79a4a1abd 100644
--- a/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb
+++ b/ee/spec/lib/ee/sidebars/projects/menus/settings_menu_spec.rb
@@ -99,5 +99,21 @@
end
end
end
+
+ describe 'CI/CD' do
+ let(:item_id) { :ci_cd }
+
+ describe 'when the user is not an admin but has `admin_cicd_variables` custom ability' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :admin_project, project).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :admin_cicd_variables, project).and_return(true)
+ end
+
+ it 'includes CI/CD menu item' do
+ expect(subject.title).to eql('CI/CD')
+ end
+ end
+ end
end
end
diff --git a/ee/spec/requests/custom_roles/admin_cicd_variables/projects_request_spec.rb b/ee/spec/requests/custom_roles/admin_cicd_variables/projects_request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cb857bbb2c719e9b60d9a9a0d15bc9bd52563216
--- /dev/null
+++ b/ee/spec/requests/custom_roles/admin_cicd_variables/projects_request_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User with admin_cicd_variables custom role', feature_category: :secrets_management do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:role) { create(:member_role, :guest, namespace: group, admin_cicd_variables: true) }
+ let_it_be(:member) { create(:project_member, :guest, member_role: role, user: user, project: project) }
+
+ before do
+ stub_licensed_features(custom_roles: true)
+
+ sign_in(user)
+ end
+
+ describe Projects::Settings::CiCdController do
+ describe '#show' do
+ it 'user can view CI/CD settings page' do
+ get project_settings_ci_cd_path(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to include('CI/CD Settings')
+ end
+ end
+ end
+
+ describe 'Querying CI Variables and environment scopes' do
+ include GraphqlHelpers
+
+ let_it_be(:variable) { create(:ci_variable, project: project, key: 'new_key', value: 'dummy_value') }
+ let_it_be(:inherited_variable) { create(:ci_group_variable, group: group, key: 'my_key') }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ ciVariables {
+ nodes {
+ key
+ value
+ }
+ }
+ inheritedCiVariables {
+ nodes {
+ key
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns variables and inherited variables for the project' do
+ result = GitlabSchema.execute(query, context: { current_user: user }).as_json
+
+ project_data = result.dig('data', 'project')
+
+ expect(project_data.dig('ciVariables', 'nodes').first).to eq('key' => variable.key, 'value' => variable.value)
+ expect(project_data.dig('inheritedCiVariables', 'nodes').first).to eq('key' => inherited_variable.key)
+ end
+ end
+
+ describe Projects::VariablesController do
+ describe '#update' do
+ it 'user can create CI/CD variables' do
+ params = { variables_attributes: [{ key: 'new_key', secret_value: 'dummy_value' }] }
+ put project_variables_path(project, params: params, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Json.parse(response.body)['variables'][0])
+ .to include('key' => 'new_key', 'value' => 'dummy_value')
+ end
+
+ it 'user can update CI/CD variables' do
+ var = create(:ci_variable, project: project)
+
+ params = { variables_attributes: [{ id: var.id, key: 'new_key', secret_value: 'dummy_value' }] }
+ put project_variables_path(project, params: params, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Json.parse(response.body)['variables'][0])
+ .to include('key' => 'new_key', 'value' => 'dummy_value')
+ end
+
+ it 'user can destroy CI/CD variables' do
+ var = create(:ci_variable, project: project)
+
+ params = { variables_attributes: [{ id: var.id, _destroy: 'true' }] }
+ put project_variables_path(project, params: params, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect { var.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end