Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e684f438e6
commit
587b778d3e
21 changed files with 251 additions and 38 deletions
|
@ -16,6 +16,7 @@ export const i18n = {
|
|||
downloadArtifact: __('Download %{name} artifact'),
|
||||
artifactSectionHeader: __('Download artifacts'),
|
||||
artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
|
||||
emptyArtifactsMessage: __('No artifacts found'),
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -99,6 +100,10 @@ export default {
|
|||
|
||||
<gl-loading-icon v-if="isLoading" size="sm" />
|
||||
|
||||
<gl-dropdown-item v-if="!artifacts.length" data-testid="artifacts-empty-message">
|
||||
{{ $options.i18n.emptyArtifactsMessage }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-for="(artifact, i) in artifacts"
|
||||
:key="i"
|
||||
|
|
|
@ -87,22 +87,21 @@ export default {
|
|||
@click="handleRetryClick"
|
||||
/>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.flags.cancelable"
|
||||
v-gl-tooltip.hover
|
||||
v-gl-modal-directive="'confirmation-modal'"
|
||||
:aria-label="$options.i18n.cancelTitle"
|
||||
:title="$options.i18n.cancelTitle"
|
||||
:loading="isCancelling"
|
||||
:disabled="isCancelling"
|
||||
icon="close"
|
||||
variant="danger"
|
||||
category="primary"
|
||||
class="js-pipelines-cancel-button"
|
||||
@click="handleCancelClick"
|
||||
/>
|
||||
|
||||
<pipeline-multi-actions :pipeline-id="pipeline.id" />
|
||||
</div>
|
||||
<gl-button
|
||||
v-if="pipeline.flags.cancelable"
|
||||
v-gl-tooltip.hover
|
||||
v-gl-modal-directive="'confirmation-modal'"
|
||||
:aria-label="$options.i18n.cancelTitle"
|
||||
:title="$options.i18n.cancelTitle"
|
||||
:loading="isCancelling"
|
||||
:disabled="isCancelling"
|
||||
icon="cancel"
|
||||
variant="danger"
|
||||
category="primary"
|
||||
class="js-pipelines-cancel-button gl-ml-1"
|
||||
@click="handleCancelClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -168,7 +168,7 @@ class Member < ApplicationRecord
|
|||
|
||||
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
|
||||
|
||||
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
||||
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? && !member.invite_accepted_at? }
|
||||
|
||||
after_create :send_invite, if: :invite?, unless: :importing?
|
||||
after_create :send_request, if: :request?, unless: :importing?
|
||||
|
|
|
@ -105,7 +105,7 @@
|
|||
- if can?(current_user, :update_build, job)
|
||||
- if job.active?
|
||||
= link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'gl-button btn btn-default btn-icon' do
|
||||
= sprite_icon('close', css_class: 'gl-icon')
|
||||
= sprite_icon('cancel', css_class: 'gl-icon')
|
||||
- elsif job.scheduled?
|
||||
.gl-button.btn.btn-default.btn-icon.disabled{ disabled: true }
|
||||
= sprite_icon('planning', css_class: 'gl-icon')
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: vulnerability_finding_tracking_signatures
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54608
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322044
|
||||
milestone: '13.11'
|
||||
type: development
|
||||
group: group::vulnerability research
|
||||
default_enabled: false
|
18
db/migrate/20210813101742_create_zentao_tracker_data.rb
Normal file
18
db/migrate/20210813101742_create_zentao_tracker_data.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateZentaoTrackerData < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :zentao_tracker_data do |t|
|
||||
t.references :integration, foreign_key: { on_delete: :cascade }, type: :bigint, index: true, null: false
|
||||
t.timestamps_with_timezone
|
||||
t.binary :encrypted_url
|
||||
t.binary :encrypted_url_iv
|
||||
t.binary :encrypted_api_url
|
||||
t.binary :encrypted_api_url_iv
|
||||
t.binary :encrypted_zentao_product_xid
|
||||
t.binary :encrypted_zentao_product_xid_iv
|
||||
t.binary :encrypted_api_token
|
||||
t.binary :encrypted_api_token_iv
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeCiSourcesPipelinesBigintConversion < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = 'ci_sources_pipelines'
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
|
||||
table_name: TABLE_NAME,
|
||||
column_name: 'id',
|
||||
job_arguments: [['source_job_id'], ['source_job_id_convert_to_bigint']]
|
||||
)
|
||||
|
||||
swap
|
||||
end
|
||||
|
||||
def down
|
||||
swap
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def swap
|
||||
# This is to replace the existing "index_ci_sources_pipelines_on_source_job_id" btree (source_job_id)
|
||||
add_concurrent_index TABLE_NAME, :source_job_id_convert_to_bigint, name: 'index_ci_sources_pipelines_on_source_job_id_convert_to_bigint'
|
||||
|
||||
# Add a foreign key on `source_job_id_convert_to_bigint` before we swap the columns and drop the old FK (fk_be5624bf37)
|
||||
add_concurrent_foreign_key TABLE_NAME, :ci_builds,
|
||||
column: :source_job_id_convert_to_bigint, on_delete: :cascade,
|
||||
name: 'fk_be5624bf37_tmp', reverse_lock_order: true
|
||||
|
||||
with_lock_retries(raise_on_exhaustion: true) do
|
||||
# We'll need ACCESS EXCLUSIVE lock on the related tables,
|
||||
# lets make sure it can be acquired from the start
|
||||
execute "LOCK TABLE ci_builds, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"
|
||||
|
||||
# Swap column names
|
||||
temp_name = 'source_job_id_tmp'
|
||||
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:source_job_id)} TO #{quote_column_name(temp_name)}"
|
||||
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:source_job_id_convert_to_bigint)} TO #{quote_column_name(:source_job_id)}"
|
||||
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(temp_name)} TO #{quote_column_name(:source_job_id_convert_to_bigint)}"
|
||||
|
||||
# We need to update the trigger function in order to make PostgreSQL to
|
||||
# regenerate the execution plan for it. This is to avoid type mismatch errors like
|
||||
# "type of parameter 15 (bigint) does not match that when preparing the plan (integer)"
|
||||
function_name = Gitlab::Database::UnidirectionalCopyTrigger.on_table(TABLE_NAME).name(:source_job_id, :source_job_id_convert_to_bigint)
|
||||
execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
|
||||
|
||||
# No need to swap defaults, both columns have no default value
|
||||
|
||||
# Rename the index on the `bigint` column to match the new column name
|
||||
# (we already hold an exclusive lock, so no need to use DROP INDEX CONCURRENTLY here)
|
||||
execute 'DROP INDEX index_ci_sources_pipelines_on_source_job_id'
|
||||
rename_index TABLE_NAME, 'index_ci_sources_pipelines_on_source_job_id_convert_to_bigint', 'index_ci_sources_pipelines_on_source_job_id'
|
||||
|
||||
# Drop original FK on the old int4 `source_job_id` (fk_be5624bf37)
|
||||
remove_foreign_key TABLE_NAME, name: 'fk_be5624bf37'
|
||||
# We swapped the columns but the FK is still using the temporary name
|
||||
# So we have to also swap the FK name now that we dropped the other one
|
||||
rename_constraint(TABLE_NAME, 'fk_be5624bf37_tmp', 'fk_be5624bf37')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OrphanedInviteTokensCleanup < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
TMP_INDEX_NAME = 'tmp_idx_orphaned_invite_tokens'
|
||||
QUERY_CONDITION = "invite_token IS NOT NULL and invite_accepted_at IS NOT NULL and invite_accepted_at < created_at"
|
||||
|
||||
def up
|
||||
membership = define_batchable_model('members')
|
||||
|
||||
add_concurrent_index('members', :id, where: QUERY_CONDITION, name: TMP_INDEX_NAME)
|
||||
|
||||
membership.where(QUERY_CONDITION).pluck(:id).each_slice(10) do |group|
|
||||
membership.where(id: group).where(QUERY_CONDITION).update_all(invite_token: nil)
|
||||
end
|
||||
|
||||
remove_concurrent_index_by_name('members', TMP_INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name('members', TMP_INDEX_NAME) if index_exists_by_name?('members', TMP_INDEX_NAME)
|
||||
|
||||
# This migration is irreversible
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210713042153
Normal file
1
db/schema_migrations/20210713042153
Normal file
|
@ -0,0 +1 @@
|
|||
95aa786acfbd53f1e819e49c6c6ec703d609396f2cc04a5791d1ffc77800dc72
|
1
db/schema_migrations/20210809123658
Normal file
1
db/schema_migrations/20210809123658
Normal file
|
@ -0,0 +1 @@
|
|||
f4a1963c8f21b8c767766c3a18037bae223efce8452c87f570cf9789d6f666d6
|
1
db/schema_migrations/20210813101742
Normal file
1
db/schema_migrations/20210813101742
Normal file
|
@ -0,0 +1 @@
|
|||
4401423fc602a6d5df95857282bb84851fa99ddeb6c3d1ce4614f12c1dc4ec87
|
|
@ -11269,9 +11269,9 @@ CREATE TABLE ci_sources_pipelines (
|
|||
project_id integer,
|
||||
pipeline_id integer,
|
||||
source_project_id integer,
|
||||
source_job_id integer,
|
||||
source_job_id_convert_to_bigint integer,
|
||||
source_pipeline_id integer,
|
||||
source_job_id_convert_to_bigint bigint
|
||||
source_job_id bigint
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ci_sources_pipelines_id_seq
|
||||
|
@ -19948,6 +19948,30 @@ CREATE SEQUENCE x509_issuers_id_seq
|
|||
|
||||
ALTER SEQUENCE x509_issuers_id_seq OWNED BY x509_issuers.id;
|
||||
|
||||
CREATE TABLE zentao_tracker_data (
|
||||
id bigint NOT NULL,
|
||||
integration_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
encrypted_url bytea,
|
||||
encrypted_url_iv bytea,
|
||||
encrypted_api_url bytea,
|
||||
encrypted_api_url_iv bytea,
|
||||
encrypted_zentao_product_xid bytea,
|
||||
encrypted_zentao_product_xid_iv bytea,
|
||||
encrypted_api_token bytea,
|
||||
encrypted_api_token_iv bytea
|
||||
);
|
||||
|
||||
CREATE SEQUENCE zentao_tracker_data_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE zentao_tracker_data_id_seq OWNED BY zentao_tracker_data.id;
|
||||
|
||||
CREATE TABLE zoom_meetings (
|
||||
id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
|
@ -20885,6 +20909,8 @@ ALTER TABLE ONLY x509_commit_signatures ALTER COLUMN id SET DEFAULT nextval('x50
|
|||
|
||||
ALTER TABLE ONLY x509_issuers ALTER COLUMN id SET DEFAULT nextval('x509_issuers_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY zentao_tracker_data ALTER COLUMN id SET DEFAULT nextval('zentao_tracker_data_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY zoom_meetings ALTER COLUMN id SET DEFAULT nextval('zoom_meetings_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY product_analytics_events_experimental
|
||||
|
@ -22654,6 +22680,9 @@ ALTER TABLE ONLY x509_commit_signatures
|
|||
ALTER TABLE ONLY x509_issuers
|
||||
ADD CONSTRAINT x509_issuers_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY zentao_tracker_data
|
||||
ADD CONSTRAINT zentao_tracker_data_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY zoom_meetings
|
||||
ADD CONSTRAINT zoom_meetings_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -25627,6 +25656,8 @@ CREATE INDEX index_x509_commit_signatures_on_x509_certificate_id ON x509_commit_
|
|||
|
||||
CREATE INDEX index_x509_issuers_on_subject_key_identifier ON x509_issuers USING btree (subject_key_identifier);
|
||||
|
||||
CREATE INDEX index_zentao_tracker_data_on_integration_id ON zentao_tracker_data USING btree (integration_id);
|
||||
|
||||
CREATE INDEX index_zoom_meetings_on_issue_id ON zoom_meetings USING btree (issue_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_zoom_meetings_on_issue_id_and_issue_status ON zoom_meetings USING btree (issue_id, issue_status) WHERE (issue_status = 1);
|
||||
|
@ -27673,6 +27704,9 @@ ALTER TABLE ONLY required_code_owners_sections
|
|||
ALTER TABLE ONLY dast_site_profiles
|
||||
ADD CONSTRAINT fk_rails_83e309d69e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY zentao_tracker_data
|
||||
ADD CONSTRAINT fk_rails_84efda7be0 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY boards_epic_user_preferences
|
||||
ADD CONSTRAINT fk_rails_851fe1510a FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -22,8 +22,8 @@ module API
|
|||
end
|
||||
|
||||
def feature_enabled?
|
||||
::Feature.enabled?(:integrated_error_tracking, project) &&
|
||||
project.error_tracking_setting&.enabled?
|
||||
project.error_tracking_setting&.enabled? &&
|
||||
project.error_tracking_setting&.integrated_client?
|
||||
end
|
||||
|
||||
def find_client_key(public_key)
|
||||
|
|
|
@ -15,10 +15,7 @@ module Gitlab
|
|||
@base_report = base_report
|
||||
@head_report = head_report
|
||||
|
||||
@signatures_enabled = (
|
||||
::Feature.enabled?(:vulnerability_finding_tracking_signatures, project) &&
|
||||
project.licensed_feature_available?(:vulnerability_finding_signatures)
|
||||
)
|
||||
@signatures_enabled = project.licensed_feature_available?(:vulnerability_finding_signatures)
|
||||
|
||||
if @signatures_enabled
|
||||
@added_findings = []
|
||||
|
|
|
@ -22520,6 +22520,9 @@ msgstr ""
|
|||
msgid "No approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "No artifacts found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No assignee"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -12,5 +12,9 @@ FactoryBot.define do
|
|||
trait :disabled do
|
||||
enabled { false }
|
||||
end
|
||||
|
||||
trait :integrated do
|
||||
integrated { true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,6 +53,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
|
|||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId);
|
||||
const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId);
|
||||
const findEmptyMessage = () => wrapper.findByTestId('artifacts-empty-message');
|
||||
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
|
@ -86,6 +87,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
|
|||
createComponent({ mockData: { artifacts } });
|
||||
|
||||
expect(findAllArtifactItems()).toHaveLength(artifacts.length);
|
||||
expect(findEmptyMessage().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should render the correct artifact name and path', () => {
|
||||
|
@ -95,6 +97,12 @@ describe('Pipeline Multi Actions Dropdown', () => {
|
|||
expect(findFirstArtifactItem().text()).toBe(`Download ${artifacts[0].name} artifact`);
|
||||
});
|
||||
|
||||
it('should render empty message when no artifacts are found', () => {
|
||||
createComponent({ mockData: { artifacts: [] } });
|
||||
|
||||
expect(findEmptyMessage().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('with a failing request', () => {
|
||||
it('should render an error message', async () => {
|
||||
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
|
||||
|
|
|
@ -24,12 +24,11 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
|
|||
|
||||
subject { described_class.new(project, base_report, head_report) }
|
||||
|
||||
where(vulnerability_finding_tracking_signatures_enabled: [true, false])
|
||||
where(vulnerability_finding_signatures: [true, false])
|
||||
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(vulnerability_finding_tracking_signatures: vulnerability_finding_tracking_signatures_enabled)
|
||||
stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_tracking_signatures_enabled)
|
||||
stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_signatures)
|
||||
end
|
||||
|
||||
describe '#base_report_out_of_date' do
|
||||
|
|
32
spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
Normal file
32
spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration! 'orphaned_invite_tokens_cleanup'
|
||||
|
||||
RSpec.describe OrphanedInviteTokensCleanup, :migration do
|
||||
def create_member(**extra_attributes)
|
||||
defaults = {
|
||||
access_level: 10,
|
||||
source_id: 1,
|
||||
source_type: "Project",
|
||||
notification_level: 0,
|
||||
type: 'ProjectMember'
|
||||
}
|
||||
|
||||
table(:members).create!(defaults.merge(extra_attributes))
|
||||
end
|
||||
|
||||
describe '#up', :aggregate_failures do
|
||||
it 'removes invite tokens for accepted records with invite_accepted_at < created_at' do
|
||||
record1 = create_member(invite_token: 'foo', invite_accepted_at: 1.day.ago, created_at: 1.hour.ago)
|
||||
record2 = create_member(invite_token: 'foo2', invite_accepted_at: nil, created_at: 1.hour.ago)
|
||||
record3 = create_member(invite_token: 'foo3', invite_accepted_at: 1.day.ago, created_at: 1.year.ago)
|
||||
|
||||
migrate!
|
||||
|
||||
expect(table(:members).find(record1.id).invite_token).to eq nil
|
||||
expect(table(:members).find(record2.id).invite_token).to eq 'foo2'
|
||||
expect(table(:members).find(record3.id).invite_token).to eq 'foo3'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -667,7 +667,23 @@ RSpec.describe Member do
|
|||
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
|
||||
|
||||
it "sets the invite token" do
|
||||
expect { member.generate_invite_token }.to change { member.invite_token}
|
||||
expect { member.generate_invite_token }.to change { member.invite_token }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'generate invite token on create' do
|
||||
let!(:member) { build(:project_member, invite_email: "user@example.com") }
|
||||
|
||||
it "sets the invite token" do
|
||||
expect { member.save! }.to change { member.invite_token }.to(kind_of(String))
|
||||
end
|
||||
|
||||
context 'when invite was already accepted' do
|
||||
it "does not set invite token" do
|
||||
member.invite_accepted_at = 1.day.ago
|
||||
|
||||
expect { member.save! }.not_to change { member.invite_token }.from(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe API::ErrorTrackingCollector do
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let_it_be(:setting) { create(:project_error_tracking_setting, project: project) }
|
||||
let_it_be(:setting) { create(:project_error_tracking_setting, :integrated, project: project) }
|
||||
let_it_be(:client_key) { create(:error_tracking_client_key, project: project) }
|
||||
|
||||
describe "POST /error_tracking/collector/api/:id/envelope" do
|
||||
|
@ -40,6 +40,14 @@ RSpec.describe API::ErrorTrackingCollector do
|
|||
it_behaves_like 'not found'
|
||||
end
|
||||
|
||||
context 'integrated error tracking is disabled' do
|
||||
before do
|
||||
setting.update!(integrated: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not found'
|
||||
end
|
||||
|
||||
context 'feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(integrated_error_tracking: false)
|
||||
|
|
Loading…
Reference in a new issue