diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
commit | e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 (patch) | |
tree | 2fcdfa7dcdb9db8f5208b2562f4b4e803d671243 /app/models/ci | |
parent | ffda4e7bcac36987f936b4ba515995a6698698f0 (diff) |
Add latest changes from gitlab-org/gitlab@16-2-stable-eev16.2.0-rc42
Diffstat (limited to 'app/models/ci')
25 files changed, 247 insertions, 101 deletions
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb index f87b18d516f..1f6d218b015 100644 --- a/app/models/ci/artifact_blob.rb +++ b/app/models/ci/artifact_blob.rb @@ -4,8 +4,6 @@ module Ci class ArtifactBlob include BlobLike - EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json .xml .log].freeze - attr_reader :entry def initialize(entry) @@ -35,31 +33,18 @@ module Ci :build_artifact end - def external_url(project, job) - return unless external_link?(job) - - url_project_path = project.full_path.partition('/').last - - artifact_path = [ - '-', url_project_path, '-', - 'jobs', job.id, - 'artifacts', path - ].join('/') - - "#{project.pages_namespace_url}/#{artifact_path}" + def external_url(job) + pages_url_builder(job.project).artifact_url(entry, job) end def external_link?(job) - pages_config.enabled && - pages_config.artifacts_server && - EXTENSIONS_SERVED_BY_PAGES.include?(File.extname(name)) && - (pages_config.access_control || job.project.public?) + pages_url_builder(job.project).artifact_url_available?(entry, job) end private - def pages_config - Gitlab.config.pages + def pages_url_builder(project) + @pages_url_builder ||= Gitlab::Pages::UrlBuilder.new(project) end end end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 7cdd0d56a98..5052d84378f 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -224,15 +224,46 @@ module Ci end end + def target_revision_ref + downstream_pipeline_params.dig(:target_revision, :ref) + end + def downstream_variables - calculate_downstream_variables - .reverse # variables priority - .uniq { |var| var[:key] } # only one variable key to pass - .reverse + Gitlab::Ci::Variables::Downstream::Generator.new(self).calculate end - def target_revision_ref - downstream_pipeline_params.dig(:target_revision, :ref) + def variables + strong_memoize(:variables) do + Gitlab::Ci::Variables::Collection.new + .concat(scoped_variables) + .concat(pipeline.persisted_variables) + end + end + + def pipeline_variables + pipeline.variables + end + + def pipeline_schedule_variables + return [] unless pipeline.pipeline_schedule + + pipeline.pipeline_schedule.variables.to_a + end + + def forward_yaml_variables? + strong_memoize(:forward_yaml_variables) do + result = options&.dig(:trigger, :forward, :yaml_variables) + + result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result + end + end + + def forward_pipeline_variables? + strong_memoize(:forward_pipeline_variables) do + result = options&.dig(:trigger, :forward, :pipeline_variables) + + result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result + end end private @@ -273,70 +304,6 @@ module Ci } } end - - def calculate_downstream_variables - expand_variables = scoped_variables - .concat(pipeline.persisted_variables) - .to_runner_variables - - # The order of this list refers to the priority of the variables - downstream_yaml_variables(expand_variables) + - downstream_pipeline_variables(expand_variables) + - downstream_pipeline_schedule_variables(expand_variables) - end - - def downstream_yaml_variables(expand_variables) - return [] unless forward_yaml_variables? - - yaml_variables.to_a.map do |hash| - if hash[:raw] - { key: hash[:key], value: hash[:value], raw: true } - else - { key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) } - end - end - end - - def downstream_pipeline_variables(expand_variables) - return [] unless forward_pipeline_variables? - - pipeline.variables.to_a.map do |variable| - if variable.raw? - { key: variable.key, value: variable.value, raw: true } - else - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } - end - end - end - - def downstream_pipeline_schedule_variables(expand_variables) - return [] unless forward_pipeline_variables? - return [] unless pipeline.pipeline_schedule - - pipeline.pipeline_schedule.variables.to_a.map do |variable| - if variable.raw? - { key: variable.key, value: variable.value, raw: true } - else - { key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) } - end - end - end - - def forward_yaml_variables? - strong_memoize(:forward_yaml_variables) do - result = options&.dig(:trigger, :forward, :yaml_variables) - - result.nil? ? FORWARD_DEFAULTS[:yaml_variables] : result - end - end - - def forward_pipeline_variables? - strong_memoize(:forward_pipeline_variables) do - result = options&.dig(:trigger, :forward, :pipeline_variables) - - result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result - end - end end end diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 382f861a802..4c723bb7c0c 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -10,11 +10,9 @@ module Ci include Presentable include ChronicDurationAttribute include Gitlab::Utils::StrongMemoize - include SafelyChangeColumnDefault self.table_name = 'p_ci_builds_metadata' self.primary_key = 'id' - columns_changing_default :partition_id partitionable scope: :build diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 940221619b3..317f2523f69 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -3,8 +3,12 @@ module Ci class BuildNeed < Ci::ApplicationRecord include Ci::Partitionable - include BulkInsertSafe include IgnorableColumns + include SafelyChangeColumnDefault + include BulkInsertSafe + + columns_changing_default :partition_id + ignore_column :id_convert_to_bigint, remove_with: '16.4', remove_after: '2023-09-22' belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb index 966884ae158..0b88f745d78 100644 --- a/app/models/ci/build_pending_state.rb +++ b/app/models/ci/build_pending_state.rb @@ -2,6 +2,9 @@ class Ci::BuildPendingState < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id, inverse_of: :pending_state diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb index b2d99fab295..90b621b8da1 100644 --- a/app/models/ci/build_report_result.rb +++ b/app/models/ci/build_report_result.rb @@ -3,6 +3,9 @@ module Ci class BuildReportResult < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id self.primary_key = :build_id diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index 5773b6132be..eaa2e1c428e 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -5,6 +5,9 @@ module Ci # Data will be removed after transitioning from running to any state. class BuildRunnerSession < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' DEFAULT_SERVICE_NAME = 'build' diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 03b59b19ef1..0a0f401c9d5 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -8,6 +8,9 @@ module Ci include ::Checksummable include ::Gitlab::ExclusiveLeaseHelpers include ::Gitlab::OptimisticLocking + include SafelyChangeColumnDefault + + columns_changing_default :partition_id belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :trace_chunks @@ -166,7 +169,7 @@ module Ci raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save? self.class.with_read_consistency(build) do - reset.then(&:unsafe_persist_data!) + reset.unsafe_persist_data! end end rescue FailedToObtainLockError @@ -242,7 +245,7 @@ module Ci ## # We need to so persist data then save a new store identifier before we # remove data from the previous store to make this operation - # trasnaction-safe. `unsafe_set_data! calls `save!` because of this + # transaction-safe. `unsafe_set_data! calls `save!` because of this # reason. # # TODO consider using callbacks and state machine to remove old data diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb index 4c76089617f..c5ad3d19425 100644 --- a/app/models/ci/build_trace_metadata.rb +++ b/app/models/ci/build_trace_metadata.rb @@ -3,6 +3,9 @@ module Ci class BuildTraceMetadata < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id MAX_ATTEMPTS = 5 self.table_name = 'ci_build_trace_metadata' diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index 77cfe91ddd6..38603ddfe59 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -19,6 +19,8 @@ module Ci delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project + enum state: { draft: 0, published: 1 } + def versions project.releases.order_released_desc end diff --git a/app/models/ci/external_pull_request.rb b/app/models/ci/external_pull_request.rb new file mode 100644 index 00000000000..bd37aa9f85a --- /dev/null +++ b/app/models/ci/external_pull_request.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# This model stores pull requests coming from external providers, such as +# GitHub, when GitLab project is set as CI/CD only and remote mirror. +# +# When setting up a remote mirror with GitHub we subscribe to push and +# pull_request webhook events. When a pull request is opened on GitHub, +# a webhook is sent out, we create or update the status of the pull +# request locally. +# +# When the mirror is updated and changes are pushed to branches we check +# if there are open pull requests for the source and target branch. +# If so, we create pipelines for external pull requests. +module Ci + class ExternalPullRequest < Ci::ApplicationRecord + include Gitlab::Utils::StrongMemoize + include ShaAttribute + include EachBatch + + belongs_to :project + + sha_attribute :source_sha + sha_attribute :target_sha + + validates :source_branch, presence: true + validates :target_branch, presence: true + validates :source_sha, presence: true + validates :target_sha, presence: true + validates :source_repository, presence: true + validates :target_repository, presence: true + validates :status, presence: true + + enum status: { + open: 1, + closed: 2 + } + + # We currently don't support pull requests from fork, so + # we are going to return an error to the webhook + validate :not_from_fork + + scope :by_source_branch, ->(branch) { where(source_branch: branch) } + scope :by_source_repository, ->(repository) { where(source_repository: repository) } + + # Needed to override Ci::ApplicationRecord as this does not have ci_ table prefix + self.table_name = 'external_pull_requests' + + def self.create_or_update_from_params(params) + find_params = params.slice(:project_id, :source_branch, :target_branch) + + safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request| + yield(pull_request) if block_given? + end + end + + def actual_branch_head? + actual_source_branch_sha == source_sha + end + + def from_fork? + source_repository != target_repository + end + + def source_ref + Gitlab::Git::BRANCH_REF_PREFIX + source_branch + end + + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch) + variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch) + end + end + + def modified_paths + project.repository.diff_stats(target_sha, source_sha).paths + end + + private + + def actual_source_branch_sha + project.commit(source_ref)&.sha + end + + def not_from_fork + return unless from_fork? + + errors.add(:base, _('Pull requests from fork are not supported')) + end + + def self.safe_find_or_initialize_and_update(find:, update:) + safe_ensure_unique(retries: 1) do + model = find_or_initialize_by(find) + + yield(model) if model.update(update) && block_given? + + model + end + end + end +end diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index 5522a01758f..25d0228beb0 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -14,6 +14,7 @@ module Ci alias_attribute :secret_value, :value + validates :description, length: { maximum: 255 }, allow_blank: true validates :key, uniqueness: { scope: [:group_id, :environment_scope], message: "(%{value}) has already been taken" @@ -36,6 +37,12 @@ module Ci .pluck(:environment_scope) end + # Sorting + scope :order_created_asc, -> { reorder(created_at: :asc) } + scope :order_created_desc, -> { reorder(created_at: :desc) } + scope :order_key_asc, -> { reorder(key: :asc) } + scope :order_key_desc, -> { reorder(key: :desc) } + self.limit_name = 'group_ci_variables' self.limit_scope = :group @@ -50,5 +57,14 @@ module Ci def group_ci_cd_settings_path Gitlab::Routing.url_helpers.group_settings_ci_cd_path(group) end + + def self.sort_by_attribute(method) + case method.to_s + when 'created_at_asc' then order_created_asc + when 'created_at_desc' then order_created_desc + when 'key_asc' then order_key_asc + when 'key_desc' then order_key_desc + end + end end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 5cd7988837e..11d70e088e9 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -13,6 +13,9 @@ module Ci include FileStoreMounter include EachBatch include Gitlab::Utils::StrongMemoize + include SafelyChangeColumnDefault + + columns_changing_default :partition_id enum accessibility: { public: 0, private: 1 }, _suffix: true diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb index 573999995bc..21c9842399e 100644 --- a/app/models/ci/job_variable.rb +++ b/app/models/ci/job_variable.rb @@ -5,8 +5,11 @@ module Ci include Ci::Partitionable include Ci::NewHasVariable include Ci::RawVariable + include SafelyChangeColumnDefault include BulkInsertSafe + columns_changing_default :partition_id + belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id, inverse_of: :job_variables partitionable scope: :job diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb index 14050a1e78e..dc9a8b7a1bf 100644 --- a/app/models/ci/pending_build.rb +++ b/app/models/ci/pending_build.rb @@ -4,6 +4,9 @@ module Ci class PendingBuild < Ci::ApplicationRecord include EachBatch include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id belongs_to :project belongs_to :build, class_name: 'Ci::Build' diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb index 57aa1962bd2..f713d5952bc 100644 --- a/app/models/ci/persistent_ref.rb +++ b/app/models/ci/persistent_ref.rb @@ -19,6 +19,11 @@ module Ci false end + # This needs to be kept in sync with `Ci::Pipeline#after_transition` calling `pipeline.persistent_ref.delete` + def should_delete? + pipeline.status.to_sym.in?(::Ci::Pipeline.stopped_statuses) + end + def create create_ref(sha, path) rescue StandardError => e @@ -27,6 +32,8 @@ module Ci end def delete + return unless should_delete? + delete_refs(path) rescue Gitlab::Git::Repository::NoRepository # no-op diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 6f2939583e0..bd327cfbe7b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -17,6 +17,9 @@ module Ci include UpdatedAtFilterable include EachBatch include FastDestroyAll::Helpers + include SafelyChangeColumnDefault + + columns_changing_default :partition_id include IgnorableColumns ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' @@ -51,7 +54,7 @@ module Ci belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline', inverse_of: :auto_canceled_pipelines belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' belongs_to :merge_request, class_name: 'MergeRequest' - belongs_to :external_pull_request + belongs_to :external_pull_request, class_name: 'Ci::ExternalPullRequest' belongs_to :ci_ref, class_name: 'Ci::Ref', foreign_key: :ci_ref_id, inverse_of: :pipelines has_internal_id :iid, scope: :project, presence: false, @@ -335,9 +338,14 @@ module Ci end end + # This needs to be kept in sync with `Ci::PipelineRef#should_delete?` after_transition any => ::Ci::Pipeline.stopped_statuses do |pipeline| pipeline.run_after_commit do - pipeline.persistent_ref.delete + if Feature.enabled?(:pipeline_cleanup_ref_worker_async, pipeline.project) + ::Ci::PipelineCleanupRefWorker.perform_async(pipeline.id) + else + pipeline.persistent_ref.delete + end end end diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index f2457af0074..9747f9ef527 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -5,9 +5,12 @@ module Ci include Ci::Partitionable include Ci::HasVariable include Ci::RawVariable - include IgnorableColumns + include SafelyChangeColumnDefault + + columns_changing_default :partition_id ignore_column :id_convert_to_bigint, remove_with: '16.3', remove_after: '2023-08-22' + ignore_column :pipeline_id_convert_to_bigint, remove_with: '16.5', remove_after: '2023-10-22' belongs_to :pipeline diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 6319163b0d7..4eb5c3c9ed2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -72,6 +72,7 @@ module Ci has_many :runner_managers, inverse_of: :runner has_many :builds + has_many :running_builds, inverse_of: :runner has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :runner_projects, disable_joins: true has_many :runner_namespaces, inverse_of: :runner, autosave: true @@ -198,6 +199,7 @@ module Ci scope :order_created_at_desc, -> { order(created_at: :desc) } scope :order_token_expires_at_asc, -> { order(token_expires_at: :asc) } scope :order_token_expires_at_desc, -> { order(token_expires_at: :desc) } + scope :with_tags, -> { preload(:tags) } scope :with_creator, -> { preload(:creator) } @@ -456,7 +458,7 @@ module Ci end new_version = values[:version] - schedule_runner_version_update(new_version) if new_version && values[:version] != version + schedule_runner_version_update(new_version) if new_version && new_version != version merge_cache_attributes(values) diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb index e36024d9f5b..3a3f95a8c69 100644 --- a/app/models/ci/runner_manager.rb +++ b/app/models/ci/runner_manager.rb @@ -44,6 +44,10 @@ module Ci remove_duplicates: false).where(created_some_time_ago) end + scope :for_runner, ->(runner_id) do + where(runner_id: runner_id) + end + def self.online_contact_time_deadline Ci::Runner.online_contact_time_deadline end @@ -52,6 +56,13 @@ module Ci STALE_TIMEOUT.ago end + def self.aggregate_upgrade_status_by_runner_id + joins(:runner_version) + .group(:runner_id) + .maximum(:status) + .transform_values { |s| Ci::RunnerVersion.statuses.key(s).to_sym } + end + def heartbeat(values, update_contacted_at: true) ## # We can safely ignore writes performed by a runner heartbeat. We do @@ -66,7 +77,7 @@ module Ci end new_version = values[:version] - schedule_runner_version_update(new_version) if new_version && values[:version] != version + schedule_runner_version_update(new_version) if new_version && new_version != version merge_cache_attributes(values) diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb index e6f80658f5d..cfdc47de531 100644 --- a/app/models/ci/running_build.rb +++ b/app/models/ci/running_build.rb @@ -10,6 +10,9 @@ module Ci # of the running builds there is worth the additional pressure. class RunningBuild < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id partitionable scope: :build diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb index 719d19f4169..4853c57d41f 100644 --- a/app/models/ci/sources/pipeline.rb +++ b/app/models/ci/sources/pipeline.rb @@ -5,6 +5,9 @@ module Ci class Pipeline < Ci::ApplicationRecord include Ci::Partitionable include Ci::NamespacedModelName + include SafelyChangeColumnDefault + + columns_changing_default :partition_id self.table_name = "ci_sources_pipelines" diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d61760bd0fc..4f9a2e44562 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -7,6 +7,9 @@ module Ci include Ci::HasStatus include Gitlab::OptimisticLocking include Presentable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id partitionable scope: :pipeline @@ -148,7 +151,7 @@ module Ci end def manual_playable? - blocked? || skipped? + blocked? end # This will be removed with ci_remove_ensure_stage_service diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb index cfef1249164..37893f6cdae 100644 --- a/app/models/ci/unit_test_failure.rb +++ b/app/models/ci/unit_test_failure.rb @@ -3,6 +3,9 @@ module Ci class UnitTestFailure < Ci::ApplicationRecord include Ci::Partitionable + include SafelyChangeColumnDefault + + columns_changing_default :partition_id REPORT_WINDOW = 14.days diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 23fe89c38df..6f5972ebefa 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -14,6 +14,7 @@ module Ci alias_attribute :secret_value, :value + validates :description, length: { maximum: 255 }, allow_blank: true validates :key, uniqueness: { scope: [:project_id, :environment_scope], message: "(%{value}) has already been taken" |