Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-23 06:07:44 +00:00
parent 5316a9bca9
commit 35b0f1fe13
27 changed files with 398 additions and 74 deletions

View File

@ -260,6 +260,8 @@
- ".gitlab/ci/**/*"
- "*_VERSION"
- "scripts/rspec_helpers.sh"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
# DB patterns + .ci-patterns
.db-patterns: &db-patterns
@ -314,10 +316,11 @@
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "data/whats_new/*.yml"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
# .code-patterns + .backstage-patterns
.code-backstage-patterns: &code-backstage-patterns
@ -338,7 +341,6 @@
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "data/whats_new/*.yml"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
@ -349,6 +351,8 @@
- "{,ee/,jh/}rubocop/**/*"
- "{,ee/,jh/}spec/**/*"
- "{,spec/}tooling/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
# .code-patterns + .qa-patterns
.code-qa-patterns: &code-qa-patterns
@ -369,13 +373,14 @@
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "data/whats_new/*.yml"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
# QA changes
- ".dockerignore"
- "qa/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
# .code-patterns + .backstage-patterns + .qa-patterns
.code-backstage-qa-patterns: &code-backstage-qa-patterns
@ -396,7 +401,6 @@
- "config.ru"
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
- "data/whats_new/*.yml"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
@ -410,6 +414,8 @@
# QA changes
- ".dockerignore"
- "qa/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
# .code-backstage-qa-patterns + .workhorse-patterns
.setup-test-env-patterns: &setup-test-env-patterns

View File

@ -1,16 +1,10 @@
<script>
import {
GlSafeHtmlDirective as SafeHtml,
GlModal,
GlModalDirective,
GlPopover,
GlButton,
} from '@gitlab/ui';
import { GlSafeHtmlDirective as SafeHtml, GlModal, GlTooltip, GlModalDirective } from '@gitlab/ui';
import $ from 'jquery';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import TaskList from '~/task_list';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -25,9 +19,8 @@ export default {
},
components: {
GlModal,
GlPopover,
CreateWorkItem,
GlButton,
GlTooltip,
WorkItemDetailModal,
},
mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
@ -216,9 +209,11 @@ export default {
this.taskButtons.push(button.id);
button.innerHTML = `
<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14">
<use href="${gon.sprite_icons}#ellipsis_v"></use>
<use href="${gon.sprite_icons}#doc-new"></use>
</svg>
`;
button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
button.addEventListener('click', () => this.openCreateTaskModal(button.id));
item.prepend(button);
});
},
@ -246,9 +241,6 @@ export default {
this.$emit('updateDescription', description);
this.closeCreateTaskModal();
},
focusButton() {
this.$refs.convertButton[0].$el.focus();
},
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] },
};
@ -309,23 +301,9 @@ export default {
@error="handleWorkItemDetailModalError"
/>
<template v-if="workItemsEnabled">
<gl-popover
v-for="item in taskButtons"
:key="item"
:target="item"
placement="top"
triggers="focus"
@shown="focusButton"
>
<gl-button
ref="convertButton"
variant="link"
data-testid="convert-to-task"
class="gl-text-gray-900! gl-text-decoration-none! gl-outline-0!"
@click="openCreateTaskModal(item)"
>{{ s__('WorkItem|Convert to work item') }}</gl-button
>
</gl-popover>
<gl-tooltip v-for="item in taskButtons" :key="item" :target="item">
{{ s__('WorkItem|Convert to work item') }}
</gl-tooltip>
</template>
</div>
</template>

View File

@ -87,10 +87,7 @@ export default {
return this.selectedWorkItemType?.name || s__('WorkItem|Type');
},
formOptions() {
return [
{ value: null, text: s__('WorkItem|Please select work item type') },
...this.workItemTypes,
];
return [{ value: null, text: s__('WorkItem|Select type') }, ...this.workItemTypes];
},
isButtonDisabled() {
return this.title.trim().length === 0 || !this.selectedWorkItemType;

View File

@ -311,7 +311,7 @@ ul.related-merge-requests > li gl-emoji {
.description.work-items-enabled {
ul.task-list {
> li.task-list-item {
padding-inline-start: 2.25rem;
padding-inline-start: 2.5rem;
.js-add-task {
svg {
@ -324,7 +324,7 @@ ul.related-merge-requests > li gl-emoji {
}
> input.task-list-item-checkbox {
left: 0.875rem;
left: 1.25rem;
}
&:hover,

View File

@ -196,6 +196,27 @@ class ApplicationController < ActionController::Base
end
end
# Devise defines current_user to be:
#
# def current_user
# @current_user ||= warden.authenticate(scope: mapping)
# end
#
# That means whenever current_user is called and `@current_user` is
# nil, Warden will attempt to authenticate a user. To avoid
# reauthenticating anonymous users, we may need to invalidate
# the user.
def reset_auth_user!
return if strong_memoized?(:auth_user) && auth_user
# Controllers usually call auth_user first, but for some controllers
# authenticate_sessionless_user! is called after that. If we relied
# on the memoized auth_user, the value would always be nil for
# sessionless users.
clear_memoization(:auth_user)
auth_user
end
def log_exception(exception)
# At this point, the controller already exits set_current_context around
# block. To maintain the context while handling error exception, we need to

View File

@ -20,16 +20,21 @@ module SessionlessAuthentication
end
def sessionless_sign_in(user)
if user.can_log_in_with_non_expired_password?
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
# sign in token, you can simply remove store: false.
sign_in(user, store: false, message: :sessionless_sign_in)
elsif request_authenticator.can_sign_in_bot?(user)
# we suppress callbacks to avoid redirecting the bot
sign_in(user, store: false, message: :sessionless_sign_in, run_callbacks: false)
end
signed_in_user =
if user.can_log_in_with_non_expired_password?
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
# sign in token, you can simply remove store: false.
sign_in(user, store: false, message: :sessionless_sign_in)
elsif request_authenticator.can_sign_in_bot?(user)
# we suppress callbacks to avoid redirecting the bot
sign_in(user, store: false, message: :sessionless_sign_in, run_callbacks: false)
end
reset_auth_user! if respond_to?(:reset_auth_user!, true)
signed_in_user
end
def sessionless_bypass_admin_mode!(&block)

View File

@ -5,6 +5,7 @@ module Mutations
module CanaryIngress
class Update < ::Mutations::BaseMutation
graphql_name 'EnvironmentsCanaryIngressUpdate'
description '**Deprecated** This endpoint is planned to be removed along with certificate-based clusters. [See this epic](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) for more information.'
authorize :update_environment
@ -18,7 +19,13 @@ module Mutations
required: true,
description: 'Weight of the Canary Ingress.'
REMOVAL_ERR_MSG = 'This endpoint was deactivated as part of the certificate-based' \
'kubernetes integration removal. See Epic:' \
'https://gitlab.com/groups/gitlab-org/configure/-/epics/8'
def resolve(id:, **kwargs)
return { errors: [REMOVAL_ERR_MSG] } if cert_based_clusters_ff_disabled?
environment = authorized_find!(id: id)
result = ::Environments::CanaryIngress::UpdateService
@ -33,6 +40,12 @@ module Mutations
id = ::Types::GlobalIDType[::Environment].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
private
def cert_based_clusters_ff_disabled?
Feature.disabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
end
end
end
end

View File

@ -51,7 +51,7 @@ class EnvironmentStatus
def deployment
strong_memoize(:deployment) do
Deployment.where(environment: environment).find_by_sha(sha)
Deployment.where(environment: environment).ordered.find_by_sha(sha)
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class RemoveIssuesWorkItemTypeIdIndex < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_issues_on_work_item_type_id'
def up
remove_concurrent_index_by_name :issues, name: INDEX_NAME
end
def down
add_concurrent_index :issues, :work_item_type_id, name: INDEX_NAME
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddTemporaryIssueTypeIndexForWorkItemTypes < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'tmp_index_issues_on_issue_type_and_id'
def up
add_concurrent_index :issues, [:issue_type, :id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :issues, INDEX_NAME
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
class BackfillWorkItemTypeIdOnIssues < Gitlab::Database::Migration[1.0]
MIGRATION = 'BackfillWorkItemTypeIdForIssues'
BATCH_SIZE = 10_000
MAX_BATCH_SIZE = 30_000
SUB_BATCH_SIZE = 100
INTERVAL = 2.minutes
class MigrationWorkItemType < ApplicationRecord
self.table_name = 'work_item_types'
def self.id_by_type
where(namespace_id: nil).order(:base_type).pluck(:base_type, :id).to_h
end
end
def up
# We expect no more than 5 types. Only 3 of them are expected to have associated issues at the moment
MigrationWorkItemType.id_by_type.each do |base_type, type_id|
queue_batched_background_migration(
MIGRATION,
:issues,
:id,
base_type,
type_id,
job_interval: INTERVAL,
batch_size: BATCH_SIZE,
max_batch_size: MAX_BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
end
def down
Gitlab::Database::BackgroundMigration::BatchedMigration.where(job_class_name: MIGRATION).delete_all
end
end

View File

@ -0,0 +1 @@
1276e86db3e2922cd1535e98f17768d1ab0dd653317f918fb8e6819760893a47

View File

@ -0,0 +1 @@
8ab13f76037450704a29220ca6af0174b373ce743620952e3bc197c952d00c1d

View File

@ -0,0 +1 @@
7dc3d314e76f26a539c0aa009bd45ea6ccc9c6108f798728909fd4b18e3c31b9

View File

@ -27961,8 +27961,6 @@ CREATE INDEX index_issues_on_updated_at ON issues USING btree (updated_at);
CREATE INDEX index_issues_on_updated_by_id ON issues USING btree (updated_by_id) WHERE (updated_by_id IS NOT NULL);
CREATE INDEX index_issues_on_work_item_type_id ON issues USING btree (work_item_type_id);
CREATE INDEX index_iterations_cadences_on_group_id ON iterations_cadences USING btree (group_id);
CREATE UNIQUE INDEX index_jira_connect_installations_on_client_key ON jira_connect_installations USING btree (client_key);
@ -29615,6 +29613,8 @@ CREATE INDEX tmp_index_for_namespace_id_migration_on_group_members ON members US
CREATE INDEX tmp_index_for_namespace_id_migration_on_routes ON routes USING btree (id) WHERE ((namespace_id IS NULL) AND ((source_type)::text = 'Namespace'::text));
CREATE INDEX tmp_index_issues_on_issue_type_and_id ON issues USING btree (issue_type, id);
CREATE INDEX tmp_index_members_on_state ON members USING btree (state) WHERE (state = 2);
CREATE INDEX tmp_index_namespaces_empty_traversal_ids_with_child_namespaces ON namespaces USING btree (id) WHERE ((parent_id IS NOT NULL) AND (traversal_ids = '{}'::integer[]));

View File

@ -2372,6 +2372,8 @@ Input type: `EnableDevopsAdoptionNamespaceInput`
### `Mutation.environmentsCanaryIngressUpdate`
**Deprecated** This endpoint is planned to be removed along with certificate-based clusters. [See this epic](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) for more information.
Input type: `EnvironmentsCanaryIngressUpdateInput`
#### Arguments

View File

@ -23,7 +23,7 @@ Otherwise, to add your license:
1. Select the **Terms of Service** checkbox.
1. Select **Add license**.
## Add your license during installation
## Add your license file during installation
You can import a license file when you install GitLab.

View File

@ -0,0 +1,73 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Backfills the `issues.work_item_type_id` column, replacing any
# instances of `NULL` with the appropriate `work_item_types.id` based on `issues.issue_type`
class BackfillWorkItemTypeIdForIssues
# Basic AR model for issues table
class MigrationIssue < ApplicationRecord
include ::EachBatch
self.table_name = 'issues'
scope :base_query, ->(base_type) { where(work_item_type_id: nil, issue_type: base_type) }
end
MAX_UPDATE_RETRIES = 3
def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms, base_type, base_type_id)
parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id, base_type)
parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first
# The query need to be reconstructed because .each_batch modifies the default scope
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510
reconstructed_sub_batch = MigrationIssue.unscoped.base_query(base_type).where(id: first..last)
batch_metrics.time_operation(:update_all) do
update_with_retry(reconstructed_sub_batch, base_type_id)
end
pause_ms = 0 if pause_ms < 0
sleep(pause_ms * 0.001)
end
end
def batch_metrics
@batch_metrics ||= Gitlab::Database::BackgroundMigration::BatchMetrics.new
end
private
# Retry mechanism required as update statements on the issues table will randomly take longer than
# expected due to gin indexes https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71869#note_775796352
def update_with_retry(sub_batch, base_type_id)
update_attempt = 1
begin
update_batch(sub_batch, base_type_id)
rescue ActiveRecord::StatementTimeout => e
update_attempt += 1
if update_attempt <= MAX_UPDATE_RETRIES
# sleeping 30 seconds as it might take a long time to clean the gin index pending list
sleep(30)
retry
end
raise e
end
end
def update_batch(sub_batch, base_type_id)
sub_batch.update_all(work_item_type_id: base_type_id)
end
def relation_scoped_to_range(source_table, source_key_column, start_id, end_id, base_type)
MigrationIssue.where(source_key_column => start_id..end_id).base_query(base_type)
end
end
end
end

View File

@ -42374,7 +42374,7 @@ msgstr ""
msgid "WorkItem|New Task"
msgstr ""
msgid "WorkItem|Please select work item type"
msgid "WorkItem|Select type"
msgstr ""
msgid "WorkItem|Something went wrong when creating a work item. Please try again"

View File

@ -5,4 +5,7 @@ require_relative '../spec/simplecov_env'
SimpleCovEnv.configure_profile
SimpleCovEnv.configure_formatter
SimpleCov.collate Dir.glob(File.join(SimpleCov.coverage_path, '*', '.resultset.json'))
resultset_files = Dir.glob(File.join(SimpleCov.coverage_path, '*', '.resultset.json'))
exit(0) if resultset_files.empty?
SimpleCov.collate(resultset_files)

View File

@ -134,6 +134,12 @@ RSpec.describe GraphqlController do
post :execute
end
it "assigns username in ApplicationContext" do
post :execute
expect(Gitlab::ApplicationContext.current).to include('meta.user' => user.username)
end
end
context 'when user uses an API token' do
@ -189,6 +195,12 @@ RSpec.describe GraphqlController do
expect(assigns(:context)[:is_sessionless_user]).to be true
end
it "assigns username in ApplicationContext" do
subject
expect(Gitlab::ApplicationContext.current).to include('meta.user' => user.username)
end
it 'calls the track api when trackable method' do
agent = 'vs-code-gitlab-workflow/3.11.1 VSCode/1.52.1 Node.js/12.14.1 (darwin; x64)'
request.env['HTTP_USER_AGENT'] = agent
@ -222,6 +234,12 @@ RSpec.describe GraphqlController do
expect(assigns(:context)[:is_sessionless_user]).to be false
end
it "does not assign a username in ApplicationContext" do
subject
expect(Gitlab::ApplicationContext.current.key?('meta.user')).to be false
end
end
it 'includes request object in context' do

View File

@ -10,6 +10,10 @@ RSpec.describe 'Database schema' do
let(:tables) { connection.tables }
let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb }
IGNORED_INDEXES_ON_FKS = {
issues: %w[work_item_type_id]
}.with_indifferent_access.freeze
# List of columns historically missing a FK, don't add more columns
# See: https://docs.gitlab.com/ee/development/foreign_keys.html#naming-foreign-keys
IGNORED_FK_COLUMNS = {
@ -115,6 +119,7 @@ RSpec.describe 'Database schema' do
columns.first.chomp
end
foreign_keys_columns = all_foreign_keys.map(&:column)
required_indexed_columns = foreign_keys_columns - ignored_index_columns(table)
# Add the primary key column to the list of indexed columns because
# postgres and mysql both automatically create an index on the primary
@ -122,7 +127,7 @@ RSpec.describe 'Database schema' do
# automatically generated indexes (like the primary key index).
first_indexed_column.push(primary_key_column)
expect(first_indexed_column.uniq).to include(*foreign_keys_columns)
expect(first_indexed_column.uniq).to include(*required_indexed_columns)
end
end
@ -305,8 +310,12 @@ RSpec.describe 'Database schema' do
@models_by_table_name ||= ApplicationRecord.descendants.reject(&:abstract_class).group_by(&:table_name)
end
def ignored_fk_columns(column)
IGNORED_FK_COLUMNS.fetch(column, [])
def ignored_fk_columns(table)
IGNORED_FK_COLUMNS.fetch(table, [])
end
def ignored_index_columns(table)
IGNORED_INDEXES_ON_FKS.fetch(table, [])
end
def ignored_limit_enums(model)

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import { nextTick } from 'vue';
import '~/behaviors/markdown/render_gfm';
import { GlPopover, GlModal } from '@gitlab/ui';
import { GlTooltip, GlModal } from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
import { mockTracking } from 'helpers/tracking_helper';
@ -29,9 +29,9 @@ describe('Description component', () => {
const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]');
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
const findTaskActionButtons = () => wrapper.findAll('.js-add-task');
const findConvertToTaskButton = () => wrapper.find('[data-testid="convert-to-task"]');
const findConvertToTaskButton = () => wrapper.find('.js-add-task');
const findPopovers = () => wrapper.findAllComponents(GlPopover);
const findTooltips = () => wrapper.findAllComponents(GlTooltip);
const findModal = () => wrapper.findComponent(GlModal);
const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
@ -51,7 +51,6 @@ describe('Description component', () => {
hide: hideModal,
},
}),
GlPopover,
},
});
}
@ -254,9 +253,9 @@ describe('Description component', () => {
expect(findTaskActionButtons()).toHaveLength(3);
});
it('renders a list of popovers corresponding to checkboxes in description HTML', () => {
expect(findPopovers()).toHaveLength(3);
expect(findPopovers().at(0).props('target')).toBe(
it('renders a list of tooltips corresponding to checkboxes in description HTML', () => {
expect(findTooltips()).toHaveLength(3);
expect(findTooltips().at(0).props('target')).toBe(
findTaskActionButtons().at(0).attributes('id'),
);
});
@ -265,20 +264,18 @@ describe('Description component', () => {
expect(findModal().props('visible')).toBe(false);
});
it('opens a modal when a button on popover is clicked and displays correct title', async () => {
findConvertToTaskButton().vm.$emit('click');
expect(showModal).toHaveBeenCalled();
await nextTick();
it('opens a modal when a button is clicked and displays correct title', async () => {
await findConvertToTaskButton().trigger('click');
expect(findCreateWorkItem().props('initialTitle').trim()).toBe('todo 1');
});
it('closes the modal on `closeCreateTaskModal` event', () => {
findConvertToTaskButton().vm.$emit('click');
it('closes the modal on `closeCreateTaskModal` event', async () => {
await findConvertToTaskButton().trigger('click');
findCreateWorkItem().vm.$emit('closeModal');
expect(hideModal).toHaveBeenCalled();
});
it('emits `updateDescription` on `onCreate` event', async () => {
it('emits `updateDescription` on `onCreate` event', () => {
const newDescription = `<p>New description</p>`;
findCreateWorkItem().vm.$emit('onCreate', newDescription);
expect(hideModal).toHaveBeenCalled();

View File

@ -36,6 +36,20 @@ RSpec.describe Mutations::Environments::CanaryIngress::Update do
it 'returns no errors' do
expect(subject[:errors]).to be_empty
end
context 'with certificate_based_clusters disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it 'returns notice about feature removal' do
expect(subject[:errors]).to match_array([
'This endpoint was deactivated as part of the certificate-based' \
'kubernetes integration removal. See Epic:' \
'https://gitlab.com/groups/gitlab-org/configure/-/epics/8'
])
end
end
end
context 'when service encounters a problem' do

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillWorkItemTypeIdForIssues do
subject(:migrate) { migration.perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms, issue_type_enum[:issue], issue_type.id) }
let(:migration) { described_class.new }
let(:batch_table) { 'issues' }
let(:batch_column) { 'id' }
let(:sub_batch_size) { 2 }
let(:pause_ms) { 0 }
# let_it_be can't be used in migration specs because all tables but `work_item_types` are deleted after each spec
let(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } }
let(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
let(:project) { table(:projects).create!(namespace_id: namespace.id) }
let(:issues_table) { table(:issues) }
let(:issue_type) { table(:work_item_types).find_by!(namespace_id: nil, base_type: issue_type_enum[:issue]) }
let(:issue1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) }
let(:issue2) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) }
let(:issue3) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:issue]) }
let(:incident1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:incident]) }
# test_case and requirement are EE only, but enum values exist on the FOSS model
let(:test_case1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:test_case]) }
let(:requirement1) { issues_table.create!(project_id: project.id, issue_type: issue_type_enum[:requirement]) }
let(:start_id) { issue1.id }
let(:end_id) { requirement1.id }
let(:all_issues) { [issue1, issue2, issue3, incident1, test_case1, requirement1] }
it 'sets work_item_type_id only for the given type' do
expect(all_issues).to all(have_attributes(work_item_type_id: nil))
expect { migrate }.to make_queries_matching(/UPDATE \"issues\" SET "work_item_type_id"/, 2)
all_issues.each(&:reload)
expect([issue1, issue2, issue3]).to all(have_attributes(work_item_type_id: issue_type.id))
expect(all_issues - [issue1, issue2, issue3]).to all(have_attributes(work_item_type_id: nil))
end
it 'tracks timings of queries' do
expect(migration.batch_metrics.timings).to be_empty
expect { migrate }.to change { migration.batch_metrics.timings }
end
it 'retries on ActiveRecord::StatementTimeout' do
expect(migration).to receive(:update_batch).exactly(3).times.and_raise(ActiveRecord::StatementTimeout)
expect(migration).to receive(:sleep).with(30).twice
expect do
migrate
end.to raise_error(ActiveRecord::StatementTimeout)
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe BackfillWorkItemTypeIdOnIssues, :migration do
let_it_be(:migration) { described_class::MIGRATION }
let_it_be(:interval) { 2.minutes }
let_it_be(:issue_type_enum) { { issue: 0, incident: 1, test_case: 2, requirement: 3, task: 4 } }
let_it_be(:base_work_item_type_ids) do
table(:work_item_types).where(namespace_id: nil).order(:base_type).each_with_object({}) do |type, hash|
hash[type.base_type] = type.id
end
end
describe '#up' do
it 'correctly schedules background migrations' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
scheduled_migrations = Gitlab::Database::BackgroundMigration::BatchedMigration.where(job_class_name: migration)
work_item_types = table(:work_item_types).where(namespace_id: nil)
expect(scheduled_migrations.count).to eq(work_item_types.count)
[:issue, :incident, :test_case, :requirement, :task].each do |issue_type|
expect(migration).to have_scheduled_batched_migration(
table_name: :issues,
column_name: :id,
job_arguments: [issue_type_enum[issue_type], base_work_item_type_ids[issue_type_enum[issue_type]]],
interval: interval,
batch_size: described_class::BATCH_SIZE,
max_batch_size: described_class::MAX_BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
end
end
end
describe '#down' do
it 'deletes all batched migration records' do
migrate!
schema_migrate_down!
expect(migration).not_to have_scheduled_batched_migration
end
end
end

View File

@ -34,6 +34,13 @@ RSpec.describe EnvironmentStatus do
subject { environment_status.deployment }
it { is_expected.to eq(deployment) }
context 'multiple deployments' do
it {
new_deployment = create(:deployment, :succeed, environment: deployment.environment, sha: deployment.sha )
is_expected.to eq(new_deployment)
}
end
end
# $ git diff --stat pages-deploy-target...pages-deploy