Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
218585fc85
commit
cb8835f38a
|
|
@ -6,6 +6,7 @@ Rails/AvoidTimeComparison:
|
|||
- 'app/workers/container_registry/migration/enqueuer_worker.rb'
|
||||
- 'app/workers/gitlab/import/advance_stage.rb'
|
||||
- 'ee/app/services/incident_management/pending_escalations/process_service.rb'
|
||||
- 'ee/app/services/phone_verification/users/send_verification_code_service.rb'
|
||||
- 'ee/app/workers/update_all_mirrors_worker.rb'
|
||||
- 'lib/gitlab/chaos.rb'
|
||||
- 'lib/gitlab/database/background_migration/batched_migration.rb'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import diffGeneratedSubscription from '~/pages/projects/merge_requests/queries/diff_generated.subscription.graphql';
|
||||
|
||||
import createApolloClient from '../lib/graphql';
|
||||
|
||||
import { getDerivedMergeRequestInformation } from '../diffs/utils/merge_request';
|
||||
import { EVT_MR_PREPARED } from '../diffs/constants';
|
||||
import { EVT_MR_PREPARED, EVT_MR_DIFF_GENERATED } from '../diffs/constants';
|
||||
|
||||
import getMr from '../graphql_shared/queries/merge_request.query.graphql';
|
||||
import mrPreparation from '../graphql_shared/subscriptions/merge_request_prepared.subscription.graphql';
|
||||
|
|
@ -48,9 +50,32 @@ async function observeMergeRequestFinishingPreparation({ apollo, signaler }) {
|
|||
}
|
||||
}
|
||||
|
||||
function observeMergeRequestDiffGenerated({ apollo, signaler }) {
|
||||
const tabCount = document.querySelector('.js-changes-tab-count');
|
||||
|
||||
if (!tabCount) return;
|
||||
|
||||
const susbription = apollo.subscribe({
|
||||
query: diffGeneratedSubscription,
|
||||
variables: {
|
||||
issuableId: tabCount.dataset.gid,
|
||||
},
|
||||
});
|
||||
|
||||
susbription.subscribe(({ data: { mergeRequestDiffGenerated } }) => {
|
||||
if (mergeRequestDiffGenerated) {
|
||||
signaler.$emit(EVT_MR_DIFF_GENERATED, mergeRequestDiffGenerated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function start({
|
||||
signalBus = required('signalBus'),
|
||||
apolloClient = createApolloClient(),
|
||||
} = {}) {
|
||||
if (window.gon?.features?.mergeRequestDiffGeneratedSubscription) {
|
||||
observeMergeRequestDiffGenerated({ signaler: signalBus, apollo: apolloClient });
|
||||
}
|
||||
|
||||
await observeMergeRequestFinishingPreparation({ signaler: signalBus, apollo: apolloClient });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export const RENAMED_DIFF_TRANSITIONS = {
|
|||
};
|
||||
|
||||
// MR Diffs known events
|
||||
export const EVT_MR_DIFF_GENERATED = 'mr:diffGenerated';
|
||||
export const EVT_MR_PREPARED = 'mr:asyncPreparationFinished';
|
||||
export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles';
|
||||
export const EVT_DISCUSSIONS_ASSIGNED = 'mr:diffs:discussionsAssigned';
|
||||
|
|
|
|||
|
|
@ -68,10 +68,15 @@ export default {
|
|||
projectPath: { default: null },
|
||||
sourceProjectPath: { default: null },
|
||||
title: { default: '' },
|
||||
tabs: { default: () => [] },
|
||||
isFluidLayout: { default: false },
|
||||
blocksMerge: { default: false },
|
||||
},
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isStickyHeaderVisible: false,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import initMrNotes from 'ee_else_ce/mr_notes';
|
|||
import StickyHeader from '~/merge_requests/components/sticky_header.vue';
|
||||
import { start as startCodeReviewMessaging } from '~/code_review/signals';
|
||||
import diffsEventHub from '~/diffs/event_hub';
|
||||
import { EVT_MR_DIFF_GENERATED } from '~/diffs/constants';
|
||||
import store from '~/mr_notes/stores';
|
||||
import initSidebarBundle from '~/sidebar/sidebar_bundle';
|
||||
import { apolloProvider } from '~/graphql_shared/issuable_client';
|
||||
|
|
@ -14,11 +15,23 @@ import getStateQuery from './queries/get_state.query.graphql';
|
|||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const tabData = Vue.observable({
|
||||
tabs: [],
|
||||
});
|
||||
|
||||
export function initMrPage() {
|
||||
initMrNotes();
|
||||
initShow(store);
|
||||
initMrMoreDropdown();
|
||||
startCodeReviewMessaging({ signalBus: diffsEventHub });
|
||||
|
||||
const changesCountBadge = document.querySelector('.js-changes-tab-count');
|
||||
diffsEventHub.$on(EVT_MR_DIFF_GENERATED, (mergeRequestDiffGenerated) => {
|
||||
const { fileCount } = mergeRequestDiffGenerated.diffStatsSummary;
|
||||
|
||||
changesCountBadge.textContent = fileCount;
|
||||
Vue.set(tabData.tabs[tabData.tabs.length - 1], 3, fileCount);
|
||||
});
|
||||
}
|
||||
|
||||
requestIdleCallback(() => {
|
||||
|
|
@ -38,6 +51,8 @@ requestIdleCallback(() => {
|
|||
blocksMerge,
|
||||
} = JSON.parse(data);
|
||||
|
||||
tabData.tabs = tabs;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
|
|
@ -48,13 +63,16 @@ requestIdleCallback(() => {
|
|||
iid,
|
||||
projectPath,
|
||||
title,
|
||||
tabs,
|
||||
isFluidLayout: parseBoolean(isFluidLayout),
|
||||
blocksMerge: parseBoolean(blocksMerge),
|
||||
sourceProjectPath,
|
||||
},
|
||||
render(h) {
|
||||
return h(StickyHeader);
|
||||
return h(StickyHeader, {
|
||||
props: {
|
||||
tabs: tabData.tabs,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
subscription diffGeneratedSubscription($issuableId: IssuableID!) {
|
||||
mergeRequestDiffGenerated(issuableId: $issuableId) {
|
||||
... on MergeRequest {
|
||||
id
|
||||
diffStatsSummary {
|
||||
fileCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:merge_blocked_component, current_user)
|
||||
push_frontend_feature_flag(:mention_autocomplete_backend_filtering, project)
|
||||
push_frontend_feature_flag(:pinned_file, project)
|
||||
push_frontend_feature_flag(:merge_request_diff_generated_subscription, project)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :discussions]
|
||||
|
|
|
|||
|
|
@ -277,7 +277,9 @@ class Member < ApplicationRecord
|
|||
after_create :create_notification_setting, unless: [:pending?, :importing?]
|
||||
after_create :post_create_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
|
||||
after_create :update_two_factor_requirement, unless: :invite?
|
||||
after_create :create_organization_user_record
|
||||
after_update :post_update_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
|
||||
after_update :create_organization_user_record, if: :saved_change_to_user_id? # only occurs on invite acceptance
|
||||
after_destroy :destroy_notification_setting
|
||||
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
|
||||
after_destroy :update_two_factor_requirement, unless: :invite?
|
||||
|
|
@ -288,14 +290,6 @@ class Member < ApplicationRecord
|
|||
refresh_member_authorized_projects
|
||||
end
|
||||
|
||||
after_create if: :update_organization_user? do
|
||||
Organizations::OrganizationUser.upsert(
|
||||
{ organization_id: source.organization_id, user_id: user_id, access_level: :default },
|
||||
unique_by: [:organization_id, :user_id],
|
||||
on_duplicate: :skip # Do not change access_level, could make :owner :default
|
||||
)
|
||||
end
|
||||
|
||||
attribute :notification_level, default: -> { NotificationSetting.levels[:global] }
|
||||
|
||||
class << self
|
||||
|
|
@ -668,12 +662,6 @@ class Member < ApplicationRecord
|
|||
user&.project_bot?
|
||||
end
|
||||
|
||||
def update_organization_user?
|
||||
return false unless Feature.enabled?(:update_organization_users, source.root_ancestor, type: :gitlab_com_derisk)
|
||||
|
||||
!invite? && source.organization.present?
|
||||
end
|
||||
|
||||
def log_invitation_token_cleanup
|
||||
return true unless Gitlab.com? && invite? && invite_accepted_at?
|
||||
|
||||
|
|
@ -684,6 +672,18 @@ class Member < ApplicationRecord
|
|||
def event_service
|
||||
EventCreateService.new # rubocop:todo CodeReuse/ServiceClass -- Legacy, convert to value object eventually
|
||||
end
|
||||
|
||||
def create_organization_user_record
|
||||
return if Feature.disabled?(:update_organization_users, source.root_ancestor, type: :gitlab_com_derisk)
|
||||
return if invite?
|
||||
return if source.organization.blank?
|
||||
|
||||
Organizations::OrganizationUser.upsert(
|
||||
{ organization_id: source.organization_id, user_id: user_id, access_level: :default },
|
||||
unique_by: [:organization_id, :user_id],
|
||||
on_duplicate: :skip # Do not change access_level, could make :owner :default
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
Member.prepend_mod_with('Member')
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
= render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab js-diffs-tab", id: "diffs-tab", testid: "diffs-tab" do
|
||||
= tab_link_for @merge_request, :diffs do
|
||||
= _("Changes")
|
||||
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { size: :sm }
|
||||
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { size: :sm, class: 'js-changes-tab-count', data: { gid: @merge_request.to_gid.to_s } }
|
||||
.d-flex.flex-wrap.align-items-center.justify-content-lg-end
|
||||
#js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
|
||||
- if moved_mr_sidebar_enabled?
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: bitbucket_importer_exponential_backoff
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136842
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432379
|
||||
milestone: '16.7'
|
||||
type: development
|
||||
group: group::import and integrate
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# This is a template for a "Whats New" release.
|
||||
# A release typically contains multiple entries of features that we'd like to highlight.
|
||||
#
|
||||
# Below is an example of what a single entry should look like, it's required attributes,
|
||||
# and what types we expect those attribute values to be. All attributes are required.
|
||||
#
|
||||
# For more information please refer to the handbook documentation here:
|
||||
# https://about.gitlab.com/handbook/marketing/blog/release-posts/index.html#create-mr-for-whats-new-entries
|
||||
#
|
||||
# Please delete this line and above before submitting your merge request.
|
||||
|
||||
- name: GCP Secret Manager support # Match the release post entry
|
||||
description: | # Do not modify this line, instead modify the lines below.
|
||||
Secrets stored in GCP Secrets Manager can now be easily retrieved and used in CI/CD jobs. Our new integration simplifies the process of interacting with GCP Secrets Manager through GitLab CI/CD, helping you streamline your build and deploy processes! This is just one of the many ways [GitLab and Google Cloud are better together](https://about.gitlab.com/blog/2023/08/29/gitlab-google-partnership-s3c/)!
|
||||
stage: Verify # String value of the stage that the feature was created in. e.g., Growth
|
||||
self-managed: true # Boolean value (true or false)
|
||||
gitlab-com: true # Boolean value (true or false)
|
||||
available_in: [Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
|
||||
documentation_link: https://docs.gitlab.com/ee/ci/secrets/gcp_secret_manager.html # This is the documentation URL, but can be a URL to a video if there is one
|
||||
image_url: https://about.gitlab.com/images/16_8/gcp_secrets_mgr.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
|
||||
published_at: 2024-01-18 # YYYY-MM-DD
|
||||
release: 16.8 # XX.Y
|
||||
|
||||
- name: Enforce 2FA for GitLab administrators # Match the release post entry
|
||||
description: | # Do not modify this line, instead modify the lines below.
|
||||
You can now enforce whether GitLab administrators are required to use two-factor authentication (2FA) in their self-managed instance. It is good security practice to use 2FA for all accounts, especially for privileged accounts like administrators. If this setting is enforced, and an administrator does not already use 2FA, they must set 2FA up on their next sign-in.
|
||||
stage: Govern # String value of the stage that the feature was created in. e.g., Growth
|
||||
self-managed: true # Boolean value (true or false)
|
||||
gitlab-com: false # Boolean value (true or false)
|
||||
available_in: [Free, Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
|
||||
documentation_link: https://docs.gitlab.com/ee/security/two_factor_authentication.html#enforce-2fa-for-administrator-users # This is the documentation URL, but can be a URL to a video if there is one
|
||||
image_url: https://img.youtube.com/vi/fHleeXzoB6k/hqdefault.jpg # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
|
||||
published_at: 2024-01-18 # YYYY-MM-DD
|
||||
release: 16.8 # XX.Y
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChangeICodeReviewCreateMrKeysFromHllToInteger < Gitlab::Database::Migration[2.2]
|
||||
milestone '16.9'
|
||||
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
REDIS_HLL_PREFIX = '{hll_counters}_i_code_review_create_mr'
|
||||
REDIS_PREFIX = '{event_counters}_i_code_review_user_create_mr'
|
||||
|
||||
def up
|
||||
# For each old (redis_hll) counter we find the corresponding target (redis) counter and add
|
||||
# old value to migrate a metric. If the Redis counter does not exist, it will get created.
|
||||
# Since the RedisHLL keys expire after 6 weeks, we will migrate 6 keys at the most.
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.scan_each(match: "#{REDIS_HLL_PREFIX}-*", count: 10_000) do |key|
|
||||
redis_key = key.sub(REDIS_HLL_PREFIX, REDIS_PREFIX)
|
||||
redis_hll_value = redis.pfcount(key)
|
||||
|
||||
redis.incrby(redis_key, redis_hll_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
c796d14931b4da8791fabaaf67307119273a3ee2affaa56359de1a7203e4ca03
|
||||
|
|
@ -664,6 +664,38 @@ For example: index the content of `spec:` section for CI components.
|
|||
|
||||
See an [example of development workflow](dev_workflow.md) for a components repository.
|
||||
|
||||
### GitLab-maintained catalog resources
|
||||
|
||||
GitLab provides catalog resources for all SaaS projects to use. To communicate a clear ownership
|
||||
such projects will be located inside `components` top-level group.
|
||||
Additionally, we will mark those projects as `Verified creator` to increase trust.
|
||||
|
||||
The `components` group is not just a development space but also a feature of GitLab product.
|
||||
Users anywhere in GitLab can reference components located inside this group.
|
||||
|
||||
Each project under `components` must be owned explicitly by the team that owns
|
||||
the related product category. For example: components related to SAST are owned by the team that
|
||||
maintains the SAST product.
|
||||
|
||||
Other main alternatives considered were:
|
||||
|
||||
- A subgroup `gitlab-org/gitlab-components`.
|
||||
- This had the advantage of clarifying who owns this group (GitLab organization).
|
||||
- The disadvantage where verbose path and the fact that components are also features
|
||||
of the product and deserve a short-hand path. In addition, components from SaaS should
|
||||
be importable on self-managed instances and having a `gitlab-org` origin group makes it
|
||||
confusing and more sensitive to naming conflicts.
|
||||
- A new top-level group `gitlab-components`.
|
||||
- This had the advantage of having a less verbose path but the `gitlab-` prefix was redundant.
|
||||
- This requires AppSec to duplicate security and compliance standards that are
|
||||
already applied to existing GitLab groups. We still ended up doing this for the `components`
|
||||
group but the tradeoff was that `components` group is part of GitLab features and deserves
|
||||
a separate dev environment than `gitlab-org`.
|
||||
- The existing `gitlab-community/cicd-components` which is used by community contributors.
|
||||
- The advantage was that AppSec already has controls the security and compliance for this group.
|
||||
- The disadvantage is that `gitlab-community` mainly contains forks from `gitlab-org` and
|
||||
this could be confusing.
|
||||
|
||||
## Implementation guidelines
|
||||
|
||||
- Start with the smallest user base. Dogfood the feature for `gitlab-org` and
|
||||
|
|
|
|||
|
|
@ -225,3 +225,23 @@ HTTP redirects are not followed and omitting `.git` can result in a 301 error:
|
|||
```plaintext
|
||||
13:fetch remote: "fatal: unable to access 'https://gitlab.com/group/project': The requested URL returned error: 301\n": exit status 128.
|
||||
```
|
||||
|
||||
## Push mirror from GitLab instance to Geo secondary fails: `The requested URL returned error: 302`
|
||||
|
||||
Push mirroring of a GitLab repository using the HTTP or HTTPS protocols fails when the destination
|
||||
is a Geo secondary node due to the proxying of the push request to the Geo primary node,
|
||||
and the following error is displayed:
|
||||
|
||||
```plaintext
|
||||
13:get remote references: create git ls-remote: exit status 128, stderr: "fatal: unable to access 'https://gitlab.example.com/group/destination.git/': The requested URL returned error: 302".
|
||||
```
|
||||
|
||||
This occurs when a Geo unified URL is configured and the target host name resolves to the secondary node's IP address.
|
||||
|
||||
The error can be avoided by:
|
||||
|
||||
- Configuring the push mirror to use the SSH protocol. However, the repository must not contain any
|
||||
LFS objects, which are always transferred over HTTP or HTTPS and are still redirected.
|
||||
- Using a reverse proxy to direct all requests from the source instance to the primary Geo node.
|
||||
- Adding a local `hosts` file entry on the source to force the target host name to resolve to the Geo primary node's IP address.
|
||||
- Configuring a pull mirror on the target instead.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ provided that the agent is properly configured for remote development.
|
|||
| [`network_policy`](#network_policy) | Firewall rules for workspaces. |
|
||||
| [`default_resources_per_workspace_container`](#default_resources_per_workspace_container) | Default requests and limits for CPU and memory per workspace container. |
|
||||
| [`max_resources_per_workspace`](#max_resources_per_workspace) | Maximum requests and limits for CPU and memory per workspace. |
|
||||
| [`workspaces_quota`](#workspaces_quota) | Maximum number of workspaces for the GitLab agent. |
|
||||
| [`workspaces_per_user_quota`](#workspaces_per_user_quota) | Maximum number of workspaces per user. |
|
||||
|
||||
NOTE:
|
||||
If a setting has an invalid value, it's not possible to update any setting until you fix that value.
|
||||
|
|
@ -202,6 +204,52 @@ remote_development:
|
|||
The maximum resources you define must include any resources required for init containers
|
||||
to perform bootstrapping operations such as cloning the project repository.
|
||||
|
||||
### `workspaces_quota`
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/11586) in GitLab 16.9.
|
||||
|
||||
Use this setting to set the maximum number of workspaces for the GitLab agent.
|
||||
|
||||
You cannot create new workspaces for an agent when:
|
||||
|
||||
- The number of workspaces for the agent has reached the defined `workspaces_quota`.
|
||||
- `workspaces_quota` is set to `0`.
|
||||
|
||||
This setting does not affect existing workspaces for the agent.
|
||||
|
||||
The default value is `-1` (unlimited).
|
||||
Possible values are greater than or equal to `-1`.
|
||||
|
||||
**Example configuration:**
|
||||
|
||||
```yaml
|
||||
remote_development:
|
||||
workspaces_quota: 10
|
||||
```
|
||||
|
||||
### `workspaces_per_user_quota`
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/11586) in GitLab 16.9.
|
||||
|
||||
Use this setting to set the maximum number of workspaces per user.
|
||||
|
||||
You cannot create new workspaces for a user when:
|
||||
|
||||
- The number of workspaces for the user has reached the defined `workspaces_per_user_quota`.
|
||||
- `workspaces_per_user_quota` is set to `0`.
|
||||
|
||||
This setting does not affect existing workspaces for the user.
|
||||
|
||||
The default value is `-1` (unlimited).
|
||||
Possible values are greater than or equal to `-1`.
|
||||
|
||||
**Example configuration:**
|
||||
|
||||
```yaml
|
||||
remote_development:
|
||||
workspaces_per_user_quota: 3
|
||||
```
|
||||
|
||||
## Configuring user access with remote development
|
||||
|
||||
You can configure the `user_access` module to access the connected Kubernetes cluster with your GitLab credentials.
|
||||
|
|
|
|||
|
|
@ -24,13 +24,9 @@ module Bitbucket
|
|||
def get(path, extra_query = {})
|
||||
refresh! if expired?
|
||||
|
||||
response = if Feature.enabled?(:bitbucket_importer_exponential_backoff)
|
||||
retry_with_exponential_backoff do
|
||||
connection.get(build_url(path), params: @default_query.merge(extra_query))
|
||||
end
|
||||
else
|
||||
connection.get(build_url(path), params: @default_query.merge(extra_query))
|
||||
end
|
||||
response = retry_with_exponential_backoff do
|
||||
connection.get(build_url(path), params: @default_query.merge(extra_query))
|
||||
end
|
||||
|
||||
response.parsed
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def interpolate!
|
||||
return errors.push(config.error) unless config.valid?
|
||||
return errors.concat(config.errors) unless config.valid?
|
||||
|
||||
if inputs_without_header?
|
||||
return errors.push(
|
||||
_('Given inputs not defined in the `spec` section of the included configuration file'))
|
||||
end
|
||||
|
||||
return @result ||= config.content unless config.has_header?
|
||||
return @result ||= config.content unless config.header
|
||||
|
||||
return errors.concat(header.errors) unless header.valid?
|
||||
return errors.concat(inputs.errors) unless inputs.valid?
|
||||
|
|
@ -65,7 +65,7 @@ module Gitlab
|
|||
attr_reader :config, :input_args, :variables
|
||||
|
||||
def inputs_without_header?
|
||||
input_args.any? && !config.has_header?
|
||||
input_args.any? && !config.header
|
||||
end
|
||||
|
||||
def header
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
class Config
|
||||
module Yaml
|
||||
class Documents
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :errors
|
||||
|
||||
def initialize(documents)
|
||||
@documents = documents
|
||||
@errors = []
|
||||
|
||||
parsed_first_document
|
||||
end
|
||||
|
||||
def valid?
|
||||
errors.none?
|
||||
end
|
||||
|
||||
def header
|
||||
return unless has_header?
|
||||
|
||||
parsed_first_document
|
||||
end
|
||||
|
||||
def content
|
||||
return documents.last.raw if has_header?
|
||||
|
||||
documents.first&.raw || ''
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :documents
|
||||
|
||||
def has_header?
|
||||
return false unless parsed_first_document.is_a?(Hash)
|
||||
|
||||
documents.count > 1 && parsed_first_document.key?(:spec)
|
||||
end
|
||||
|
||||
def parsed_first_document
|
||||
return {} if documents.count == 0
|
||||
|
||||
documents.first.load!
|
||||
rescue ::Gitlab::Config::Loader::FormatError => e
|
||||
errors << e.message
|
||||
end
|
||||
strong_memoize_attr :parsed_first_document
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -13,7 +13,10 @@ module Gitlab
|
|||
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :raw
|
||||
|
||||
def initialize(config, additional_permitted_classes: [])
|
||||
@raw = config
|
||||
@config = YAML.safe_load(config,
|
||||
permitted_classes: [Symbol, *additional_permitted_classes],
|
||||
permitted_symbols: [],
|
||||
|
|
|
|||
|
|
@ -38414,7 +38414,7 @@ msgstr ""
|
|||
msgid "ProjectSettings|Configure your infrastructure."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Connect your instance"
|
||||
msgid "ProjectSettings|Configure your instance"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Contact an admin to change this setting."
|
||||
|
|
@ -38615,7 +38615,7 @@ msgstr ""
|
|||
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Override instance analytics configuration for this project"
|
||||
msgid "ProjectSettings|Override the instance analytics configuration for this project."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Package registry"
|
||||
|
|
@ -40539,6 +40539,12 @@ msgstr ""
|
|||
msgid "RemoteDevelopment|Workspaces"
|
||||
msgstr ""
|
||||
|
||||
msgid "RemoteDevelopment|You cannot create a workspace because there are already \"%{count}\" existing workspaces for the given agent with a total quota of \"%{quota}\" workspaces"
|
||||
msgstr ""
|
||||
|
||||
msgid "RemoteDevelopment|You cannot create a workspace because you already have \"%{count}\" existing workspaces for the given agent with a per user quota of \"%{quota}\" workspaces"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { start } from '~/code_review/signals';
|
||||
import diffsEventHub from '~/diffs/event_hub';
|
||||
import { EVT_MR_PREPARED } from '~/diffs/constants';
|
||||
import { EVT_MR_PREPARED, EVT_MR_DIFF_GENERATED } from '~/diffs/constants';
|
||||
import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
|
||||
jest.mock('~/diffs/utils/merge_request');
|
||||
|
||||
|
|
@ -154,5 +155,86 @@ describe('~/code_review', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('observeMergeRequestDiffGenerated', () => {
|
||||
const callArgs = {};
|
||||
const apollo = {};
|
||||
let apolloSubscribeSpy;
|
||||
let subscribeSpy;
|
||||
let nextSpy;
|
||||
let observable;
|
||||
let emitSpy;
|
||||
let behavior;
|
||||
|
||||
beforeEach(() => {
|
||||
apolloSubscribeSpy = jest.fn();
|
||||
subscribeSpy = jest.fn();
|
||||
nextSpy = jest.fn();
|
||||
observable = {
|
||||
next: nextSpy,
|
||||
subscribe: subscribeSpy.mockReturnValue(),
|
||||
};
|
||||
emitSpy = jest.spyOn(diffsEventHub, '$emit');
|
||||
nextSpy.mockImplementation((data) => behavior?.(data));
|
||||
subscribeSpy.mockImplementation((handler) => {
|
||||
behavior = handler;
|
||||
});
|
||||
|
||||
apolloSubscribeSpy.mockReturnValue(observable);
|
||||
|
||||
apollo.subscribe = apolloSubscribeSpy;
|
||||
|
||||
callArgs.signalBus = io;
|
||||
callArgs.apolloClient = apollo;
|
||||
|
||||
getDerivedMergeRequestInformation.mockImplementationOnce(() => ({}));
|
||||
});
|
||||
|
||||
it('does not subscribe if the feature flag mergeRequestDiffGeneratedSubscription is disabled', async () => {
|
||||
await start(callArgs);
|
||||
|
||||
expect(apolloSubscribeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('with mergeRequestDiffGeneratedSubscription feature flag enabled', () => {
|
||||
beforeEach(() => {
|
||||
setHTMLFixture('<div class="js-changes-tab-count" data-gid="1"></div>');
|
||||
|
||||
window.gon.features = {
|
||||
mergeRequestDiffGeneratedSubscription: true,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.gon.features = {};
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('does not subscribe if the page is not a merge request', async () => {
|
||||
await start(callArgs);
|
||||
|
||||
expect(apolloSubscribeSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ variables: { issuableId: '1' } }),
|
||||
);
|
||||
expect(observable.subscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not emit an event when mergeRequestDiffGenerated is null', async () => {
|
||||
await start(callArgs);
|
||||
|
||||
observable.next({ data: { mergeRequestDiffGenerated: null } });
|
||||
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits an event', async () => {
|
||||
await start(callArgs);
|
||||
|
||||
observable.next({ data: { mergeRequestDiffGenerated: { totalCount: 1 } } });
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith(EVT_MR_DIFF_GENERATED, { totalCount: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,26 +56,6 @@ RSpec.describe Bitbucket::Connection, feature_category: :integrations do
|
|||
expect { connection.get('/users') }.to raise_error(Bitbucket::ExponentialBackoff::RateLimitError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the bitbucket_importer_exponential_backoff feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(bitbucket_importer_exponential_backoff: false)
|
||||
end
|
||||
|
||||
it 'does not run with exponential backoff' do
|
||||
expect_next_instance_of(described_class) do |instance|
|
||||
expect(instance).not_to receive(:retry_with_exponential_backoff).and_call_original
|
||||
end
|
||||
|
||||
expect_next_instance_of(OAuth2::AccessToken) do |instance|
|
||||
expect(instance).to receive(:get).and_return(double(parsed: true))
|
||||
end
|
||||
|
||||
connection = described_class.new({ token: token })
|
||||
|
||||
connection.get('/users')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#expired?' do
|
||||
|
|
|
|||
|
|
@ -3,23 +3,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_category: :pipeline_composition do
|
||||
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) }
|
||||
let(:arguments) { { website: 'gitlab.com' } }
|
||||
let(:content) { ::Gitlab::Config::Loader::Yaml.new("test: 'deploy $[[ inputs.website ]]'") }
|
||||
let(:header) { ::Gitlab::Config::Loader::Yaml.new("spec:\n inputs:\n website: ") }
|
||||
let(:documents) { ::Gitlab::Ci::Config::Yaml::Documents.new([header, content]) }
|
||||
|
||||
subject(:interpolator) { described_class.new(result, arguments, []) }
|
||||
subject(:interpolator) { described_class.new(documents, arguments, []) }
|
||||
|
||||
context 'when input data is valid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy $[[ inputs.website ]]'"
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'correctly interpolates the config' do
|
||||
interpolator.interpolate!
|
||||
|
||||
|
|
@ -29,14 +20,24 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
end
|
||||
end
|
||||
|
||||
context 'when config has a syntax error' do
|
||||
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') }
|
||||
context 'when interpolation is not used' do
|
||||
let(:arguments) { nil }
|
||||
let(:content) { ::Gitlab::Config::Loader::Yaml.new("test: 'deploy production'") }
|
||||
let(:documents) { ::Gitlab::Ci::Config::Yaml::Documents.new([content]) }
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
it 'returns original content' do
|
||||
interpolator.interpolate!
|
||||
|
||||
expect(interpolator).not_to be_interpolated
|
||||
expect(interpolator).to be_valid
|
||||
expect(interpolator.to_result).to eq("test: 'deploy production'")
|
||||
end
|
||||
end
|
||||
|
||||
it 'surfaces an error about invalid config' do
|
||||
context 'when the header has an error while being parsed' do
|
||||
let(:header) { ::Gitlab::Config::Loader::Yaml.new('_!@malformedyaml:&') }
|
||||
|
||||
it 'surfaces the error' do
|
||||
interpolator.interpolate!
|
||||
|
||||
expect(interpolator).not_to be_valid
|
||||
|
|
@ -45,9 +46,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
end
|
||||
|
||||
context 'when spec header is missing but inputs are specified' do
|
||||
let(:header) { nil }
|
||||
let(:content) { "test: 'echo'" }
|
||||
let(:arguments) { { foo: 'bar' } }
|
||||
let(:documents) { ::Gitlab::Ci::Config::Yaml::Documents.new([content]) }
|
||||
|
||||
it 'surfaces an error about invalid inputs' do
|
||||
interpolator.interpolate!
|
||||
|
|
@ -60,17 +59,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
end
|
||||
|
||||
context 'when spec header is invalid' do
|
||||
let(:header) do
|
||||
{ spec: { arguments: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy $[[ inputs.website ]]'"
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
let(:header) { ::Gitlab::Config::Loader::Yaml.new("spec:\n arguments:\n website:") }
|
||||
|
||||
it 'surfaces an error about invalid header' do
|
||||
interpolator.interpolate!
|
||||
|
|
@ -81,17 +70,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
end
|
||||
|
||||
context 'when provided interpolation argument is invalid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy $[[ inputs.website ]]'"
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: ['gitlab.com'] }
|
||||
end
|
||||
let(:arguments) { { website: ['gitlab.com'] } }
|
||||
|
||||
it 'returns an error about the invalid argument' do
|
||||
interpolator.interpolate!
|
||||
|
|
@ -102,17 +81,7 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
end
|
||||
|
||||
context 'when interpolation block is invalid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy $[[ inputs.abc ]]'"
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
let(:content) { ::Gitlab::Config::Loader::Yaml.new("test: 'deploy $[[ inputs.abc ]]'") }
|
||||
|
||||
it 'returns an error about the invalid block' do
|
||||
interpolator.interpolate!
|
||||
|
|
@ -123,16 +92,8 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
end
|
||||
|
||||
context 'when multiple interpolation blocks are invalid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]'"
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
::Gitlab::Config::Loader::Yaml.new("test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]'")
|
||||
end
|
||||
|
||||
it 'stops execution after the first invalid block' do
|
||||
|
|
@ -145,16 +106,24 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
|
||||
context 'when there are many invalid arguments' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: {
|
||||
allow_failure: { type: 'boolean' },
|
||||
image: nil,
|
||||
parallel: { type: 'number' },
|
||||
website: nil
|
||||
} } }
|
||||
::Gitlab::Config::Loader::Yaml.new(
|
||||
<<~YAML
|
||||
spec:
|
||||
inputs:
|
||||
allow_failure:
|
||||
type: boolean
|
||||
image:
|
||||
parallel:
|
||||
type: number
|
||||
website:
|
||||
YAML
|
||||
)
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy $[[ inputs.website ]] $[[ inputs.parallel ]] $[[ inputs.allow_failure ]] $[[ inputs.image ]]'"
|
||||
::Gitlab::Config::Loader::Yaml.new(
|
||||
"test: 'deploy $[[ inputs.website ]] $[[ inputs.parallel ]] $[[ inputs.allow_failure ]] $[[ inputs.image ]]'"
|
||||
)
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
|
|
@ -178,44 +147,4 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_result' do
|
||||
context 'when interpolation is not used' do
|
||||
let(:result) do
|
||||
::Gitlab::Ci::Config::Yaml::Result.new(config: content)
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy production'"
|
||||
end
|
||||
|
||||
let(:arguments) { nil }
|
||||
|
||||
it 'returns original content' do
|
||||
interpolator.interpolate!
|
||||
|
||||
expect(interpolator.to_result).to eq(content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation is available' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
"test: 'deploy $[[ inputs.website ]]'"
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'correctly interpolates content' do
|
||||
interpolator.interpolate!
|
||||
|
||||
expect(interpolator.to_result).to eq("test: 'deploy gitlab.com'")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Yaml::Documents, feature_category: :pipeline_composition do
|
||||
let(:documents) { described_class.new(yaml_documents) }
|
||||
|
||||
describe '#valid?' do
|
||||
context 'when there are no errors' do
|
||||
let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('job:')] }
|
||||
|
||||
it 'returns true' do
|
||||
expect(documents).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are errors' do
|
||||
let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('_!@malformedyaml:&')] }
|
||||
|
||||
it 'returns false' do
|
||||
expect(documents).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#header' do
|
||||
context 'when there are at least 2 documents and the first document has a `spec` keyword' do
|
||||
let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('spec:'), ::Gitlab::Config::Loader::Yaml.new('job:')] }
|
||||
|
||||
it 'returns the header' do
|
||||
expect(documents.header).to eq(spec: nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are fewer than 2 documents' do
|
||||
let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('job:')] }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(documents.header).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are at least 2 documents and the first document does not have a `spec` keyword' do
|
||||
let(:yaml_documents) do
|
||||
[::Gitlab::Config::Loader::Yaml.new('job1:'), ::Gitlab::Config::Loader::Yaml.new('job2:')]
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(documents.header).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#content' do
|
||||
context 'when there is a header' do
|
||||
let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('spec:'), ::Gitlab::Config::Loader::Yaml.new('job:')] }
|
||||
|
||||
it 'returns the unparsed content of the last document' do
|
||||
expect(documents.content).to eq('job:')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no header' do
|
||||
let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('job:')] }
|
||||
|
||||
it 'returns the unparsed content of the first document' do
|
||||
expect(documents.content).to eq('job:')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -208,4 +208,10 @@ RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_composi
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#raw' do
|
||||
it 'returns the unparsed YAML' do
|
||||
expect(loader.raw).to eq(yml)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe ChangeICodeReviewCreateMrKeysFromHllToInteger, :migration, :clean_gitlab_redis_cache, feature_category: :service_ping do
|
||||
def set_redis_hll(key, value)
|
||||
Gitlab::Redis::HLL.add(key: key, value: value, expiry: 6.weeks)
|
||||
end
|
||||
|
||||
def get_int_from_redis(key)
|
||||
Gitlab::Redis::SharedState.with { |redis| redis.get(key)&.to_i }
|
||||
end
|
||||
|
||||
describe "#up" do
|
||||
before do
|
||||
set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-16', value: 1)
|
||||
set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-16', value: 2)
|
||||
set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-47', value: 3)
|
||||
set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-48', value: 1)
|
||||
set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-49', value: 2)
|
||||
set_redis_hll('{hll_counters}_i_code_review_create_mr-2023-49', value: 4)
|
||||
set_redis_hll('{hll_counters}_some_other_event-2023-49', value: 7)
|
||||
end
|
||||
|
||||
it 'migrates all RedisHLL keys for i_code_review_create_mr', :aggregate_failures do
|
||||
migrate!
|
||||
|
||||
expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-16')).to eq(2)
|
||||
expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-47')).to eq(1)
|
||||
expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-48')).to eq(1)
|
||||
expect(get_int_from_redis('{event_counters}_i_code_review_user_create_mr-2023-49')).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not not migrate other RedisHLL keys' do
|
||||
migrate!
|
||||
|
||||
expect(get_int_from_redis('{event_counters}_some_other_event-2023-16')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1086,105 +1086,157 @@ RSpec.describe Member, feature_category: :groups_and_projects do
|
|||
|
||||
context 'for updating organization_users' do
|
||||
let_it_be(:group) { create(:group, :with_organization) }
|
||||
let(:member) { create(:group_member, source: group) }
|
||||
let(:update_organization_users_enabled) { true }
|
||||
|
||||
subject(:commit_member) { member }
|
||||
|
||||
before do
|
||||
allow(Organizations::OrganizationUser).to receive(:upsert).once.and_call_original
|
||||
stub_feature_flags(update_organization_users: update_organization_users_enabled)
|
||||
end
|
||||
|
||||
context 'when update_organization_users is enabled' do
|
||||
it 'inserts new record on member creation' do
|
||||
expect { member }.to change { Organizations::OrganizationUser.count }.by(1)
|
||||
record_attrs = { organization: group.organization, user: member.user, access_level: :default }
|
||||
expect(Organizations::OrganizationUser.exists?(record_attrs)).to be(true)
|
||||
end
|
||||
|
||||
context 'when user already exists in the organization_users' do
|
||||
context 'for an already existing default organization_user' do
|
||||
let_it_be(:project) { create(:project, group: group, organization: group.organization) }
|
||||
|
||||
before do
|
||||
member
|
||||
end
|
||||
|
||||
it 'does not insert a new record in organization_users' do
|
||||
expect do
|
||||
create(:project_member, :owner, source: project, user: member.user)
|
||||
end.not_to change { Organizations::OrganizationUser.count }
|
||||
|
||||
expect(
|
||||
Organizations::OrganizationUser.exists?(
|
||||
organization: project.organization, user: member.user, access_level: :default
|
||||
)
|
||||
).to be(true)
|
||||
end
|
||||
|
||||
it 'does not update timestamps' do
|
||||
travel_to(1.day.from_now) do
|
||||
expect do
|
||||
create(:project_member, :owner, source: project, user: member.user)
|
||||
end.not_to change { Organizations::OrganizationUser.last.updated_at }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an already existing owner organization_user' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:common_attrs) { { organization: group.organization, user: user } }
|
||||
|
||||
before_all do
|
||||
create(:organization_user, :owner, common_attrs)
|
||||
end
|
||||
|
||||
it 'does not insert a new record in organization_users nor update the access_level' do
|
||||
expect do
|
||||
create(:group_member, :owner, source: group, user: user)
|
||||
end.not_to change { Organizations::OrganizationUser.count }
|
||||
|
||||
expect(
|
||||
Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :default))
|
||||
).to be(false)
|
||||
expect(
|
||||
Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :owner))
|
||||
).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating the organization_users is not successful' do
|
||||
it 'rolls back the member creation' do
|
||||
allow(Organizations::OrganizationUser).to receive(:upsert).once.and_raise(ActiveRecord::StatementTimeout)
|
||||
|
||||
expect { member }.to raise_error(ActiveRecord::StatementTimeout)
|
||||
expect(Organizations::OrganizationUser.exists?(organization: group.organization)).to be(false)
|
||||
expect(group.group_members).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'does not create an organization_user entry' do
|
||||
specify do
|
||||
expect { member }.not_to change { Organizations::OrganizationUser.count }
|
||||
expect { commit_member }.not_to change { Organizations::OrganizationUser.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update_organization_users is disabled' do
|
||||
let(:update_organization_users_enabled) { false }
|
||||
context 'when creating' do
|
||||
let(:member) { create(:group_member, source: group) }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
context 'when update_organization_users is enabled' do
|
||||
it 'inserts new record on member creation' do
|
||||
expect { member }.to change { Organizations::OrganizationUser.count }.by(1)
|
||||
record_attrs = { organization: group.organization, user: member.user, access_level: :default }
|
||||
expect(Organizations::OrganizationUser.exists?(record_attrs)).to be(true)
|
||||
end
|
||||
|
||||
context 'when user already exists in the organization_users' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:common_attrs) { { organization: group.organization, user: user } }
|
||||
let(:new_member) { create(:group_member, :owner, source: group, user: user) }
|
||||
|
||||
context 'for an already existing default organization_user' do
|
||||
before_all do
|
||||
create(:organization_user, common_attrs)
|
||||
end
|
||||
|
||||
it 'does not insert a new record in organization_users' do
|
||||
expect { new_member }.not_to change { Organizations::OrganizationUser.count }
|
||||
expect(
|
||||
Organizations::OrganizationUser.exists?(
|
||||
organization: group.organization, user: user, access_level: :default
|
||||
)
|
||||
).to be(true)
|
||||
end
|
||||
|
||||
it 'does not update timestamps' do
|
||||
travel_to(1.day.from_now) do
|
||||
expect { new_member }.not_to change { Organizations::OrganizationUser.last.updated_at }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for an already existing owner organization_user' do
|
||||
before_all do
|
||||
create(:organization_user, :owner, common_attrs)
|
||||
end
|
||||
|
||||
it 'does not insert a new record in organization_users nor update the access_level' do
|
||||
expect do
|
||||
create(:group_member, :owner, source: group, user: user)
|
||||
end.not_to change { Organizations::OrganizationUser.count }
|
||||
|
||||
expect(
|
||||
Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :default))
|
||||
).to be(false)
|
||||
expect(
|
||||
Organizations::OrganizationUser.exists?(common_attrs.merge(access_level: :owner))
|
||||
).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating the organization_users is not successful' do
|
||||
it 'rolls back the member creation' do
|
||||
allow(Organizations::OrganizationUser).to receive(:upsert).once.and_raise(ActiveRecord::StatementTimeout)
|
||||
|
||||
expect { commit_member }.to raise_error(ActiveRecord::StatementTimeout)
|
||||
expect(Organizations::OrganizationUser.exists?(organization: group.organization)).to be(false)
|
||||
expect(group.group_members).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update_organization_users is disabled' do
|
||||
let(:update_organization_users_enabled) { false }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
end
|
||||
|
||||
context 'when member is an invite' do
|
||||
let(:member) { create(:group_member, :invited, source: group) }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
end
|
||||
|
||||
context 'when organization does not exist' do
|
||||
let(:member) { create(:group_member) }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when member is an invite' do
|
||||
let(:member) { create(:group_member, :invited, source: group) }
|
||||
context 'when updating' do
|
||||
context 'when member is an invite' do
|
||||
let_it_be(:member, reload: true) { create(:group_member, :invited, source: group) }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
end
|
||||
context 'when accepting the invite' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
context 'when organization does not exist' do
|
||||
let(:member) { create(:group_member) }
|
||||
subject(:commit_member) { member.accept_invite!(user) }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
context 'when update_organization_users is enabled' do
|
||||
it 'inserts new record on member creation' do
|
||||
expect { commit_member }.to change { Organizations::OrganizationUser.count }.by(1)
|
||||
expect(group.organization.user?(user)).to be(true)
|
||||
end
|
||||
|
||||
context 'when updating the organization_users is not successful' do
|
||||
before do
|
||||
allow(Organizations::OrganizationUser)
|
||||
.to receive(:upsert).once.and_raise(ActiveRecord::StatementTimeout)
|
||||
end
|
||||
|
||||
it 'rolls back the member creation' do
|
||||
expect { commit_member }.to raise_error(ActiveRecord::StatementTimeout)
|
||||
expect(group.organization.user?(user)).to be(false)
|
||||
expect(member.reset.user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update_organization_users is disabled' do
|
||||
let(:update_organization_users_enabled) { false }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
end
|
||||
|
||||
context 'when organization does not exist' do
|
||||
let_it_be(:member) { create(:group_member) }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating a non user_id attribute' do
|
||||
let_it_be(:member) { create(:group_member, :reporter, source: group) }
|
||||
|
||||
subject(:commit_member) { member.update!(access_level: GroupMember::DEVELOPER) }
|
||||
|
||||
it_behaves_like 'does not create an organization_user entry'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ RSpec.describe Tooling::Danger::ProjectHelper, feature_category: :tooling do
|
|||
|
||||
'db/schema.rb' | [:database]
|
||||
'db/structure.sql' | [:database]
|
||||
'db/docs/example.yml' | [:database]
|
||||
'db/migrate/foo' | [:database]
|
||||
'db/post_migrate/foo' | [:database]
|
||||
'ee/db/geo/migrate/foo' | [:database]
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ module Tooling
|
|||
%r{\Adoc/.*(\.(md|png|gif|jpg|yml))\z} => :docs,
|
||||
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
|
||||
%r{\Adata/whats_new/} => :docs,
|
||||
%r{\Adb/docs/.+\.yml\z} => :docs,
|
||||
%r{\Adata/deprecations/} => :none,
|
||||
%r{\Adata/removals/} => :none,
|
||||
|
||||
|
|
@ -100,6 +99,7 @@ module Tooling
|
|||
|
||||
%r{\A((ee|jh)/)?db/(geo/)?(?!click_house|fixtures)[^/]+} => [:database],
|
||||
%r{\A((ee|jh)/)?db/[^/]+\z} => [:database], # db/ root files
|
||||
%r{\Adb/docs/.+\.yml\z} => [:database],
|
||||
%r{\A((ee|jh)/)?lib/(ee/)?gitlab/(database|background_migration|sql)(/|\.rb)} => [:database, :backend],
|
||||
%r{\A(app/services/authorized_project_update/find_records_due_for_refresh_service)(/|\.rb)} => [:database, :backend],
|
||||
%r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => [:database, :backend],
|
||||
|
|
|
|||
Loading…
Reference in New Issue