Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dd12b82da7
commit
03686022bc
|
|
@ -26,12 +26,13 @@ class Release < ApplicationRecord
|
|||
before_create :set_released_at
|
||||
|
||||
validates :project, :tag, presence: true
|
||||
validates :author_id, presence: true, if: :validate_release_with_author?
|
||||
|
||||
validates :tag, uniqueness: { scope: :project_id }
|
||||
|
||||
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed?
|
||||
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
|
||||
validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
|
||||
validates :author_id, presence: true, on: :create, if: :validate_release_with_author?
|
||||
|
||||
scope :sorted, -> { order(released_at: :desc) }
|
||||
scope :preloaded, -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ScheduleBackfillReleasesAuthorId < Gitlab::Database::Migration[2.1]
|
||||
MIGRATION = 'BackfillReleasesAuthorId'
|
||||
JOB_DELAY_INTERVAL = 2.minutes
|
||||
GHOST_USER_TYPE = 5
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
class User < MigrationRecord
|
||||
self.table_name = 'users'
|
||||
end
|
||||
|
||||
class Release < MigrationRecord
|
||||
self.table_name = 'releases'
|
||||
end
|
||||
|
||||
def up
|
||||
unless release_with_empty_author_exists?
|
||||
say "There are no releases with empty author_id, so skipping migration #{self.class.name}"
|
||||
return
|
||||
end
|
||||
|
||||
create_ghost_user if ghost_user_id.nil?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:releases,
|
||||
:id,
|
||||
ghost_user_id,
|
||||
job_interval: JOB_DELAY_INTERVAL
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :releases, :id, [ghost_user_id])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ghost_user_id
|
||||
User.find_by(user_type: GHOST_USER_TYPE)&.id
|
||||
end
|
||||
|
||||
def create_ghost_user
|
||||
user = User.new
|
||||
user.name = 'Ghost User'
|
||||
user.username = 'ghost'
|
||||
user.email = 'ghost@example.com'
|
||||
user.user_type = GHOST_USER_TYPE
|
||||
user.projects_limit = 100000
|
||||
|
||||
user.save!
|
||||
end
|
||||
|
||||
def release_with_empty_author_exists?
|
||||
Release.exists?(author_id: nil)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
6d5872c6c5e0a7bc9bd52eeac7cbbd49bbe41210dd5596078acf088ac8eec1bd
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Backfills releases with empty release authors.
|
||||
# More details on:
|
||||
# 1) https://gitlab.com/groups/gitlab-org/-/epics/8375
|
||||
# 2) https://gitlab.com/gitlab-org/gitlab/-/issues/367522#note_1156503600
|
||||
class BackfillReleasesAuthorId < BatchedMigrationJob
|
||||
operation_name :backfill_releases_author_id
|
||||
job_arguments :ghost_user_id
|
||||
|
||||
scope_to ->(relation) { relation.where(author_id: nil) }
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
sub_batch.update_all(author_id: ghost_user_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillReleasesAuthorId,
|
||||
:migration, schema: 20221215151822, feature_category: :release_orchestration do
|
||||
let(:releases_table) { table(:releases) }
|
||||
let(:user_table) { table(:users) }
|
||||
let(:date_time) { DateTime.now }
|
||||
|
||||
let!(:test_user) { user_table.create!(name: 'test', email: 'test@example.com', username: 'test', projects_limit: 10) }
|
||||
let!(:ghost_user) do
|
||||
user_table.create!(name: 'ghost', email: 'ghost@example.com',
|
||||
username: 'ghost', user_type: User::USER_TYPES['ghost'], projects_limit: 100000)
|
||||
end
|
||||
|
||||
let(:migration) do
|
||||
described_class.new(start_id: 1, end_id: 100,
|
||||
batch_table: :releases, batch_column: :id,
|
||||
sub_batch_size: 10, pause_ms: 0,
|
||||
job_arguments: [ghost_user.id],
|
||||
connection: ApplicationRecord.connection)
|
||||
end
|
||||
|
||||
subject(:perform_migration) { migration.perform }
|
||||
|
||||
before do
|
||||
releases_table.create!(tag: 'tag1', name: 'tag1',
|
||||
released_at: (date_time - 1.minute), author_id: test_user.id)
|
||||
releases_table.create!(tag: 'tag2', name: 'tag2',
|
||||
released_at: (date_time - 2.minutes), author_id: test_user.id)
|
||||
releases_table.new(tag: 'tag3', name: 'tag3',
|
||||
released_at: (date_time - 3.minutes), author_id: nil).save!(validate: false)
|
||||
releases_table.new(tag: 'tag4', name: 'tag4',
|
||||
released_at: (date_time - 4.minutes), author_id: nil).save!(validate: false)
|
||||
releases_table.new(tag: 'tag5', name: 'tag5',
|
||||
released_at: (date_time - 5.minutes), author_id: nil).save!(validate: false)
|
||||
releases_table.create!(tag: 'tag6', name: 'tag6',
|
||||
released_at: (date_time - 6.minutes), author_id: test_user.id)
|
||||
releases_table.new(tag: 'tag7', name: 'tag7',
|
||||
released_at: (date_time - 7.minutes), author_id: nil).save!(validate: false)
|
||||
end
|
||||
|
||||
it 'backfills `author_id` for the selected records', :aggregate_failures do
|
||||
expect(releases_table.where(author_id: ghost_user.id).count).to eq 0
|
||||
expect(releases_table.where(author_id: nil).count).to eq 4
|
||||
|
||||
perform_migration
|
||||
|
||||
expect(releases_table.where(author_id: ghost_user.id).count).to eq 4
|
||||
expect(releases_table.where(author_id: ghost_user.id).pluck(:name)).to include('tag3', 'tag4', 'tag5', 'tag7')
|
||||
expect(releases_table.where(author_id: test_user.id).count).to eq 3
|
||||
expect(releases_table.where(author_id: test_user.id).pluck(:name)).to include('tag1', 'tag2', 'tag6')
|
||||
end
|
||||
|
||||
it 'tracks timings of queries' do
|
||||
expect(migration.batch_metrics.timings).to be_empty
|
||||
|
||||
expect { perform_migration }.to change { migration.batch_metrics.timings }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe ScheduleBackfillReleasesAuthorId, feature_category: :release_orchestration do
|
||||
context 'when there are releases without author' do
|
||||
let(:releases_table) { table(:releases) }
|
||||
let(:user_table) { table(:users) }
|
||||
let(:date_time) { DateTime.now }
|
||||
let!(:batched_migration) { described_class::MIGRATION }
|
||||
let!(:test_user) do
|
||||
user_table.create!(name: 'test',
|
||||
email: 'test@example.com',
|
||||
username: 'test',
|
||||
projects_limit: 10)
|
||||
end
|
||||
|
||||
before do
|
||||
releases_table.create!(tag: 'tag1', name: 'tag1',
|
||||
released_at: (date_time - 1.minute), author_id: test_user.id)
|
||||
releases_table.create!(tag: 'tag2', name: 'tag2',
|
||||
released_at: (date_time - 2.minutes), author_id: test_user.id)
|
||||
releases_table.new(tag: 'tag3', name: 'tag3',
|
||||
released_at: (date_time - 3.minutes), author_id: nil).save!(validate: false)
|
||||
releases_table.new(tag: 'tag4', name: 'tag4',
|
||||
released_at: (date_time - 4.minutes), author_id: nil).save!(validate: false)
|
||||
end
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :releases,
|
||||
column_name: :id,
|
||||
interval: described_class::JOB_DELAY_INTERVAL,
|
||||
job_arguments: [User.find_by(user_type: :ghost)&.id]
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no releases without author' do
|
||||
it 'does not schedule batched migration' do
|
||||
expect(described_class.new.up).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -86,6 +86,10 @@ RSpec.describe Release do
|
|||
context 'when updating existing release without author' do
|
||||
let(:release) { create(:release, :legacy) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(validate_release_with_author: false)
|
||||
end
|
||||
|
||||
it 'updates successfully' do
|
||||
release.description += 'Update'
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue