Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									a7364a0474
								
							
						
					
					
						commit
						254f79fb35
					
				| 
						 | 
				
			
			@ -28,7 +28,6 @@ import initUserPopovers from '~/user_popovers';
 | 
			
		|||
import { mergeUrlParams } from '~/lib/utils/url_utility';
 | 
			
		||||
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
 | 
			
		||||
import { isScopedLabel } from '~/lib/utils/common_utils';
 | 
			
		||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 | 
			
		||||
 | 
			
		||||
import { convertToCamelCase } from '~/lib/utils/text_utility';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +36,7 @@ export default {
 | 
			
		|||
    openedAgo: __('opened %{timeAgoString} by %{user}'),
 | 
			
		||||
    openedAgoJira: __('opened %{timeAgoString} by %{user} in Jira'),
 | 
			
		||||
  },
 | 
			
		||||
  inject: ['scopedLabelsAvailable'],
 | 
			
		||||
  components: {
 | 
			
		||||
    IssueAssignees,
 | 
			
		||||
    GlLink,
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,6 @@ export default {
 | 
			
		|||
    GlTooltip,
 | 
			
		||||
    SafeHtml,
 | 
			
		||||
  },
 | 
			
		||||
  mixins: [glFeatureFlagsMixin()],
 | 
			
		||||
  props: {
 | 
			
		||||
    issuable: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
| 
						 | 
				
			
			@ -85,9 +84,6 @@ export default {
 | 
			
		|||
 | 
			
		||||
      return this.issuableLink({ milestone_title: title });
 | 
			
		||||
    },
 | 
			
		||||
    scopedLabelsAvailable() {
 | 
			
		||||
      return this.glFeatures.scopedLabels;
 | 
			
		||||
    },
 | 
			
		||||
    hasWeight() {
 | 
			
		||||
      return isNumber(this.issuable.weight);
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,10 +41,13 @@ function mountIssuablesListApp() {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  document.querySelectorAll('.js-issuables-list').forEach(el => {
 | 
			
		||||
    const { canBulkEdit, emptyStateMeta = {}, ...data } = el.dataset;
 | 
			
		||||
    const { canBulkEdit, emptyStateMeta = {}, scopedLabelsAvailable, ...data } = el.dataset;
 | 
			
		||||
 | 
			
		||||
    return new Vue({
 | 
			
		||||
      el,
 | 
			
		||||
      provide: {
 | 
			
		||||
        scopedLabelsAvailable: parseBoolean(scopedLabelsAvailable),
 | 
			
		||||
      },
 | 
			
		||||
      render(createElement) {
 | 
			
		||||
        return createElement(IssuablesListApp, {
 | 
			
		||||
          props: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,9 +8,6 @@ module IssuableActions
 | 
			
		|||
    before_action :authorize_destroy_issuable!, only: :destroy
 | 
			
		||||
    before_action :check_destroy_confirmation!, only: :destroy
 | 
			
		||||
    before_action :authorize_admin_issuable!, only: :bulk_update
 | 
			
		||||
    before_action only: :show do
 | 
			
		||||
      push_frontend_feature_flag(:scoped_labels, type: :licensed, default_enabled: true)
 | 
			
		||||
    end
 | 
			
		||||
    before_action do
 | 
			
		||||
      push_frontend_feature_flag(:not_issuable_queries, @project, default_enabled: true)
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,10 +57,6 @@ class Projects::IssuesController < Projects::ApplicationController
 | 
			
		|||
    record_experiment_user(:invite_members_version_b)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  before_action only: :index do
 | 
			
		||||
    push_frontend_feature_flag(:scoped_labels, @project, type: :licensed)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  around_action :allow_gitaly_ref_name_caching, only: [:discussions]
 | 
			
		||||
 | 
			
		||||
  respond_to :html
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -214,7 +214,7 @@ class Projects::PipelinesController < Projects::ApplicationController
 | 
			
		|||
  def config_variables
 | 
			
		||||
    respond_to do |format|
 | 
			
		||||
      format.json do
 | 
			
		||||
        render json: Ci::ListConfigVariablesService.new(@project).execute(params[:sha])
 | 
			
		||||
        render json: Ci::ListConfigVariablesService.new(@project, current_user).execute(params[:sha])
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController
 | 
			
		|||
  before_action :redirect_deprecated_prometheus_service, only: [:update]
 | 
			
		||||
  before_action only: :edit do
 | 
			
		||||
    push_frontend_feature_flag(:jira_issues_integration, @project, type: :licensed, default_enabled: true)
 | 
			
		||||
    push_frontend_feature_flag(:jira_vulnerabilities_integration, @project, type: :licensed, default_enabled: true)
 | 
			
		||||
    push_frontend_feature_flag(:jira_for_vulnerabilities, @project, type: :development, default_enabled: false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  respond_to :html
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -170,6 +170,11 @@ module IssuesHelper
 | 
			
		|||
      submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable)
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Overridden in EE
 | 
			
		||||
  def scoped_labels_available?(parent)
 | 
			
		||||
    false
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
IssuesHelper.prepend_if_ee('EE::IssuesHelper')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -416,6 +416,7 @@ class ApplicationSetting < ApplicationRecord
 | 
			
		|||
  attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm
 | 
			
		||||
  attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
 | 
			
		||||
  attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_truncated_aes_256_gcm
 | 
			
		||||
  attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm
 | 
			
		||||
 | 
			
		||||
  before_validation :ensure_uuid!
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ class JiraService < IssueTrackerService
 | 
			
		|||
 | 
			
		||||
  # TODO: we can probably just delegate as part of
 | 
			
		||||
  # https://gitlab.com/gitlab-org/gitlab/issues/29404
 | 
			
		||||
  data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled
 | 
			
		||||
  data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled, :vulnerabilities_enabled, :vulnerabilities_issuetype
 | 
			
		||||
 | 
			
		||||
  before_update :reset_password
 | 
			
		||||
  after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,10 @@ module Ci
 | 
			
		|||
      config = project.ci_config_for(sha)
 | 
			
		||||
      return {} unless config
 | 
			
		||||
 | 
			
		||||
      result = Gitlab::Ci::YamlProcessor.new(config).execute
 | 
			
		||||
      result = Gitlab::Ci::YamlProcessor.new(config, project: project,
 | 
			
		||||
                                                     user:    current_user,
 | 
			
		||||
                                                     sha:     sha).execute
 | 
			
		||||
 | 
			
		||||
      result.valid? ? result.variables_with_data : {}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,11 +26,27 @@ module Projects
 | 
			
		|||
          project.set_repository_writable!
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        if result && block_given?
 | 
			
		||||
          yield
 | 
			
		||||
        result
 | 
			
		||||
      rescue Gitlab::Git::CommandError => e
 | 
			
		||||
        logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
 | 
			
		||||
 | 
			
		||||
        rollback_migration!
 | 
			
		||||
 | 
			
		||||
        false
 | 
			
		||||
      rescue OpenSSL::Cipher::CipherError => e
 | 
			
		||||
        logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
 | 
			
		||||
 | 
			
		||||
        rollback_migration!
 | 
			
		||||
 | 
			
		||||
        false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def rollback_migration!
 | 
			
		||||
        rollback_folder_move
 | 
			
		||||
        project.storage_version = nil
 | 
			
		||||
        project.set_repository_writable!
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,14 +21,32 @@ module Projects
 | 
			
		|||
          project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        project.repository_read_only = false
 | 
			
		||||
        project.transaction do
 | 
			
		||||
          project.save!(validate: false)
 | 
			
		||||
 | 
			
		||||
        if result && block_given?
 | 
			
		||||
          yield
 | 
			
		||||
          project.set_repository_writable!
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
      rescue Gitlab::Git::CommandError => e
 | 
			
		||||
        logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
 | 
			
		||||
 | 
			
		||||
        rollback_migration!
 | 
			
		||||
 | 
			
		||||
        false
 | 
			
		||||
      rescue OpenSSL::Cipher::CipherError => e
 | 
			
		||||
        logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
 | 
			
		||||
 | 
			
		||||
        rollback_migration!
 | 
			
		||||
 | 
			
		||||
        false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def rollback_migration!
 | 
			
		||||
        rollback_folder_move
 | 
			
		||||
        project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
 | 
			
		||||
        project.set_repository_writable!
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,6 +30,7 @@
 | 
			
		|||
      'can-bulk-edit': @can_bulk_update.to_json,
 | 
			
		||||
      'empty-state-meta': { svg_path: image_path('illustrations/issues.svg') },
 | 
			
		||||
      'sort-key': @sort,
 | 
			
		||||
      type: 'issues' } }
 | 
			
		||||
      type: 'issues',
 | 
			
		||||
      'scoped-labels-available': scoped_labels_available?(@group).to_json } }
 | 
			
		||||
  - else
 | 
			
		||||
    = render 'shared/issues'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,8 @@
 | 
			
		|||
    'empty-state-meta': data_empty_state_meta.to_json,
 | 
			
		||||
    'can-bulk-edit': @can_bulk_update.to_json,
 | 
			
		||||
    'sort-key': @sort,
 | 
			
		||||
    type: type } }
 | 
			
		||||
    type: type,
 | 
			
		||||
    'scoped-labels-available': scoped_labels_available?(@project).to_json } }
 | 
			
		||||
- else
 | 
			
		||||
  - empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
 | 
			
		||||
  %ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
title: Fix pipeline security tab filters not showing
 | 
			
		||||
merge_request: 47294
 | 
			
		||||
author:
 | 
			
		||||
type: fixed
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
title: "Hashed Storage: make migration and rollback resilient to exceptions"
 | 
			
		||||
merge_request: 46178
 | 
			
		||||
author:
 | 
			
		||||
type: fixed
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
title: Fix config variables when having includes
 | 
			
		||||
merge_request: 47189
 | 
			
		||||
author:
 | 
			
		||||
type: fixed
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
title: Fix duplicate epic iids and add uniqueness constraint
 | 
			
		||||
merge_request: 47081
 | 
			
		||||
author:
 | 
			
		||||
type: fixed
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
---
 | 
			
		||||
title: Add cloud_license_auth_token column to application_settings
 | 
			
		||||
merge_request: 47396
 | 
			
		||||
author:
 | 
			
		||||
type: added
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
---
 | 
			
		||||
name: jira_for_vulnerabilities
 | 
			
		||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46982
 | 
			
		||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276893
 | 
			
		||||
type: development
 | 
			
		||||
group: group::threat insights
 | 
			
		||||
default_enabled: false
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AddVulnerabilitiesEnabledAndIssuetypeToJiraTrackerData < ActiveRecord::Migration[6.0]
 | 
			
		||||
  DOWNTIME = false
 | 
			
		||||
 | 
			
		||||
  # rubocop:disable Migration/AddLimitToTextColumns
 | 
			
		||||
  # limit is added in 20201105143312_add_text_limit_to_jira_tracker_data_issuetype.rb
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :jira_tracker_data, :vulnerabilities_issuetype, :text
 | 
			
		||||
    add_column :jira_tracker_data, :vulnerabilities_enabled, :boolean, default: false, null: false
 | 
			
		||||
  end
 | 
			
		||||
  # rubocop:enable Migration/AddLimitToTextColumns
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AddTextLimitToJiraTrackerDataIssuetype < ActiveRecord::Migration[6.0]
 | 
			
		||||
  include Gitlab::Database::MigrationHelpers
 | 
			
		||||
 | 
			
		||||
  DOWNTIME = false
 | 
			
		||||
 | 
			
		||||
  disable_ddl_transaction!
 | 
			
		||||
 | 
			
		||||
  def up
 | 
			
		||||
    add_text_limit :jira_tracker_data, :vulnerabilities_issuetype, 255
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down
 | 
			
		||||
    remove_text_limit :jira_tracker_data, :vulnerabilities_issuetype
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AddCloudLicenseAuthTokenToSettings < ActiveRecord::Migration[6.0]
 | 
			
		||||
  DOWNTIME = false
 | 
			
		||||
 | 
			
		||||
  # rubocop:disable Migration/AddLimitToTextColumns
 | 
			
		||||
  # limit is added in 20201111110918_add_cloud_license_auth_token_application_settings_text_limit
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :application_settings, :encrypted_cloud_license_auth_token, :text
 | 
			
		||||
    add_column :application_settings, :encrypted_cloud_license_auth_token_iv, :text
 | 
			
		||||
  end
 | 
			
		||||
  # rubocop:enable Migration/AddLimitToTextColumns
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AddCloudLicenseAuthTokenApplicationSettingsTextLimit < ActiveRecord::Migration[6.0]
 | 
			
		||||
  include Gitlab::Database::MigrationHelpers
 | 
			
		||||
 | 
			
		||||
  DOWNTIME = false
 | 
			
		||||
 | 
			
		||||
  disable_ddl_transaction!
 | 
			
		||||
 | 
			
		||||
  def up
 | 
			
		||||
    add_text_limit :application_settings, :encrypted_cloud_license_auth_token_iv, 255
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down
 | 
			
		||||
    remove_text_limit :application_settings, :encrypted_cloud_license_auth_token_iv
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,121 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class DeduplicateEpicIids < ActiveRecord::Migration[6.0]
 | 
			
		||||
  include Gitlab::Database::MigrationHelpers
 | 
			
		||||
 | 
			
		||||
  DOWNTIME = false
 | 
			
		||||
  INDEX_NAME = 'index_epics_on_group_id_and_iid'
 | 
			
		||||
 | 
			
		||||
  disable_ddl_transaction!
 | 
			
		||||
 | 
			
		||||
  class Epic < ActiveRecord::Base
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  class InternalId < ActiveRecord::Base
 | 
			
		||||
    class << self
 | 
			
		||||
      def generate_next(subject, scope, usage, init)
 | 
			
		||||
        InternalIdGenerator.new(subject, scope, usage, init).generate
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # Increments #last_value and saves the record
 | 
			
		||||
    #
 | 
			
		||||
    # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
 | 
			
		||||
    # As such, the increment is atomic and safe to be called concurrently.
 | 
			
		||||
    def increment_and_save!
 | 
			
		||||
      update_and_save { self.last_value = (last_value || 0) + 1 }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def update_and_save(&block)
 | 
			
		||||
      lock!
 | 
			
		||||
      yield
 | 
			
		||||
      save!
 | 
			
		||||
      last_value
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # See app/models/internal_id
 | 
			
		||||
  class InternalIdGenerator
 | 
			
		||||
    attr_reader :subject, :scope, :scope_attrs, :usage, :init
 | 
			
		||||
 | 
			
		||||
    def initialize(subject, scope, usage, init = nil)
 | 
			
		||||
      @subject = subject
 | 
			
		||||
      @scope = scope
 | 
			
		||||
      @usage = usage
 | 
			
		||||
      @init = init
 | 
			
		||||
 | 
			
		||||
      raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty? || usage.to_s != 'epics'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # Generates next internal id and returns it
 | 
			
		||||
    # init: Block that gets called to initialize InternalId record if not present
 | 
			
		||||
    #       Make sure to not throw exceptions in the absence of records (if this is expected).
 | 
			
		||||
    def generate
 | 
			
		||||
      subject.transaction do
 | 
			
		||||
        # Create a record in internal_ids if one does not yet exist
 | 
			
		||||
        # and increment its last value
 | 
			
		||||
        #
 | 
			
		||||
        # Note this will acquire a ROW SHARE lock on the InternalId record
 | 
			
		||||
        record.increment_and_save!
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def record
 | 
			
		||||
      @record ||= (lookup || create_record)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def lookup
 | 
			
		||||
      InternalId.find_by(**scope, usage: usage_value)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def usage_value
 | 
			
		||||
      4 # see Enums::InternalId - this is the value for epics
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # Create InternalId record for (scope, usage) combination, if it doesn't exist
 | 
			
		||||
    #
 | 
			
		||||
    # We blindly insert without synchronization. If another process
 | 
			
		||||
    # was faster in doing this, we'll realize once we hit the unique key constraint
 | 
			
		||||
    # violation. We can safely roll-back the nested transaction and perform
 | 
			
		||||
    # a lookup instead to retrieve the record.
 | 
			
		||||
    def create_record
 | 
			
		||||
      raise ArgumentError, 'Cannot initialize without init!' unless init
 | 
			
		||||
 | 
			
		||||
      instance = subject.is_a?(::Class) ? nil : subject
 | 
			
		||||
 | 
			
		||||
      subject.transaction(requires_new: true) do
 | 
			
		||||
        InternalId.create!(
 | 
			
		||||
          **scope,
 | 
			
		||||
          usage: usage_value,
 | 
			
		||||
          last_value: init.call(instance, scope) || 0
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
    rescue ActiveRecord::RecordNotUnique
 | 
			
		||||
      lookup
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def up
 | 
			
		||||
    duplicate_epic_ids = ApplicationRecord.connection.execute('SELECT iid, group_id, COUNT(*) FROM epics GROUP BY iid, group_id HAVING COUNT(*) > 1;')
 | 
			
		||||
 | 
			
		||||
    duplicate_epic_ids.each do |dup|
 | 
			
		||||
      Epic.where(iid: dup['iid'], group_id: dup['group_id']).last(dup['count'] - 1).each do |epic|
 | 
			
		||||
        new_iid = InternalId.generate_next(epic,
 | 
			
		||||
          { namespace_id: epic.group_id },
 | 
			
		||||
          :epics, ->(instance, _) { instance.class.where(group_id: epic.group_id).maximum(:iid) }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        epic.update!(iid: new_iid)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    add_concurrent_index :epics, [:group_id, :iid], unique: true, name: INDEX_NAME
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down
 | 
			
		||||
    # only remove the index, as we do not want to create the duplicates back
 | 
			
		||||
    remove_concurrent_index :epics, [:group_id, :iid], name: INDEX_NAME
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
b614435cdb654ebbd11bcc5ac0ed69352219e51b368d8f10c0b2998c5258caf9
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
decdc314dbcf6b8ac2ce140f81f9d342efca0d98bbeff10c7a041568a67b63f3
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
f6e4e62dbd992fc8283f3d7872bb33f1b6bea1b366806caf8f7a65140584c0c1
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
4168c39fe93b1c11d8080e07167f79c8234c74a7b274332174d9e861f2084ada
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
f5705da7bce46d98ca798c85f08d8a6a0577839aabacd0ba9b50e0b7351a4e96
 | 
			
		||||
| 
						 | 
				
			
			@ -9342,6 +9342,8 @@ CREATE TABLE application_settings (
 | 
			
		|||
    domain_denylist text,
 | 
			
		||||
    domain_allowlist text,
 | 
			
		||||
    new_user_signups_cap integer,
 | 
			
		||||
    encrypted_cloud_license_auth_token text,
 | 
			
		||||
    encrypted_cloud_license_auth_token_iv text,
 | 
			
		||||
    CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
 | 
			
		||||
    CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
 | 
			
		||||
    CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
 | 
			
		||||
| 
						 | 
				
			
			@ -9351,7 +9353,8 @@ CREATE TABLE application_settings (
 | 
			
		|||
    CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
 | 
			
		||||
    CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
 | 
			
		||||
    CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)),
 | 
			
		||||
    CONSTRAINT check_e5aba18f02 CHECK ((char_length(container_registry_version) <= 255))
 | 
			
		||||
    CONSTRAINT check_e5aba18f02 CHECK ((char_length(container_registry_version) <= 255)),
 | 
			
		||||
    CONSTRAINT check_ef6176834f CHECK ((char_length(encrypted_cloud_license_auth_token_iv) <= 255))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE SEQUENCE application_settings_id_seq
 | 
			
		||||
| 
						 | 
				
			
			@ -13192,6 +13195,9 @@ CREATE TABLE jira_tracker_data (
 | 
			
		|||
    project_key text,
 | 
			
		||||
    issues_enabled boolean DEFAULT false NOT NULL,
 | 
			
		||||
    deployment_type smallint DEFAULT 0 NOT NULL,
 | 
			
		||||
    vulnerabilities_issuetype text,
 | 
			
		||||
    vulnerabilities_enabled boolean DEFAULT false NOT NULL,
 | 
			
		||||
    CONSTRAINT check_0bf84b76e9 CHECK ((char_length(vulnerabilities_issuetype) <= 255)),
 | 
			
		||||
    CONSTRAINT check_214cf6a48b CHECK ((char_length(project_key) <= 255))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20735,6 +20741,8 @@ CREATE INDEX index_epics_on_group_id ON epics USING btree (group_id);
 | 
			
		|||
 | 
			
		||||
CREATE UNIQUE INDEX index_epics_on_group_id_and_external_key ON epics USING btree (group_id, external_key) WHERE (external_key IS NOT NULL);
 | 
			
		||||
 | 
			
		||||
CREATE UNIQUE INDEX index_epics_on_group_id_and_iid ON epics USING btree (group_id, iid);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX index_epics_on_group_id_and_iid_varchar_pattern ON epics USING btree (group_id, ((iid)::character varying) varchar_pattern_ops);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX index_epics_on_iid ON epics USING btree (iid);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24036,9 +24036,6 @@ msgstr ""
 | 
			
		|||
msgid "SecurityReports|Scan details"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "SecurityReports|Scanner"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "SecurityReports|Security Dashboard"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -83,6 +83,8 @@ FactoryBot.define do
 | 
			
		|||
      jira_issue_transition_id { '56-1' }
 | 
			
		||||
      issues_enabled { false }
 | 
			
		||||
      project_key { nil }
 | 
			
		||||
      vulnerabilities_enabled { false }
 | 
			
		||||
      vulnerabilities_issuetype { nil }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    before(:create) do |service, evaluator|
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +92,8 @@ FactoryBot.define do
 | 
			
		|||
        create(:jira_tracker_data, service: service,
 | 
			
		||||
               url: evaluator.url, api_url: evaluator.api_url, jira_issue_transition_id: evaluator.jira_issue_transition_id,
 | 
			
		||||
               username: evaluator.username, password: evaluator.password, issues_enabled: evaluator.issues_enabled,
 | 
			
		||||
               project_key: evaluator.project_key
 | 
			
		||||
               project_key: evaluator.project_key, vulnerabilities_enabled: evaluator.vulnerabilities_enabled,
 | 
			
		||||
               vulnerabilities_issuetype: evaluator.vulnerabilities_issuetype
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ describe('Issuable component', () => {
 | 
			
		|||
  let DateOrig;
 | 
			
		||||
  let wrapper;
 | 
			
		||||
 | 
			
		||||
  const factory = (props = {}, scopedLabels = false) => {
 | 
			
		||||
  const factory = (props = {}, scopedLabelsAvailable = false) => {
 | 
			
		||||
    wrapper = shallowMount(Issuable, {
 | 
			
		||||
      propsData: {
 | 
			
		||||
        issuable: simpleIssue,
 | 
			
		||||
| 
						 | 
				
			
			@ -46,9 +46,7 @@ describe('Issuable component', () => {
 | 
			
		|||
        ...props,
 | 
			
		||||
      },
 | 
			
		||||
      provide: {
 | 
			
		||||
        glFeatures: {
 | 
			
		||||
          scopedLabels,
 | 
			
		||||
        },
 | 
			
		||||
        scopedLabelsAvailable,
 | 
			
		||||
      },
 | 
			
		||||
      stubs: {
 | 
			
		||||
        'gl-sprintf': GlSprintf,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,13 +34,13 @@ RSpec.describe Resolvers::MergeRequestsResolver do
 | 
			
		|||
 | 
			
		||||
    context 'no arguments' do
 | 
			
		||||
      it 'returns all merge requests' do
 | 
			
		||||
        result = resolve_mr(project, {})
 | 
			
		||||
        result = resolve_mr(project)
 | 
			
		||||
 | 
			
		||||
        expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns only merge requests that the current user can see' do
 | 
			
		||||
        result = resolve_mr(project, {}, user: build(:user))
 | 
			
		||||
        result = resolve_mr(project, user: build(:user))
 | 
			
		||||
 | 
			
		||||
        expect(result).to be_empty
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -236,10 +236,10 @@ RSpec.describe Resolvers::MergeRequestsResolver do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def resolve_mr_single(project, iid)
 | 
			
		||||
    resolve_mr(project, { iids: iid }, resolver: described_class.single)
 | 
			
		||||
    resolve_mr(project, resolver: described_class.single, iids: iid)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def resolve_mr(project, args, resolver: described_class, user: current_user)
 | 
			
		||||
  def resolve_mr(project, resolver: described_class, user: current_user, **args)
 | 
			
		||||
    resolve(resolver, obj: project, args: args, ctx: { current_user: user })
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'spec_helper'
 | 
			
		||||
require Rails.root.join('db', 'post_migrate', '20201106134950_deduplicate_epic_iids.rb')
 | 
			
		||||
 | 
			
		||||
RSpec.describe DeduplicateEpicIids, :migration, schema: 20201106082723 do
 | 
			
		||||
  let(:routes) { table(:routes) }
 | 
			
		||||
  let(:epics) { table(:epics) }
 | 
			
		||||
  let(:users) { table(:users) }
 | 
			
		||||
  let(:namespaces) { table(:namespaces) }
 | 
			
		||||
 | 
			
		||||
  let!(:group) { create_group('foo') }
 | 
			
		||||
  let!(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
 | 
			
		||||
  let!(:dup_epic1) { epics.create!(iid: 1, title: 'epic 1', group_id: group.id, author_id: user.id, created_at: Time.now, updated_at: Time.now, title_html: 'any') }
 | 
			
		||||
  let!(:dup_epic2) { epics.create!(iid: 1, title: 'epic 2', group_id: group.id, author_id: user.id, created_at: Time.now, updated_at: Time.now, title_html: 'any') }
 | 
			
		||||
  let!(:dup_epic3) { epics.create!(iid: 1, title: 'epic 3', group_id: group.id, author_id: user.id, created_at: Time.now, updated_at: Time.now, title_html: 'any') }
 | 
			
		||||
 | 
			
		||||
  it 'deduplicates epic iids', :aggregate_failures do
 | 
			
		||||
    duplicate_epics_count = epics.where(iid: 1, group_id: group.id).count
 | 
			
		||||
    expect(duplicate_epics_count).to eq 3
 | 
			
		||||
 | 
			
		||||
    migrate!
 | 
			
		||||
 | 
			
		||||
    duplicate_epics_count = epics.where(iid: 1, group_id: group.id).count
 | 
			
		||||
    expect(duplicate_epics_count).to eq 1
 | 
			
		||||
    expect(dup_epic1.reload.iid).to eq 1
 | 
			
		||||
    expect(dup_epic2.reload.iid).to eq 2
 | 
			
		||||
    expect(dup_epic3.reload.iid).to eq 3
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create_group(path)
 | 
			
		||||
    namespaces.create!(name: path, path: path, type: 'Group').tap do |namespace|
 | 
			
		||||
      routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +25,7 @@ RSpec.describe MigrateDiscussionIdOnPromotedEpics do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def create_epic
 | 
			
		||||
    epics.create!(author_id: user.id, iid: 1,
 | 
			
		||||
    epics.create!(author_id: user.id, iid: epics.maximum(:iid).to_i + 1,
 | 
			
		||||
                  group_id: namespace.id,
 | 
			
		||||
                  title: 'Epic with discussion',
 | 
			
		||||
                  title_html: 'Epic with discussion')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -665,6 +665,20 @@ RSpec.describe ApplicationSetting do
 | 
			
		|||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      describe '#cloud_license_auth_token' do
 | 
			
		||||
        it { is_expected.to allow_value(nil).for(:cloud_license_auth_token) }
 | 
			
		||||
 | 
			
		||||
        it 'is encrypted' do
 | 
			
		||||
          subject.cloud_license_auth_token = 'token-from-customers-dot'
 | 
			
		||||
 | 
			
		||||
          aggregate_failures do
 | 
			
		||||
            expect(subject.encrypted_cloud_license_auth_token).to be_present
 | 
			
		||||
            expect(subject.encrypted_cloud_license_auth_token_iv).to be_present
 | 
			
		||||
            expect(subject.encrypted_cloud_license_auth_token).not_to eq(subject.cloud_license_auth_token)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'static objects external storage' do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,9 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Ci::ListConfigVariablesService do
 | 
			
		||||
  let_it_be(:project) { create(:project, :repository) }
 | 
			
		||||
  let(:service) { described_class.new(project) }
 | 
			
		||||
  let(:project) { create(:project, :repository) }
 | 
			
		||||
  let(:user) { project.creator }
 | 
			
		||||
  let(:service) { described_class.new(project, user) }
 | 
			
		||||
  let(:result) { YAML.dump(ci_config) }
 | 
			
		||||
 | 
			
		||||
  subject { service.execute(sha) }
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +39,40 @@ RSpec.describe Ci::ListConfigVariablesService do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'when config has includes' do
 | 
			
		||||
    let(:sha) { 'master' }
 | 
			
		||||
    let(:ci_config) do
 | 
			
		||||
      {
 | 
			
		||||
        include: [{ local: 'other_file.yml' }],
 | 
			
		||||
        variables: {
 | 
			
		||||
          KEY1: { value: 'val 1', description: 'description 1' }
 | 
			
		||||
        },
 | 
			
		||||
        test: {
 | 
			
		||||
          stage: 'test',
 | 
			
		||||
          script: 'echo'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      allow_next_instance_of(Repository) do |repository|
 | 
			
		||||
        allow(repository).to receive(:blob_data_at).with(sha, 'other_file.yml') do
 | 
			
		||||
          <<~HEREDOC
 | 
			
		||||
            variables:
 | 
			
		||||
              KEY2:
 | 
			
		||||
                value: 'val 2'
 | 
			
		||||
                description: 'description 2'
 | 
			
		||||
          HEREDOC
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns variable list' do
 | 
			
		||||
      expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
 | 
			
		||||
      expect(subject['KEY2']).to eq({ value: 'val 2', description: 'description 2' })
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'when sending an invalid sha' do
 | 
			
		||||
    let(:sha) { 'invalid-sha' }
 | 
			
		||||
    let(:ci_config) { nil }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when exception happens' do
 | 
			
		||||
      it 'handles OpenSSL::Cipher::CipherError' do
 | 
			
		||||
        expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
 | 
			
		||||
 | 
			
		||||
        expect { service.execute }.not_to raise_exception
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'ensures rollback when OpenSSL::Cipher::CipherError' do
 | 
			
		||||
        expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
 | 
			
		||||
        expect(service).to receive(:rollback_folder_move).and_call_original
 | 
			
		||||
 | 
			
		||||
        service.execute
 | 
			
		||||
        project.reload
 | 
			
		||||
 | 
			
		||||
        expect(project.legacy_storage?).to be_truthy
 | 
			
		||||
        expect(project.repository_read_only?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'handles Gitlab::Git::CommandError' do
 | 
			
		||||
        expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
 | 
			
		||||
 | 
			
		||||
        expect { service.execute }.not_to raise_exception
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'ensures rollback when Gitlab::Git::CommandError' do
 | 
			
		||||
        expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
 | 
			
		||||
        expect(service).to receive(:rollback_folder_move).and_call_original
 | 
			
		||||
 | 
			
		||||
        service.execute
 | 
			
		||||
        project.reload
 | 
			
		||||
 | 
			
		||||
        expect(project.legacy_storage?).to be_truthy
 | 
			
		||||
        expect(project.repository_read_only?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when one move fails' do
 | 
			
		||||
      it 'rollsback repositories to original name' do
 | 
			
		||||
        allow(service).to receive(:move_repository).and_call_original
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when exception happens' do
 | 
			
		||||
      it 'handles OpenSSL::Cipher::CipherError' do
 | 
			
		||||
        expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
 | 
			
		||||
 | 
			
		||||
        expect { service.execute }.not_to raise_exception
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'ensures rollback when OpenSSL::Cipher::CipherError' do
 | 
			
		||||
        expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
 | 
			
		||||
        expect(service).to receive(:rollback_folder_move).and_call_original
 | 
			
		||||
 | 
			
		||||
        service.execute
 | 
			
		||||
        project.reload
 | 
			
		||||
 | 
			
		||||
        expect(project.hashed_storage?(:repository)).to be_truthy
 | 
			
		||||
        expect(project.repository_read_only?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'handles Gitlab::Git::CommandError' do
 | 
			
		||||
        expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
 | 
			
		||||
 | 
			
		||||
        expect { service.execute }.not_to raise_exception
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'ensures rollback when Gitlab::Git::CommandError' do
 | 
			
		||||
        expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
 | 
			
		||||
        expect(service).to receive(:rollback_folder_move).and_call_original
 | 
			
		||||
 | 
			
		||||
        service.execute
 | 
			
		||||
        project.reload
 | 
			
		||||
 | 
			
		||||
        expect(project.hashed_storage?(:repository)).to be_truthy
 | 
			
		||||
        expect(project.repository_read_only?).to be_falsey
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when one move fails' do
 | 
			
		||||
      it 'rolls repositories back to original name' do
 | 
			
		||||
        allow(service).to receive(:move_repository).and_call_original
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue