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
|
before_create :set_released_at
|
||||||
|
|
||||||
validates :project, :tag, presence: true
|
validates :project, :tag, presence: true
|
||||||
|
validates :author_id, presence: true, if: :validate_release_with_author?
|
||||||
|
|
||||||
validates :tag, uniqueness: { scope: :project_id }
|
validates :tag, uniqueness: { scope: :project_id }
|
||||||
|
|
||||||
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed?
|
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_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 :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 :sorted, -> { order(released_at: :desc) }
|
||||||
scope :preloaded, -> {
|
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
|
context 'when updating existing release without author' do
|
||||||
let(:release) { create(:release, :legacy) }
|
let(:release) { create(:release, :legacy) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(validate_release_with_author: false)
|
||||||
|
end
|
||||||
|
|
||||||
it 'updates successfully' do
|
it 'updates successfully' do
|
||||||
release.description += 'Update'
|
release.description += 'Update'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue