Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-04 12:10:22 +00:00
parent 191020103b
commit eef37e3313
33 changed files with 698 additions and 588 deletions

View File

@ -55,8 +55,14 @@ update-qa-cache:
before_script:
- source scripts/utils.sh
- install_gitlab_gem
- tooling/bin/find_change_diffs ${CHANGES_DIFFS_DIR}
script:
- ./scripts/trigger-build omnibus
- |
if tooling/bin/qa/check_if_only_quarantined_specs ${CHANGES_DIFFS_DIR}; then
exit 0
else
./scripts/trigger-build omnibus
fi
# These jobs often time out, so temporarily use private runners and a long timeout: https://gitlab.com/gitlab-org/gitlab/-/issues/238563
tags:
- prm
@ -66,14 +72,19 @@ update-qa-cache:
artifacts: false
- job: build-assets-image
artifacts: false
- detect-tests
artifacts:
expire_in: 7d
paths:
- ${CHANGES_FILE}
- ${CHANGES_DIFFS_DIR}/*
variables:
CHANGES_FILE: tmp/changed_files.txt
CHANGES_DIFFS_DIR: tmp/diffs
.package-and-qa-ff-base:
needs:
- detect-tests
variables:
CHANGED_FILES: tmp/changed_files.txt
script:
- export GITLAB_QA_OPTIONS="--set-feature-flags $(scripts/changed-feature-flags --files $(cat $CHANGED_FILES | tr ' ' ',') --state $QA_FF_STATE)"
- export GITLAB_QA_OPTIONS="--set-feature-flags $(scripts/changed-feature-flags --files $(cat $CHANGES_FILE | tr ' ' ',') --state $QA_FF_STATE)"
- echo $GITLAB_QA_OPTIONS
- ./scripts/trigger-build omnibus

View File

@ -6,11 +6,6 @@
- .default-before_script
- .rails-cache
.minimal-bundle-install:
script:
- export BUNDLE_WITHOUT="${BUNDLE_WITHOUT}:default:test:puma:kerberos:metrics:omnibus:ed25519"
- bundle_install_script
.base-script:
script:
# Only install knapsack after bundle install! Otherwise oddly some native
@ -241,11 +236,11 @@ update-gitaly-binaries-cache:
.coverage-base:
extends:
- .default-retry
- .default-before_script
- .coverage-cache
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
before_script:
- source scripts/utils.sh
- export BUNDLE_WITHOUT="${BUNDLE_WITHOUT}:default:test:puma:kerberos:metrics:omnibus:ed25519"
- bundle_install_script
rspec migration pg12:
extends:
@ -476,21 +471,38 @@ rspec:coverage:
# so we use `dependencies` here.
dependencies:
- setup-test-env
# FOSS/EE jobs
- rspec migration pg12
- rspec unit pg12
- rspec integration pg12
- rspec system pg12
# FOSS/EE minimal jobs
- rspec migration pg12 minimal
- rspec unit pg12 minimal
- rspec integration pg12 minimal
- rspec system pg12 minimal
# EE jobs
- rspec-ee migration pg12
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
# EE minimal jobs
- rspec-ee migration pg12 minimal
- rspec-ee unit pg12 minimal
- rspec-ee integration pg12 minimal
- rspec-ee system pg12 minimal
# Geo jobs
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
# Geo minimal jobs
- rspec-ee unit pg12 geo minimal
- rspec-ee integration pg12 geo minimal
- rspec-ee system pg12 geo minimal
# Memory jobs
- memory-static
- memory-on-boot
script:
- !reference [.minimal-bundle-install, script]
- run_timed_command "bundle exec scripts/merge-simplecov"
- run_timed_command "bundle exec scripts/gather-test-memory-data"
coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
@ -515,7 +527,6 @@ rspec:feature-flags:
- job: "haml-lint ee"
optional: true
script:
- !reference [.minimal-bundle-install, script]
- if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then
run_timed_command "bundle exec scripts/used-feature-flags" || (scripts/slack master-broken "☠️ \`${CI_JOB_NAME}\` failed! ☠️ See ${CI_JOB_URL}" ci_failing "GitLab Bot" && exit 1);
else

View File

@ -1258,7 +1258,7 @@
.rails:rules:detect-tests:
rules:
- changes: *code-backstage-patterns
- changes: *code-backstage-qa-patterns
- <<: *if-merge-request-labels-run-all-rspec
.rails:rules:detect-previous-failed-tests:

View File

@ -111,115 +111,9 @@ Geo secondary sites have a [Geo tracking database](https://gitlab.com/gitlab-org
- [ ] Be sure to commit the relevant changes in `ee/db/geo/structure.sql`
### Add verification state fields on the Geo primary site
### Add verification state to the Model
The Geo primary site needs to checksum every replicable in order for secondaries to verify their own checksums. To do this, Geo requires fields on the Model. There are two ways to add the necessary verification state fields. If the table is large and wide, then it may be a good idea to add verification state fields to a separate table (Option 2). Consult a database expert if needed.
#### Add verification state fields to the model table (Option 1)
- [ ] Create the migration file in `db/migrate`:
```shell
bin/rails generate migration AddVerificationStateToCoolWidgets
```
- [ ] Replace the contents of the migration file with:
```ruby
# frozen_string_literal: true
class AddVerificationStateToCoolWidgets < ActiveRecord::Migration[6.0]
def change
change_table(:cool_widgets) do |t|
t.integer :verification_state, default: 0, limit: 2, null: false
t.column :verification_started_at, :datetime_with_timezone
t.integer :verification_retry_count, limit: 2, null: false
t.column :verification_retry_at, :datetime_with_timezone
t.column :verified_at, :datetime_with_timezone
t.binary :verification_checksum, using: 'verification_checksum::bytea'
t.text :verification_failure # rubocop:disable Migration/AddLimitToTextColumns
end
end
end
```
- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
- [ ] If `cool_widgets` is a high-traffic table, follow [the database documentation to use `with_lock_retries`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/migration_style_guide.md#when-to-use-the-helper-method)
- [ ] Adding a `text` column also [requires](../database/strings_and_the_text_data_type.md#add-a-text-column-to-an-existing-table) setting a limit. Create the migration file in `db/migrate`:
```shell
bin/rails generate migration AddVerificationFailureLimitToCoolWidgets
```
- [ ] Replace the contents of the migration file with:
```ruby
# frozen_string_literal: true
class AddVerificationFailureLimitToCoolWidgets < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
CONSTRAINT_NAME = 'cool_widget_verification_failure_text_limit'
def up
add_text_limit :cool_widget, :verification_failure, 255, constraint_name: CONSTRAINT_NAME
end
def down
remove_check_constraint(:cool_widget, CONSTRAINT_NAME)
end
end
```
- [ ] Add indexes on verification fields to ensure verification can be performed efficiently. Some or all of these indexes can be omitted if the table is guaranteed to be small. Ask a database expert if you are considering omitting indexes. Create the migration file in `db/migrate`:
```shell
bin/rails generate migration AddVerificationIndexesToCoolWidgets
```
- [ ] Replace the contents of the migration file with:
```ruby
# frozen_string_literal: true
class AddVerificationIndexesToCoolWidgets < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
VERIFICATION_STATE_INDEX_NAME = "index_cool_widgets_on_verification_state"
PENDING_VERIFICATION_INDEX_NAME = "index_cool_widgets_pending_verification"
FAILED_VERIFICATION_INDEX_NAME = "index_cool_widgets_failed_verification"
NEEDS_VERIFICATION_INDEX_NAME = "index_cool_widgets_needs_verification"
disable_ddl_transaction!
def up
add_concurrent_index :cool_widgets, :verification_state, name: VERIFICATION_STATE_INDEX_NAME
add_concurrent_index :cool_widgets, :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
add_concurrent_index :cool_widgets, :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
add_concurrent_index :cool_widgets, :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
end
def down
remove_concurrent_index_by_name :cool_widgets, VERIFICATION_STATE_INDEX_NAME
remove_concurrent_index_by_name :cool_widgets, PENDING_VERIFICATION_INDEX_NAME
remove_concurrent_index_by_name :cool_widgets, FAILED_VERIFICATION_INDEX_NAME
remove_concurrent_index_by_name :cool_widgets, NEEDS_VERIFICATION_INDEX_NAME
end
end
```
- [ ] Run database migrations:
```shell
bin/rake db:migrate
```
- [ ] Be sure to commit the relevant changes in `db/structure.sql`
#### Add verification state fields to a separate table (Option 2)
The Geo primary site needs to checksum every replicable so secondaries can verify their own checksums. To do this, Geo requires the Model to have an associated table to track verification state.
- [ ] Create the migration file in `db/migrate`:
@ -273,6 +167,7 @@ The Geo primary site needs to checksum every replicable in order for secondaries
```
- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
- [ ] Run database migrations:
```shell
@ -287,7 +182,14 @@ That's all of the required database changes.
#### Step 1. Implement replication and verification
- [ ] Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
- Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- Include the `::Gitlab::Geo::VerificationState` concern.
- Delegate verification related methods to the `cool_widget_state` model.
- For verification, override some scopes to use the `cool_widget_states` table instead of the model table.
- Implement the `verification_state_object` method to return the object that holds
the verification details
- Override some methods to use the `cool_widget_states` table in verification-related queries.
Pay some attention to method `pool_repository`. Not every repository type uses repository pooling. As Geo prefers to use repository snapshotting, it can lead to data loss. Make sure to overwrite `pool_repository` so it returns nil for repositories that do not have pools.
@ -297,6 +199,7 @@ That's all of the required database changes.
# frozen_string_literal: true
class CoolWidget < ApplicationRecord
...
include ::Gitlab::Geo::ReplicableModel
include ::Gitlab::Geo::VerificationState
@ -304,31 +207,62 @@ That's all of the required database changes.
mount_uploader :file, CoolWidgetUploader
has_one :cool_widget_state, autosave: false, inverse_of: :cool_widget, class_name: 'Geo::CoolWidgetState'
delegate :verification_retry_at, :verification_retry_at=,
:verified_at, :verified_at=,
:verification_checksum, :verification_checksum=,
:verification_failure, :verification_failure=,
:verification_retry_count, :verification_retry_count=,
:verification_state=, :verification_state,
:verification_started_at=, :verification_started_at,
to: :cool_widget_state
...
scope :with_verification_state, ->(state) { joins(:cool_widget_state).where(cool_widget_states: { verification_state: verification_state_value(state) }) }
scope :checksummed, -> { joins(:cool_widget_state).where.not(cool_widget_states: { verification_checksum: nil } ) }
scope :not_checksummed, -> { joins(:cool_widget_state).where(cool_widget_states: { verification_checksum: nil } ) }
# Override the `all` default if not all records can be replicated. For an
# example of an existing Model that needs to do this, see
# `EE::MergeRequestDiff`.
# scope :available_replicables, -> { all }
# @param primary_key_in [Range, CoolWidget] arg to pass to primary_key_in scope
# @return [ActiveRecord::Relation<CoolWidget>] everything that should be synced to this node, restricted by primary key
def self.replicables_for_current_secondary(primary_key_in)
# This issue template does not help you write this method.
#
# This method is called only on Geo secondary sites. It is called when
# we want to know which records to replicate. This is not easy to automate
# because for example:
#
# * The "selective sync" feature allows admins to choose which namespaces # to replicate, per secondary site. Most Models are scoped to a
# namespace, but the nature of the relationship to a namespace varies
# between Models.
# * The "selective sync" feature allows admins to choose which shards to
# replicate, per secondary site. Repositories are associated with
# shards. Most blob types are not, but Project Uploads are.
# * Remote stored replicables are not replicated, by default. But the
# setting `sync_object_storage` enables replication of remote stored
# replicables.
#
# Search the codebase for examples, and consult a Geo expert if needed.
def verification_state_object
cool_widget_state
end
...
class_methods do
extend ::Gitlab::Utils::Override
...
# @param primary_key_in [Range, CoolWidget] arg to pass to primary_key_in scope
# @return [ActiveRecord::Relation<CoolWidget>] everything that should be synced to this node, restricted by primary key
def replicables_for_current_secondary(primary_key_in)
# This issue template does not help you write this method.
#
# This method is called only on Geo secondary sites. It is called when
# we want to know which records to replicate. This is not easy to automate
# because for example:
#
# * The "selective sync" feature allows admins to choose which namespaces # to replicate, per secondary site. Most Models are scoped to a
# namespace, but the nature of the relationship to a namespace varies
# between Models.
# * The "selective sync" feature allows admins to choose which shards to
# replicate, per secondary site. Repositories are associated with
# shards. Most blob types are not, but Project Uploads are.
# * Remote stored replicables are not replicated, by default. But the
# setting `sync_object_storage` enables replication of remote stored
# replicables.
#
# Search the codebase for examples, and consult a Geo expert if needed.
end
override :verification_state_table_class
def verification_state_table_class
CoolWidgetState
end
end
# Geo checks this method in FrameworkRepositorySyncService to avoid
@ -336,6 +270,11 @@ That's all of the required database changes.
def pool_repository
nil
end
...
def cool_widget_state
super || build_cool_widget_state
end
...
end
@ -343,6 +282,15 @@ That's all of the required database changes.
- [ ] Implement `CoolWidget.replicables_for_current_secondary` above.
- [ ] Ensure `CoolWidget.replicables_for_current_secondary` is well-tested. Search the codebase for `replicables_for_current_secondary` to find examples of parameterized table specs. You may need to add more `FactoryBot` traits.
- [ ] Add the following shared examples to `ee/spec/models/ee/cool_widget_spec.rb`:
```ruby
include_examples 'a replicable model with a separate table for verification state' do
let(:verifiable_model_record) { build(:cool_widget) } # add extra params if needed to make sure the record is included in `available_verifiables`
let(:unverifiable_model_record) { build(:cool_widget) } # add extra params if needed to make sure the record is NOT included in `available_verifiables`
end
```
- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#repository` method which should return a `<Repository>` instance, and implement the class method `.model` to return the `CoolWidget` class:
```ruby
@ -529,13 +477,7 @@ That's all of the required database changes.
- [ ] Make sure the factory also allows setting a `project` attribute. If the model does not have a direct relation to a project, you can use a `transient` attribute. Check out `spec/factories/merge_request_diffs.rb` for an example.
##### If you added verification state fields to a separate table (option 2 above), then you need to make additional model and factory changes
If you did not add verification state fields to a separate table, `cool_widget_states`, then skip to [Step 2. Implement metrics gathering](#step-2-implement-metrics-gathering).
Otherwise, you can follow [the example of Merge Request Diffs](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63309).
- [ ] Add a `Geo::CoolWidgetState` model in `ee/app/models/geo/cool_widget_state.rb`:
- [ ] Following [the example of Merge Request Diffs](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63309) add a `Geo::CoolWidgetState` model in `ee/app/models/ee/geo/cool_widget_state.rb`:
``` ruby
# frozen_string_literal: true
@ -569,63 +511,6 @@ Otherwise, you can follow [the example of Merge Request Diffs](https://gitlab.co
end
```
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
- Include the `::Gitlab::Geo::VerificationState` concern.
- Delegate verification related methods to the `cool_widget_state` model.
- Override some scopes to use the `cool_widget_states` table instead of the model table, for verification.
- Override some methods to use the `cool_widget_states` table in verification related queries.
```ruby
class CoolWidget < ApplicationRecord
...
include ::Gitlab::Geo::VerificationState
has_one :cool_widget_state, autosave: true, inverse_of: :cool_widget, class_name: 'Geo::CoolWidgetState'
delegate :verification_retry_at, :verification_retry_at=,
:verified_at, :verified_at=,
:verification_checksum, :verification_checksum=,
:verification_failure, :verification_failure=,
:verification_retry_count, :verification_retry_count=,
:verification_state=, :verification_state,
:verification_started_at=, :verification_started_at,
to: :cool_widget_state
...
scope :with_verification_state, ->(state) { joins(:cool_widget_state).where(cool_widget_states: { verification_state: verification_state_value(state) }) }
scope :checksummed, -> { joins(:cool_widget_state).where.not(cool_widget_states: { verification_checksum: nil } ) }
scope :not_checksummed, -> { joins(:cool_widget_state).where(cool_widget_states: { verification_checksum: nil } ) }
...
class_methods do
extend ::Gitlab::Utils::Override
...
override :verification_state_table_name
def verification_state_table_name
'cool_widget_states'
end
override :verification_state_model_key
def verification_state_model_key
'cool_widget_id'
end
override :verification_arel_table
def verification_arel_table
CoolWidgetState.arel_table
end
end
...
def cool_widget_state
super || build_cool_widget_state
end
...
end
```
#### Step 2. Implement metrics gathering
Metrics are gathered by `Geo::MetricsUpdateWorker`, persisted in `GeoNodeStatus` for display in the UI, and sent to Prometheus:

View File

@ -37,6 +37,7 @@ It is also a good idea to first open a proof-of-concept merge request. It can be
You can look into the following examples of MRs for implementing replication/verification for a new blob type:
- [Add db changes](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60935) and [add verification for MR diffs using SSF](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63309)
- [Verify Terraform state versions](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58800)
- [Verify LFS objects](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63981)
### Modify database schemas to prepare to add Geo support for Cool Widgets
@ -114,113 +115,9 @@ Geo secondary sites have a [Geo tracking database](https://gitlab.com/gitlab-org
### Add verification state fields on the Geo primary site
The Geo primary site needs to checksum every replicable in order for secondaries to verify their own checksums. To do this, Geo requires fields on the Model. There are two ways to add the necessary verification state fields. If the table is large and wide, then it may be a good idea to add verification state fields to a separate table (Option 2). Consult a database expert if needed.
The Geo primary site needs to checksum every replicable so secondaries can verify their own checksums. To do this, Geo requires fields on the Model. Add verification state fields to a separate table. Consult a database expert if needed.
#### Add verification state fields to the model table (Option 1)
- [ ] Create the migration file in `db/migrate`:
```shell
bin/rails generate migration AddVerificationStateToCoolWidgets
```
- [ ] Replace the contents of the migration file with:
```ruby
# frozen_string_literal: true
class AddVerificationStateToCoolWidgets < ActiveRecord::Migration[6.0]
def change
change_table(:cool_widgets) do |t|
t.integer :verification_state, default: 0, limit: 2, null: false
t.column :verification_started_at, :datetime_with_timezone
t.integer :verification_retry_count, limit: 2, null: false
t.column :verification_retry_at, :datetime_with_timezone
t.column :verified_at, :datetime_with_timezone
t.binary :verification_checksum, using: 'verification_checksum::bytea'
t.text :verification_failure # rubocop:disable Migration/AddLimitToTextColumns
end
end
end
```
- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
- [ ] If `cool_widgets` is a high-traffic table, follow [the database documentation to use `with_lock_retries`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/migration_style_guide.md#when-to-use-the-helper-method)
- [ ] Adding a `text` column also [requires](../database/strings_and_the_text_data_type.md#add-a-text-column-to-an-existing-table) setting a limit. Create the migration file in `db/migrate`:
```shell
bin/rails generate migration AddVerificationFailureLimitToCoolWidgets
```
- [ ] Replace the contents of the migration file with:
```ruby
# frozen_string_literal: true
class AddVerificationFailureLimitToCoolWidgets < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
CONSTRAINT_NAME = 'cool_widget_verification_failure_text_limit'
def up
add_text_limit :cool_widget, :verification_failure, 255, constraint_name: CONSTRAINT_NAME
end
def down
remove_check_constraint(:cool_widget, CONSTRAINT_NAME)
end
end
```
- [ ] Add indexes on verification fields to ensure verification can be performed efficiently. Some or all of these indexes can be omitted if the table is guaranteed to be small. Ask a database expert if you are considering omitting indexes. Create the migration file in `db/migrate`:
```shell
bin/rails generate migration AddVerificationIndexesToCoolWidgets
```
- [ ] Replace the contents of the migration file with:
```ruby
# frozen_string_literal: true
class AddVerificationIndexesToCoolWidgets < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
VERIFICATION_STATE_INDEX_NAME = "index_cool_widgets_on_verification_state"
PENDING_VERIFICATION_INDEX_NAME = "index_cool_widgets_pending_verification"
FAILED_VERIFICATION_INDEX_NAME = "index_cool_widgets_failed_verification"
NEEDS_VERIFICATION_INDEX_NAME = "index_cool_widgets_needs_verification"
disable_ddl_transaction!
def up
add_concurrent_index :cool_widgets, :verification_state, name: VERIFICATION_STATE_INDEX_NAME
add_concurrent_index :cool_widgets, :verified_at, where: "(verification_state = 0)", order: { verified_at: 'ASC NULLS FIRST' }, name: PENDING_VERIFICATION_INDEX_NAME
add_concurrent_index :cool_widgets, :verification_retry_at, where: "(verification_state = 3)", order: { verification_retry_at: 'ASC NULLS FIRST' }, name: FAILED_VERIFICATION_INDEX_NAME
add_concurrent_index :cool_widgets, :verification_state, where: "(verification_state = 0 OR verification_state = 3)", name: NEEDS_VERIFICATION_INDEX_NAME
end
def down
remove_concurrent_index_by_name :cool_widgets, VERIFICATION_STATE_INDEX_NAME
remove_concurrent_index_by_name :cool_widgets, PENDING_VERIFICATION_INDEX_NAME
remove_concurrent_index_by_name :cool_widgets, FAILED_VERIFICATION_INDEX_NAME
remove_concurrent_index_by_name :cool_widgets, NEEDS_VERIFICATION_INDEX_NAME
end
end
```
- [ ] Run database migrations:
```shell
bin/rake db:migrate
```
- [ ] Be sure to commit the relevant changes in `db/structure.sql`
#### Add verification state fields to a separate table (Option 2)
#### Add verification state fields to a new table
- [ ] Create the migration file in `db/migrate`:
@ -274,12 +171,15 @@ The Geo primary site needs to checksum every replicable in order for secondaries
```
- [ ] If deviating from the above example, then be sure to order columns according to [our guidelines](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/ordering_table_columns.md).
- [ ] Run database migrations:
```shell
bin/rake db:migrate
```
- [ ] If `cool_widgets` is a high-traffic table, follow [the database documentation to use `with_lock_retries`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/migration_style_guide.md#when-to-use-the-helper-method)
- [ ] Be sure to commit the relevant changes in `db/structure.sql`
That's all of the required database changes.
@ -288,14 +188,22 @@ That's all of the required database changes.
#### Step 1. Implement replication and verification
- [ ] Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
- Include `Gitlab::Geo::ReplicableModel` in the `CoolWidget` class, and specify the Replicator class `with_replicator Geo::CoolWidgetReplicator`.
- Include the `::Gitlab::Geo::VerificationState` concern.
- Delegate verification related methods to the `cool_widget_state` model.
- For verification, override some scopes to use the `cool_widget_states` table instead of the model table.
- Implement the `verification_state_object` method to return the object that holds
the verification details
- Override some methods to use the `cool_widget_states` table in verification-related queries.
At this point the `CoolWidget` class should look like this:
```ruby
# frozen_string_literal: true
class CoolWidget < ApplicationRecord
...
include ::Gitlab::Geo::ReplicableModel
include ::Gitlab::Geo::VerificationState
@ -303,38 +211,84 @@ That's all of the required database changes.
mount_uploader :file, CoolWidgetUploader
has_one :cool_widget_state, autosave: false, inverse_of: :cool_widget, class_name: 'Geo::CoolWidgetState'
delegate :verification_retry_at, :verification_retry_at=,
:verified_at, :verified_at=,
:verification_checksum, :verification_checksum=,
:verification_failure, :verification_failure=,
:verification_retry_count, :verification_retry_count=,
:verification_state=, :verification_state,
:verification_started_at=, :verification_started_at,
to: :cool_widget_state
...
scope :with_verification_state, ->(state) { joins(:cool_widget_state).where(cool_widget_states: { verification_state: verification_state_value(state) }) }
scope :checksummed, -> { joins(:cool_widget_state).where.not(cool_widget_states: { verification_checksum: nil } ) }
scope :not_checksummed, -> { joins(:cool_widget_state).where(cool_widget_states: { verification_checksum: nil } ) }
# Override the `all` default if not all records can be replicated. For an
# example of an existing Model that needs to do this, see
# `EE::MergeRequestDiff`.
# scope :available_replicables, -> { all }
# @param primary_key_in [Range, CoolWidget] arg to pass to primary_key_in scope
# @return [ActiveRecord::Relation<CoolWidget>] everything that should be synced to this node, restricted by primary key
def self.replicables_for_current_secondary(primary_key_in)
# This issue template does not help you write this method.
#
# This method is called only on Geo secondary sites. It is called when
# we want to know which records to replicate. This is not easy to automate
# because for example:
#
# * The "selective sync" feature allows admins to choose which namespaces # to replicate, per secondary site. Most Models are scoped to a
# namespace, but the nature of the relationship to a namespace varies
# between Models.
# * The "selective sync" feature allows admins to choose which shards to
# replicate, per secondary site. Repositories are associated with
# shards. Most blob types are not, but Project Uploads are.
# * Remote stored replicables are not replicated, by default. But the
# setting `sync_object_storage` enables replication of remote stored
# replicables.
#
# Search the codebase for examples, and consult a Geo expert if needed.
def verification_state_object
cool_widget_state
end
...
class_methods do
extend ::Gitlab::Utils::Override
...
# @param primary_key_in [Range, CoolWidget] arg to pass to primary_key_in scope
# @return [ActiveRecord::Relation<CoolWidget>] everything that should be synced to this node, restricted by primary key
def self.replicables_for_current_secondary(primary_key_in)
# This issue template does not help you write this method.
#
# This method is called only on Geo secondary sites. It is called when
# we want to know which records to replicate. This is not easy to automate
# because for example:
#
# * The "selective sync" feature allows admins to choose which namespaces # to replicate, per secondary site. Most Models are scoped to a
# namespace, but the nature of the relationship to a namespace varies
# between Models.
# * The "selective sync" feature allows admins to choose which shards to
# replicate, per secondary site. Repositories are associated with
# shards. Most blob types are not, but Project Uploads are.
# * Remote stored replicables are not replicated, by default. But the
# setting `sync_object_storage` enables replication of remote stored
# replicables.
#
# Search the codebase for examples, and consult a Geo expert if needed.
end
override :verification_state_table_class
def verification_state_table_class
CoolWidgetState
end
end
...
def cool_widget_state
super || build_cool_widget_state
end
...
end
```
- [ ] Implement `CoolWidget.replicables_for_current_secondary` above.
- [ ] Ensure `CoolWidget.replicables_for_current_secondary` is well-tested. Search the codebase for `replicables_for_current_secondary` to find examples of parameterized table specs. You may need to add more `FactoryBot` traits.
- [ ] Add the following shared examples to `ee/spec/models/ee/cool_widget_spec.rb`:
```ruby
include_examples 'a replicable model with a separate table for verification state' do
let(:verifiable_model_record) { build(:cool_widget) } # add extra params if needed to make sure the record is included in `available_verifiables`
let(:unverifiable_model_record) { build(:cool_widget) } # add extra params if needed to make sure the record is NOT included in `available_verifiables`
end
```
- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#carrierwave_uploader` method which should return a `CarrierWave::Uploader`, and implement the class method `.model` to return the `CoolWidget` class:
```ruby
@ -498,13 +452,7 @@ That's all of the required database changes.
- [ ] Make sure the factory also allows setting a `project` attribute. If the model does not have a direct relation to a project, you can use a `transient` attribute. Check out `spec/factories/merge_request_diffs.rb` for an example.
##### If you added verification state fields to a separate table (option 2 above), then you need to make additional model and factory changes
If you did not add verification state fields to a separate table, `cool_widget_states`, then skip to [Step 2. Implement metrics gathering](#step-2-implement-metrics-gathering).
Otherwise, you can follow [the example of Merge Request Diffs](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63309).
- [ ] Add a `Geo::CoolWidgetState` model in `ee/app/models/ee/geo/cool_widget_state.rb`:
- [ ] Following [the example of Merge Request Diffs](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63309) add a `Geo::CoolWidgetState` model in `ee/app/models/ee/geo/cool_widget_state.rb`:
``` ruby
module Geo
@ -536,63 +484,6 @@ Otherwise, you can follow [the example of Merge Request Diffs](https://gitlab.co
end
```
- [ ] Add the following lines to the `cool_widget` model to accomplish some important tasks:
- Include the `::Gitlab::Geo::VerificationState` concern.
- Delegate verification related methods to the `cool_widget_state` model.
- Override some scopes to use the `cool_widget_states` table instead of the model table, for verification.
- Override some methods to use the `cool_widget_states` table in verification related queries.
```ruby
class CoolWidget < ApplicationRecord
...
include ::Gitlab::Geo::VerificationState
has_one :cool_widget_state, autosave: true, inverse_of: :cool_widget, class_name: 'Geo::CoolWidgetState'
delegate :verification_retry_at, :verification_retry_at=,
:verified_at, :verified_at=,
:verification_checksum, :verification_checksum=,
:verification_failure, :verification_failure=,
:verification_retry_count, :verification_retry_count=,
:verification_state=, :verification_state,
:verification_started_at=, :verification_started_at,
to: :cool_widget_state
...
scope :with_verification_state, ->(state) { joins(:cool_widget_state).where(cool_widget_states: { verification_state: verification_state_value(state) }) }
scope :checksummed, -> { joins(:cool_widget_state).where.not(cool_widget_states: { verification_checksum: nil } ) }
scope :not_checksummed, -> { joins(:cool_widget_state).where(cool_widget_states: { verification_checksum: nil } ) }
...
class_methods do
extend ::Gitlab::Utils::Override
...
override :verification_state_table_name
def verification_state_table_name
'cool_widget_states'
end
override :verification_state_model_key
def verification_state_model_key
'cool_widget_id'
end
override :verification_arel_table
def verification_arel_table
CoolWidgetState.arel_table
end
end
...
def cool_widget_state
super || build_cool_widget_state
end
...
end
```
#### Step 2. Implement metrics gathering
Metrics are gathered by `Geo::MetricsUpdateWorker`, persisted in `GeoNodeStatus` for display in the UI, and sent to Prometheus:

View File

@ -123,7 +123,7 @@ Naming/FileName:
- 'ee/lib/generators/**/*'
- 'scripts/**/*'
- 'spec/**/*'
- 'tooling/bin/*'
- 'tooling/bin/**/*'
- 'ee/spec/**/*'
- 'jh/spec/**/*'
- 'qa/bin/*'
@ -708,10 +708,6 @@ Cop/UserAdmin:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Performance/OpenStruct:
Exclude:
- 'ee/spec/**/*.rb'
# See https://gitlab.com/gitlab-org/gitlab/-/issues/327495
Style/RegexpLiteral:
Enabled: false

View File

@ -2172,20 +2172,6 @@ Cop/UserAdmin:
- 'lib/gitlab/visibility_level.rb'
- 'qa/qa/runtime/api/client.rb'
# WIP https://gitlab.com/gitlab-org/gitlab/-/issues/325744
Performance/OpenStruct:
Exclude:
- 'Guardfile'
- 'app/finders/snippets_finder.rb'
- 'app/helpers/application_settings_helper.rb'
- 'ee/lib/gitlab/graphql/aggregations/epics/epic_node.rb'
- 'ee/lib/gitlab/graphql/aggregations/epics/epic_node.rb'
- 'lib/api/wikis.rb'
- 'lib/gitlab/ci/ansi2html.rb'
- 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb'
- 'lib/mattermost/session.rb'
# WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/324629
Gitlab/DelegatePredicateMethods:
Exclude:

View File

@ -1 +1 @@
d03d5db1ce9e3cdb66d63936cf7875b03bb49212
59ec40d9ed6d719bcccc77193e031ffee3098afc

View File

@ -15,6 +15,8 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import { formatDate } from '~/lib/utils/datetime_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
@ -40,7 +42,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: {
boardId: {
default: '',
@ -86,6 +88,13 @@ export default {
listTitle() {
return this.list?.label?.description || this.list?.assignee?.name || this.list.title || '';
},
listIterationPeriod() {
const iteration = this.list?.iteration;
return iteration ? this.getIterationPeriod(iteration) : '';
},
isIterationList() {
return this.listType === ListType.iteration;
},
showListHeaderButton() {
return !this.disabled && this.listType !== ListType.closed;
},
@ -96,7 +105,10 @@ export default {
return this.listType === ListType.assignee && this.showListDetails;
},
showIterationListDetails() {
return this.listType === ListType.iteration && this.showListDetails;
return this.isIterationList && this.showListDetails;
},
iterationCadencesAvailable() {
return this.isIterationList && this.glFeatures.iterationCadences;
},
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
@ -208,6 +220,16 @@ export default {
updateListFunction() {
this.updateList({ listId: this.list.id, collapsed: this.list.collapsed });
},
/**
* TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
* This method also exists as a utility function in ee/../iterations/utils.js
* Remove the duplication when the EE code is separated from this compoment.
*/
getIterationPeriod({ startDate, dueDate }) {
const start = formatDate(startDate, 'mmm d, yyyy', true);
const due = formatDate(dueDate, 'mmm d, yyyy', true);
return `${start} - ${due}`;
},
},
};
</script>
@ -307,6 +329,13 @@ export default {
class="board-title-main-text gl-text-truncate"
>
{{ listTitle }}
<div
v-if="iterationCadencesAvailable"
class="gl-display-inline-block"
data-testid="board-list-iteration-period"
>
<time class="gl-text-gray-400">{{ listIterationPeriod }}</time>
</div>
</span>
<span
v-if="listType === 'assignee'"

View File

@ -0,0 +1,77 @@
<script>
import { GlSprintf } from '@gitlab/ui';
import { escape } from 'lodash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { n__, s__ } from '~/locale';
const mergeCommitCount = s__('mrWidgetCommitsAdded|1 merge commit');
export default {
components: {
GlSprintf,
},
mixins: [glFeatureFlagMixin()],
props: {
isSquashEnabled: {
type: Boolean,
required: false,
default: false,
},
isFastForwardEnabled: {
type: Boolean,
required: true,
},
commitsCount: {
type: Number,
required: false,
default: 0,
},
targetBranch: {
type: String,
required: true,
},
},
computed: {
targetBranchEscaped() {
return escape(this.targetBranch);
},
commitsCountMessage() {
return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount);
},
message() {
return this.isFastForwardEnabled
? s__('mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}.')
: s__(
'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}%{squashedCommits}.',
);
},
textDecorativeComponent() {
return this.glFeatures.restructuredMrWidget ? 'span' : 'strong';
},
},
mergeCommitCount,
};
</script>
<template>
<span>
<gl-sprintf :message="message">
<template #commitCount>
<component :is="textDecorativeComponent" class="commits-count-message">{{
commitsCountMessage
}}</component>
</template>
<template #mergeCommitCount>
<component :is="textDecorativeComponent">{{ $options.mergeCommitCount }}</component>
</template>
<template #targetBranch>
<span class="label-branch">{{ targetBranchEscaped }}</span>
</template>
<template v-if="glFeatures.restructuredMrWidget" #squashedCommits>
<template v-if="isSquashEnabled">
{{ __('(commits will be squashed)') }}</template
></template
>
</gl-sprintf>
</span>
</template>

View File

@ -1,8 +1,10 @@
<script>
import { s__, n__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'MRWidgetRelatedLinks',
mixins: [glFeatureFlagMixin()],
props: {
relatedLinks: {
type: Object,
@ -14,6 +16,11 @@ export default {
required: false,
default: '',
},
showAssignToMe: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
closesText() {
@ -30,16 +37,25 @@ export default {
};
</script>
<template>
<section class="mr-info-list gl-ml-7 gl-pb-5">
<p v-if="relatedLinks.closing">
<section>
<p
v-if="relatedLinks.closing"
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
>
{{ closesText }}
<span v-html="relatedLinks.closing /* eslint-disable-line vue/no-v-html */"></span>
</p>
<p v-if="relatedLinks.mentioned">
<p
v-if="relatedLinks.mentioned"
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
>
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
<span v-html="relatedLinks.mentioned /* eslint-disable-line vue/no-v-html */"></span>
</p>
<p v-if="relatedLinks.assignToMe">
<p
v-if="relatedLinks.assignToMe && showAssignToMe"
:class="{ 'gl-display-line gl-m-0': glFeatures.restructuredMrWidget }"
>
<span v-html="relatedLinks.assignToMe /* eslint-disable-line vue/no-v-html */"></span>
</p>
</section>

View File

@ -1,5 +1,8 @@
<script>
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
mixins: [glFeatureFlagMixin()],
props: {
value: {
type: String,
@ -20,7 +23,10 @@ export default {
<template>
<li>
<div class="commit-message-editor">
<div class="d-flex flex-wrap align-items-center justify-content-between">
<div
:class="{ 'gl-mb-3': glFeatures.restructuredMrWidget }"
class="d-flex flex-wrap align-items-center justify-content-between"
>
<label class="col-form-label" :for="inputId">
<strong>{{ label }}</strong>
</label>

View File

@ -1,15 +1,12 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, n__, s__ } from '~/locale';
const mergeCommitCount = s__('mrWidgetCommitsAdded|1 merge commit');
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import AddedCommitMessage from '../added_commit_message.vue';
export default {
mergeCommitCount,
components: {
GlButton,
GlSprintf,
AddedCommitMessage,
},
props: {
isSquashEnabled: {
@ -39,9 +36,6 @@ export default {
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
commitsCountMessage() {
return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount);
},
modifyLinkMessage() {
if (this.isFastForwardEnabled) return __('Modify commit message');
else if (this.isSquashEnabled) return __('Modify commit messages');
@ -50,16 +44,6 @@ export default {
ariaLabel() {
return this.expanded ? __('Collapse') : __('Expand');
},
targetBranchEscaped() {
return escape(this.targetBranch);
},
message() {
return this.isFastForwardEnabled
? s__('mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}.')
: s__(
'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
);
},
},
methods: {
toggle() {
@ -86,17 +70,12 @@ export default {
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
<span class="vertical-align-middle">
<gl-sprintf :message="message">
<template #commitCount>
<strong class="commits-count-message">{{ commitsCountMessage }}</strong>
</template>
<template #mergeCommitCount>
<strong>{{ $options.mergeCommitCount }}</strong>
</template>
<template #targetBranch>
<span class="label-branch">{{ targetBranchEscaped }}</span>
</template>
</gl-sprintf>
<added-commit-message
:is-squash-enabled="isSquashEnabled"
:is-fast-forward-enabled="isFastForwardEnabled"
:commits-count="commitsCount"
:target-branch="targetBranch"
/>
</span>
<gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}

View File

@ -1,4 +1,5 @@
<script>
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
@ -6,11 +7,12 @@ export default {
components: {
statusIcon,
},
mixins: [glFeatureFlagMixin()],
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon :show-disabled-button="true" status="loading" />
<status-icon :show-disabled-button="!glFeatures.restructuredMrWidget" status="loading" />
<div class="media-body space-children">
<span class="bold"> {{ s__('mrWidget|Checking if merge request can be merged…') }} </span>
</div>

View File

@ -37,7 +37,7 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<p class="media-body gl-m-0! gl-font-weight-bold">
<p class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!">
<template v-if="canMerge">
{{ __('Ready to merge!') }}
</template>

View File

@ -35,6 +35,8 @@ import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MergeRequestStore from '../../stores/mr_widget_store';
import statusIcon from '../mr_widget_status_icon.vue';
import AddedCommitMessage from '../added_commit_message.vue';
import RelatedLinks from '../mr_widget_related_links.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
import CommitsHeader from './commits_header.vue';
@ -113,6 +115,8 @@ export default {
import(
'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
),
AddedCommitMessage,
RelatedLinks,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -134,6 +138,7 @@ export default {
isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage,
isPipelineFailedModalVisible: false,
editCommitMessage: false,
};
},
computed: {
@ -162,7 +167,7 @@ export default {
},
isMergeAllowed() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.mergeable || false;
return this.state.mergeable;
}
return this.mr.isMergeAllowed;
@ -279,6 +284,10 @@ export default {
return enableSquashBeforeMerge && this.commitsCount > 1;
},
shouldShowMergeControls() {
if (this.glFeatures.restructuredMrWidget) {
return this.restructuredWidgetShowMergeButtons;
}
return this.isMergeAllowed || this.isAutoMergeAvailable;
},
shouldShowSquashEdit() {
@ -297,15 +306,26 @@ export default {
showDangerMessageForMergeTrain() {
return this.preferredAutoMergeStrategy === MT_MERGE_STRATEGY && this.isPipelineFailed;
},
restructuredWidgetShowMergeButtons() {
if (this.glFeatures.restructuredMrWidget) {
return this.isMergeAllowed && this.state.userPermissions.canMerge;
}
return true;
},
},
mounted() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
}
},
beforeDestroy() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
eventHub.$off('MRWidgetUpdateRequested', this.updateGraphqlState);
eventHub.$off('mr.discussion.updated', this.updateGraphqlState);
}
if (this.pollingInterval) {
@ -492,7 +512,12 @@ export default {
</script>
<template>
<div>
<div
:class="{
'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7':
glFeatures.restructuredMrWidget,
}"
>
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
<gl-skeleton-loader :width="418" :height="30">
@ -504,11 +529,16 @@ export default {
</div>
</div>
<template v-else>
<div class="mr-widget-body media">
<status-icon :status="iconClass" />
<div
class="mr-widget-body media"
:class="{
'mr-widget-body-line-height-1': glFeatures.restructuredMrWidget,
}"
>
<status-icon v-if="!glFeatures.restructuredMrWidget" :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center">
<gl-button-group class="gl-align-self-start">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<gl-button-group v-if="restructuredWidgetShowMergeButtons" class="gl-align-self-start">
<gl-button
size="medium"
category="primary"
@ -555,6 +585,7 @@ export default {
</gl-button-group>
<div
v-if="shouldShowMergeControls"
:class="{ 'gl-w-full gl-order-n1 gl-mb-5': glFeatures.restructuredMrWidget }"
class="gl-display-flex gl-align-items-center gl-flex-wrap"
>
<merge-train-helper-icon
@ -570,7 +601,11 @@ export default {
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
class="js-remove-source-branch-checkbox gl-mx-3 gl-display-flex gl-align-items-center"
:class="{
'gl-mx-3': !glFeatures.restructuredMrWidget,
'gl-mr-5': glFeatures.restructuredMrWidget,
}"
class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center"
>
{{ __('Delete source branch') }}
</gl-form-checkbox>
@ -581,31 +616,141 @@ export default {
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isSquashReadOnly"
class="gl-mx-3"
:class="{
'gl-mx-3': !glFeatures.restructuredMrWidget,
'gl-mr-5': glFeatures.restructuredMrWidget,
}"
/>
<gl-form-checkbox
v-if="
glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)
"
v-model="editCommitMessage"
class="gl-display-flex gl-align-items-center"
>
{{ __('Edit commit message') }}
</gl-form-checkbox>
</div>
<template v-else>
<div class="bold js-resolve-mr-widget-items-message gl-ml-3">
<div
v-if="hasPipelineMustSucceedConflict"
class="gl-display-flex gl-align-items-center"
data-testid="pipeline-succeed-conflict"
<div
v-else-if="!glFeatures.restructuredMrWidget"
class="bold js-resolve-mr-widget-items-message gl-ml-3"
>
<div
v-if="hasPipelineMustSucceedConflict"
class="gl-display-flex gl-align-items-center"
data-testid="pipeline-succeed-conflict"
>
<gl-sprintf :message="pipelineMustSucceedConflictText" />
<gl-link
:href="mr.pipelineMustSucceedDocsPath"
target="_blank"
class="gl-display-flex gl-ml-2"
>
<gl-sprintf :message="pipelineMustSucceedConflictText" />
<gl-link
:href="mr.pipelineMustSucceedDocsPath"
target="_blank"
class="gl-display-flex gl-ml-2"
<gl-icon name="question" />
</gl-link>
</div>
<gl-sprintf v-else :message="mergeDisabledText" />
</div>
<template v-if="glFeatures.restructuredMrWidget">
<div v-show="editCommitMessage" class="gl-w-full gl-order-n1">
<ul
:class="{
'content-list': !glFeatures.restructuredMrWidget,
'gl-list-style-none gl-p-0 gl-pt-4': glFeatures.restructuredMrWidget,
}"
class="border-top commits-list flex-list"
>
<commit-edit
v-if="shouldShowSquashEdit"
v-model="squashCommitMessage"
:label="__('Squash commit message')"
input-id="squash-message-edit"
class="gl-m-0! gl-p-0!"
>
<gl-icon name="question" />
</gl-link>
</div>
<gl-sprintf v-else :message="mergeDisabledText" />
<template #header>
<commit-message-dropdown v-model="squashCommitMessage" :commits="commits" />
</template>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
v-model="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
class="gl-m-0! gl-p-0!"
>
<template #checkbox>
<label>
<input
id="include-description"
type="checkbox"
@change="updateMergeCommitMessage($event.target.checked)"
/>
{{ __('Include merge request description') }}
</label>
</template>
</commit-edit>
</ul>
</div>
<div
v-if="!restructuredWidgetShowMergeButtons"
class="gl-w-full gl-order-n1 gl-text-gray-500"
>
<strong>
{{ __('Merge details') }}
</strong>
<ul class="gl-pl-4 gl-m-0">
<li class="gl-line-height-normal">
<added-commit-message
:is-squash-enabled="squashBeforeMerge"
:is-fast-forward-enabled="!shouldShowMergeEdit"
:commits-count="commitsCount"
:target-branch="stateData.targetBranch"
/>
</li>
<li class="gl-line-height-normal">
<template v-if="removeSourceBranch">
{{ __('Source branch will be deleted.') }}
</template>
<template v-else>
{{ __('Source branch will not be deleted.') }}
</template>
</li>
<li v-if="mr.relatedLinks" class="gl-line-height-normal">
<related-links
:state="mr.state"
:related-links="mr.relatedLinks"
:show-assign-to-me="false"
class="mr-ready-merge-related-links gl-display-inline"
/>
</li>
</ul>
</div>
<div
v-else
:class="{ 'gl-mb-5': restructuredWidgetShowMergeButtons }"
class="gl-w-full gl-order-n1 gl-text-gray-500"
>
<added-commit-message
:is-squash-enabled="squashBeforeMerge"
:is-fast-forward-enabled="!shouldShowMergeEdit"
:commits-count="commitsCount"
:target-branch="stateData.targetBranch"
/>
<template v-if="mr.relatedLinks">
&middot;
<related-links
:state="mr.state"
:related-links="mr.relatedLinks"
:show-assign-to-me="false"
class="mr-ready-merge-related-links gl-display-inline"
/>
</template>
</div>
</template>
</div>
<div
v-if="showDangerMessageForMergeTrain"
v-if="showDangerMessageForMergeTrain && !glFeatures.restructuredMrWidget"
class="gl-mt-5 gl-text-gray-500"
data-testid="failed-pipeline-merge-train-text"
>
@ -613,7 +758,7 @@ export default {
</div>
</div>
</div>
<template v-if="shouldShowMergeControls">
<template v-if="shouldShowMergeControls && !glFeatures.restructuredMrWidget">
<div
v-if="!shouldShowMergeEdit"
class="mr-fast-forward-message"
@ -622,7 +767,7 @@ export default {
{{ __('Fast-forward merge without a merge commit') }}
</div>
<commits-header
v-if="shouldShowSquashEdit || shouldShowMergeEdit"
v-if="!glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit)"
:is-squash-enabled="squashBeforeMerge"
:commits-count="commitsCount"
:target-branch="stateData.targetBranch"

View File

@ -1,15 +1,18 @@
<script>
import { GlIcon, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
import { GlIcon, GlTooltipDirective, GlFormCheckbox, GlLink } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SQUASH_BEFORE_MERGE } from '../../i18n';
export default {
components: {
GlIcon,
GlFormCheckbox,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
i18n: {
...SQUASH_BEFORE_MERGE,
},
@ -33,6 +36,9 @@ export default {
tooltipTitle() {
return this.isDisabled ? this.$options.i18n.tooltipTitle : null;
},
helpIconName() {
return this.glFeatures.restructuredMrWidget ? 'question-o' : 'question';
},
},
};
</script>
@ -51,18 +57,18 @@ export default {
>
{{ $options.i18n.checkboxLabel }}
</gl-form-checkbox>
<a
<gl-link
v-if="helpPath"
v-gl-tooltip
:href="helpPath"
:title="$options.i18n.helpLabel"
:class="{ 'gl-text-blue-600': glFeatures.restructuredMrWidget }"
target="_blank"
rel="noopener noreferrer nofollow"
>
<gl-icon name="question" />
<gl-icon :name="helpIconName" />
<span class="sr-only">
{{ $options.i18n.helpLabel }}
</span>
</a>
</gl-link>
</div>
</template>

View File

@ -91,6 +91,7 @@ export default {
MrWidgetApprovals,
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
ReadyToMerge: ReadyToMergeState,
},
apollo: {
state: {
@ -213,6 +214,9 @@ export default {
window.gon?.features?.refactorMrWidgetsExtensionsUser
);
},
isRestructuredMrWidgetEnabled() {
return window.gon?.features?.restructuredMrWidget;
},
},
watch: {
'mr.machineValue': {
@ -547,12 +551,17 @@ export default {
<div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" />
<div class="mr-widget-info">
<ready-to-merge
v-if="isRestructuredMrWidgetEnabled && mr.commitsCount"
:mr="mr"
:service="service"
/>
<div v-else class="mr-widget-info">
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:state="mr.state"
:related-links="mr.relatedLinks"
class="mr-info-list gl-ml-7 gl-pb-5"
/>
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />

View File

@ -18,7 +18,9 @@ fragment ReadyToMerge on Project {
commitCount
diffHeadSha
userPermissions {
canMerge
removeSourceBranch
updateMergeRequest
}
targetBranch
mergeError

View File

@ -1,14 +1,14 @@
import { stateKey } from './state_maps';
export default function deviseState() {
if (this.hasMergeChecksFailed) {
if (!this.commitsCount) {
return stateKey.nothingToMerge;
} else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return stateKey.mergeChecksFailed;
} else if (this.projectArchived) {
return stateKey.archived;
} else if (this.branchMissing) {
return stateKey.missingBranch;
} else if (!this.commitsCount) {
return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked' || this.mergeStatus === 'checking') {
return stateKey.checking;
} else if (this.hasConflicts) {

View File

@ -269,7 +269,9 @@ $tabs-holder-z-index: 250;
}
.mr-widget-body {
line-height: 28px;
&:not(.mr-widget-body-line-height-1) {
line-height: 28px;
}
@include clearfix;

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
description: Control the job concurrency in GitLab CI/CD
---
# Resource Group **(FREE)**
# Resource group **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15536) in GitLab 12.7.
@ -13,7 +13,7 @@ By default, pipelines in GitLab CI/CD run in parallel. The parallelization is an
the feedback loop in merge requests, however, there are some situations that
you may want to limit the concurrency on deployment
jobs to run them one by one.
Resource Group allows you to strategically control
Use resource groups to strategically control
the concurrency of the jobs for optimizing your continuous deployments workflow with safety.
## Add a resource group
@ -139,9 +139,59 @@ Depending on the process mode of the resource group:
- `deploy-1`, `deploy-2`, and `deploy-3` do not run in parallel.
- `deploy-3` runs first, `deploy-2` runs second and `deploy-1` runs last.
## Pipeline-level concurrency control with Cross-Project/Parent-Child pipelines
## Pipeline-level concurrency control with cross-project/parent-child pipelines
See the how to [control the pipeline concurrency in cross-project pipelines](../yaml/index.md#pipeline-level-concurrency-control-with-cross-projectparent-child-pipelines).
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/39057) in GitLab 13.9.
You can define `resource_group` for downstream pipelines that are sensitive to concurrent
executions. The [`trigger` keyword](../yaml/index.md#trigger) can trigger downstream pipelines and the
[`resource_group` keyword](../yaml/index.md#resource_group) can co-exist with it. `resource_group` is useful to control the
concurrency of deployment pipelines, while other jobs can continue to run concurrently.
The following example has two pipeline configurations in a project. When a pipeline starts running,
non-sensitive jobs are executed first and aren't affected by concurrent executions in other
pipelines. However, GitLab ensures that there are no other deployment pipelines running before
triggering a deployment (child) pipeline. If other deployment pipelines are running, GitLab waits
until those pipelines finish before running another one.
```yaml
# .gitlab-ci.yml (parent pipeline)
build:
stage: build
script: echo "Building..."
test:
stage: test
script: echo "Testing..."
deploy:
stage: deploy
trigger:
include: deploy.gitlab-ci.yml
strategy: depend
resource_group: AWS-production
```
```yaml
# deploy.gitlab-ci.yml (child pipeline)
stages:
- provision
- deploy
provision:
stage: provision
script: echo "Provisioning..."
deployment:
stage: deploy
script: echo "Deploying..."
```
You must define [`strategy: depend`](../yaml/index.md#triggerstrategy)
with the `trigger` keyword. This ensures that the lock isn't released until the downstream pipeline
finishes.
## API

View File

@ -3941,20 +3941,24 @@ In this example, a new pipeline causes a running pipeline to be:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15536) in GitLab 12.7.
Sometimes running multiple jobs at the same time in an environment
can lead to errors during the deployment.
Use `resource_group` to create a [resource group](../resource_groups/index.md) that
ensures a job is mutually exclusive across different pipelines for the same project.
To avoid these errors, use the `resource_group` attribute to make sure that
the runner doesn't run certain jobs concurrently. Resource groups behave similar
to semaphores in other programming languages.
For example, if multiple jobs that belong to the same resource group are queued simultaneously,
only one of the jobs starts. The other jobs wait until the `resource_group` is free.
When the `resource_group` keyword is defined for a job in the `.gitlab-ci.yml` file,
job executions are mutually exclusive across different pipelines for the same project.
If multiple jobs belonging to the same resource group are enqueued simultaneously,
only one of the jobs is picked by the runner. The other jobs wait until the
`resource_group` is free.
Resource groups behave similar to semaphores in other programming languages.
For example:
You can define multiple resource groups per environment. For example,
when deploying to physical devices, you might have multiple physical devices. Each device
can be deployed to, but only one deployment can occur per device at any given time.
**Keyword type**: Job keyword. You can use it only as part of a job.
**Possible inputs**: Only letters, digits, `-`, `_`, `/`, `$`, `{`, `}`, `.`, and spaces.
It can't start or end with `/`.
**Example of `resource_group`**:
```yaml
deploy-to-production:
@ -3962,71 +3966,12 @@ deploy-to-production:
resource_group: production
```
In this case, two `deploy-to-production` jobs in two separate pipelines can never run at the same time. As a result,
In this example, two `deploy-to-production` jobs in two separate pipelines can never run at the same time. As a result,
you can ensure that concurrent deployments never happen to the production environment.
You can define multiple resource groups per environment. For example,
when deploying to physical devices, you may have multiple physical devices. Each device
can be deployed to, but there can be only one deployment per device at any given time.
**Related topics**:
The `resource_group` value can only contain letters, digits, `-`, `_`, `/`, `$`, `{`, `}`, `.`, and spaces.
It can't start or end with `/`.
For more information, see [Resource Group documentation](../resource_groups/index.md).
#### Pipeline-level concurrency control with Cross-Project/Parent-Child pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/39057) in GitLab 13.9.
You can define `resource_group` for downstream pipelines that are sensitive to concurrent
executions. The [`trigger` keyword](#trigger) can trigger downstream pipelines. The
[`resource_group` keyword](#resource_group) can co-exist with it. This is useful to control the
concurrency for deployment pipelines, while running non-sensitive jobs concurrently.
The following example has two pipeline configurations in a project. When a pipeline starts running,
non-sensitive jobs are executed first and aren't affected by concurrent executions in other
pipelines. However, GitLab ensures that there are no other deployment pipelines running before
triggering a deployment (child) pipeline. If other deployment pipelines are running, GitLab waits
until those pipelines finish before running another one.
```yaml
# .gitlab-ci.yml (parent pipeline)
build:
stage: build
script: echo "Building..."
test:
stage: test
script: echo "Testing..."
deploy:
stage: deploy
trigger:
include: deploy.gitlab-ci.yml
strategy: depend
resource_group: AWS-production
```
```yaml
# deploy.gitlab-ci.yml (child pipeline)
stages:
- provision
- deploy
provision:
stage: provision
script: echo "Provisioning..."
deployment:
stage: deploy
script: echo "Deploying..."
```
You must define [`strategy: depend`](#triggerstrategy)
with the `trigger` keyword. This ensures that the lock isn't released until the downstream pipeline
finishes.
- [Pipeline-level concurrency control with cross-project/parent-child pipelines](../resource_groups/index.md#pipeline-level-concurrency-control-with-cross-projectparent-child-pipelines).
### `release`

View File

@ -145,7 +145,7 @@ Legacy classes inherited from `BaseService` for historical reasons.
In Service classes the use of `execute` and `#execute` is preferred over `call` and `#call`.
Classes that are not service objects should be [created elsewhere](directory_structure.md#use-namespaces-to-define-bounded-contexts, such as in `lib`.
Classes that are not service objects should be [created elsewhere](directory_structure.md#use-namespaces-to-define-bounded-contexts), such as in `lib`.
#### ServiceResponse

View File

@ -24,6 +24,7 @@ You might receive notifications for one of the following reasons:
or edit, or someone mentions you.
- You've [enabled notifications in an issue, merge request, or epic](#notifications-on-issues-merge-requests-and-epics).
- You've configured notifications for the [project](#change-level-of-project-notifications) or [group](#group-notifications).
- You're subscribed to group or project pipeline notifications via the pipeline emails [integration](../project/integrations/overview.md).
NOTE:
Administrators can block notifications, preventing them from being sent.

View File

@ -1110,6 +1110,9 @@ msgstr ""
msgid "(check progress)"
msgstr ""
msgid "(commits will be squashed)"
msgstr ""
msgid "(deleted)"
msgstr ""
@ -12424,6 +12427,9 @@ msgstr ""
msgid "Edit comment"
msgstr ""
msgid "Edit commit message"
msgstr ""
msgid "Edit deploy freeze"
msgstr ""
@ -21588,6 +21594,9 @@ msgstr ""
msgid "Merge commit message"
msgstr ""
msgid "Merge details"
msgstr ""
msgid "Merge events"
msgstr ""
@ -32449,6 +32458,12 @@ msgstr ""
msgid "Source branch"
msgstr ""
msgid "Source branch will be deleted."
msgstr ""
msgid "Source branch will not be deleted."
msgstr ""
msgid "Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}"
msgstr ""
@ -40956,7 +40971,7 @@ msgstr ""
msgid "most recent deployment"
msgstr ""
msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}%{squashedCommits}."
msgstr ""
msgid "mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}."

View File

@ -9,7 +9,7 @@ exports[`New ready to merge state component renders permission text if canMerge
/>
<p
class="media-body gl-m-0! gl-font-weight-bold"
class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
>
Ready to merge by members who can write to the target branch.
@ -27,7 +27,7 @@ exports[`New ready to merge state component renders permission text if canMerge
/>
<p
class="media-body gl-m-0! gl-font-weight-bold"
class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
>
Ready to merge!

View File

@ -1,4 +1,4 @@
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue';
@ -6,7 +6,7 @@ describe('Commits header component', () => {
let wrapper;
const createComponent = (props) => {
wrapper = shallowMount(CommitsHeader, {
wrapper = mount(CommitsHeader, {
stubs: {
GlSprintf,
},

View File

@ -1,4 +1,4 @@
import { GlFormCheckbox } from '@gitlab/ui';
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue';
import { SQUASH_BEFORE_MERGE } from '~/vue_merge_request_widget/i18n';
@ -77,7 +77,7 @@ describe('Squash before merge component', () => {
value: false,
});
const aboutLink = wrapper.find('a');
const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.exists()).toBe(false);
});
@ -88,7 +88,7 @@ describe('Squash before merge component', () => {
helpPath: 'test-path',
});
const aboutLink = wrapper.find('a');
const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.exists()).toBe(true);
});
@ -99,7 +99,7 @@ describe('Squash before merge component', () => {
helpPath: 'test-path',
});
const aboutLink = wrapper.find('a');
const aboutLink = wrapper.findComponent(GlLink);
expect(aboutLink.attributes('href')).toEqual('test-path');
});

View File

@ -74,6 +74,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('nothingToMerge');
context.commitsCount = 1;
context.branchMissing = true;
expect(bound()).toEqual('missingBranch');

View File

@ -6,7 +6,7 @@ shared_examples 'deployment metrics examples' do
environment = project.environments.production.first || create(:environment, :production, project: project)
create(:deployment, :success, args.merge(environment: environment))
# this is needed for the dora_deployment_frequency_in_vsa feature flag so we have aggregated data
# this is needed for the DORA API so we have aggregated data
::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee?
end

27
tooling/bin/find_change_diffs Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'gitlab'
require 'pathname'
# This script saves the diffs of changes in an MR to the directory specified as the first argument
gitlab_token = ENV.fetch('PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE')
gitlab_endpoint = ENV.fetch('CI_API_V4_URL')
mr_project_path = ENV.fetch('CI_MERGE_REQUEST_PROJECT_PATH')
mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
abort("ERROR: Please specify a directory to write MR diffs into.") if ARGV.empty?
output_diffs_dir = Pathname.new(ARGV.shift).expand_path
Gitlab.configure do |config|
config.endpoint = gitlab_endpoint
config.private_token = gitlab_token
end
Gitlab.merge_request_changes(mr_project_path, mr_iid).changes.each do |change|
next if change['diff'].empty?
output_diffs_dir.join(File.dirname(change['new_path'])).mkpath
output_diffs_dir.join("#{change['new_path']}.diff").write(change['diff'])
end

View File

@ -0,0 +1,18 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'pathname'
# This script assumes the first argument is a directory of files containing diffs of changes from an MR. It exits with a
# success code if all diffs add a line that quarantines a test. If any diffs are not specs, or they are specs that don't
# quarantine a test, it exits with code 1 to indicate failure (i.e., there was _not_ only quarantined specs).
abort("ERROR: Please specify the directory containing MR diffs.") if ARGV.empty?
diffs_dir = Pathname.new(ARGV.shift).expand_path
diffs_dir.glob('**/*').each do |path|
next if path.directory?
exit 1 unless path.to_s.end_with?('_spec.rb.diff')
exit 1 unless path.read.match?(/^\+.*, quarantine:/)
end