Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-07-05 12:10:17 +00:00
parent 26d28ba159
commit 1c359370b3
97 changed files with 2175 additions and 486 deletions

View File

@ -8,3 +8,6 @@ include:
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "gitlab-utils"
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "click_house-client"

View File

@ -138,6 +138,7 @@ stages:
STATUS_SYM: ☠️
STATUS: failed
TYPE: "($QA_RUN_TYPE) "
ALLURE_JOB_NAME: $QA_RUN_TYPE
when: always
script:
- |

View File

@ -150,7 +150,6 @@ notify-slack:
variables:
QA_RSPEC_XML_FILE_PATTERN: ${CI_PROJECT_DIR}/qa/tmp/rspec-*.xml
RUN_WITH_BUNDLE: "true"
ALLURE_JOB_NAME: $QA_RUN_TYPE
when: on_failure
export-test-metrics:

View File

@ -193,10 +193,13 @@
- "lib/gitlab/setup_helper.rb"
.workhorse-patterns: &workhorse-patterns
- ".gitlab/ci/workhorse.gitlab-ci.yml"
- "GITLAB_WORKHORSE_VERSION"
- "workhorse/**/*"
- ".gitlab/ci/workhorse.gitlab-ci.yml"
- "scripts/gitaly-test-build"
- "scripts/gitaly-test-spawn"
- "spec/support/gitlab-git-test.git/**/*"
- "spec/support/helpers/gitaly_setup.rb"
.yaml-lint-patterns: &yaml-lint-patterns
- "**/*.{yml,yaml}{,.*}"

View File

@ -984,11 +984,6 @@ Cop/SidekiqApiUsage:
- 'config/initializers/sidekiq.rb'
- 'config/initializers/forbid_sidekiq_in_transactions.rb'
Rake/Require:
Include:
- '{,ee/,jh/}lib/**/*.rake'
- 'qa/tasks/**/*.rake'
Cop/FeatureFlagUsage:
Include:
- 'lib/gitlab/redis/**/*.rb'

View File

@ -147,7 +147,6 @@ Rails/OutputSafety:
- 'lib/gitlab/observability.rb'
- 'lib/gitlab/other_markup.rb'
- 'lib/gitlab/string_range_marker.rb'
- 'lib/gitlab/utils.rb'
- 'spec/helpers/application_helper_spec.rb'
- 'spec/helpers/notify_helper_spec.rb'
- 'spec/helpers/profiles_helper_spec.rb'

View File

@ -1 +1 @@
25fbd040740121576144a38413229d2cc571aca6
c9160a6d435d904bb72f3627c702192706ad4642

View File

@ -326,6 +326,7 @@ gem 'sassc-rails', '~> 2.1.0'
gem 'autoprefixer-rails', '10.2.5.1'
gem 'terser', '1.0.2'
gem 'click_house-client', path: 'gems/click_house-client', require: 'click_house/client'
gem 'addressable', '~> 2.8'
gem 'tanuki_emoji', '~> 0.6'
gem 'gon', '~> 6.4.0'

View File

@ -4,6 +4,14 @@ PATH
activerecord-gitlab (0.1.0)
activerecord (>= 6.1.7.3)
PATH
remote: gems/click_house-client
specs:
click_house-client (0.1.0)
activesupport (< 8)
addressable (~> 2.8)
json (~> 2.6.3)
PATH
remote: gems/gitlab-rspec
specs:
@ -1736,6 +1744,7 @@ DEPENDENCIES
carrierwave (~> 1.3)
charlock_holmes (~> 0.7.7)
circuitbox (= 2.0.0)
click_house-client!
cloud_profiler_agent (~> 0.0.0)!
commonmarker (~> 0.23.9)
concurrent-ruby (~> 1.1)

View File

@ -4,6 +4,7 @@ import $ from 'jquery';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { addMarkdownListeners, removeMarkdownListeners } from '~/lib/utils/text_markdown';
import { __ } from '~/locale';
// MarkdownPreview
@ -128,6 +129,8 @@ $(document).on('markdown-preview:show', (e, $form) => {
return;
}
removeMarkdownListeners($form);
lastTextareaPreviewed = $form.find('textarea.markdown-area');
lastTextareaHeight = lastTextareaPreviewed.height();
@ -165,6 +168,7 @@ $(document).on('markdown-preview:hide', (e, $form) => {
$form.find('.haml-markdown-button, .js-zen-enter').removeClass('gl-display-none!');
markdownPreview.hideReferencedCommands($form);
addMarkdownListeners($form);
});
$(document).on('markdown-preview:toggle', (e, keyboardEvent) => {

View File

@ -194,9 +194,8 @@ export default {
},
showFileDiscussions() {
return (
this.glFeatures.commentOnFiles &&
!this.file.viewer?.manuallyCollapsed &&
(this.fileDiscussions.length || this.file.drafts.length || this.file.hasCommentForm)
(this.fileDiscussions.length || this.file.drafts?.length || this.file.hasCommentForm)
);
},
diffFileHash() {

View File

@ -212,7 +212,7 @@ export default {
return this.expanded ? __('Hide file contents') : __('Show file contents');
},
showCommentButton() {
return this.getNoteableData.current_user.can_create_note && this.glFeatures.commentOnFiles;
return this.getNoteableData.current_user.can_create_note;
},
},
watch: {

View File

@ -597,6 +597,7 @@ export function compositionEndNoteText() {
export function updateTextForToolbarBtn($toolbarBtn) {
const $textArea = $toolbarBtn.closest('.md-area').find('textarea');
if (!$textArea.length) return;
switch ($toolbarBtn.data('mdCommand')) {
case 'indentLines':

View File

@ -75,3 +75,43 @@
}
}
}
.settings-section {
isolation: isolate;
}
$sticky-header-z-index: 98;
.settings-sticky-header,
.settings-sticky-footer {
position: sticky;
z-index: $sticky-header-z-index;
background: $body-bg;
}
.settings-sticky-header {
top: $calc-application-header-height;
&::before {
content: '';
display: block;
height: $gl-padding-8;
position: sticky;
top: calc(#{$calc-application-header-height} + 40px);
box-shadow: 0 1px 1px $gray-200;
}
}
.settings-sticky-header-inner {
position: sticky;
padding: $gl-padding $gl-padding $gl-padding-12;
margin: #{-$gl-padding} #{-$gl-padding} 0;
background: $body-bg;
}
.settings-sticky-footer {
bottom: 0;
padding-top: $gl-padding-8;
padding-bottom: $gl-padding-8;
box-shadow: 0 #{-$gl-padding-4} $gl-padding-12 $gl-padding-4 $body-bg;
}

View File

@ -48,7 +48,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
push_frontend_feature_flag(:mr_activity_filters, current_user)
push_frontend_feature_flag(:review_apps_redeploy_mr_widget, project)
push_frontend_feature_flag(:comment_on_files, current_user)
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
end

View File

@ -189,6 +189,7 @@ class WebHookService
'Content-Type' => 'application/json',
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name),
Gitlab::WebHooks::GITLAB_UUID_HEADER => SecureRandom.uuid,
Gitlab::WebHooks::GITLAB_INSTANCE_HEADER => Gitlab.config.gitlab.base_url
}

View File

@ -8,24 +8,25 @@
.js-user-profile
- else
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
.js-search-settings-section.gl-pb-6
.profile-settings-sidebar
%h4.gl-my-0
= s_("Profiles|Public avatar")
%p.gl-text-secondary
- if @user.avatar?
- if gravatar_enabled?
= s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
- else
= s_("Profiles|You can change your avatar here")
.settings-section.js-search-settings-section
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0
= s_("Profiles|Public avatar")
%p.gl-text-secondary
- if @user.avatar?
- if gravatar_enabled?
= s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
- else
- if gravatar_enabled?
= s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
- else
= s_("Profiles|You can upload your avatar here")
- if current_appearance&.profile_image_guidelines?
.md
= brand_profile_image_guidelines
= s_("Profiles|You can change your avatar here")
- else
- if gravatar_enabled?
= s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
- else
= s_("Profiles|You can upload your avatar here")
- if current_appearance&.profile_image_guidelines?
.md
= brand_profile_image_guidelines
.avatar-image
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
@ -43,11 +44,13 @@
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
method: :delete) do
= s_("Profiles|Remove avatar")
.gl-pb-8
.js-search-settings-section.gl-border-t.gl-py-6
.profile-settings-sidebar
%h4.gl-my-0= s_("Profiles|Current status")
%p.gl-text-secondary= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.settings-section.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0= s_("Profiles|Current status")
%p.gl-text-secondary= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.gl-max-w-80
#js-user-profile-set-status-form
= f.fields_for :status, @user.status do |status_form|
@ -57,22 +60,26 @@
= status_form.hidden_field :clear_status_after,
value: user_clear_status_at(@user),
data: { js_name: 'clearStatusAfter' }
.gl-pb-7
.user-time-preferences.js-search-settings-section.gl-border-t.gl-py-6
.profile-settings-sidebar
%h4.gl-my-0= s_("Profiles|Time settings")
%p.gl-text-secondary= s_("Profiles|Set your local time zone.")
.settings-section.user-time-preferences.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0= s_("Profiles|Time settings")
%p.gl-text-secondary= s_("Profiles|Set your local time zone.")
= f.label :user_timezone, _("Time zone")
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
.gl-pb-7
.js-search-settings-section.gl-border-t.gl-py-6
.profile-settings-sidebar
%h4.gl-my-0
= s_("Profiles|Main settings")
%p.gl-text-secondary
= s_("Profiles|This information will appear on your profile.")
- if current_user.ldap_user?
= s_("Profiles|Some options are unavailable for LDAP accounts")
.settings-section.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0
= s_("Profiles|Main settings")
%p.gl-text-secondary
= s_("Profiles|This information will appear on your profile.")
- if current_user.ldap_user?
= s_("Profiles|Some options are unavailable for LDAP accounts")
.form-group.gl-form-group.rspec-full-name.gl-max-w-80
= render 'profiles/name', form: f, user: @user
.form-group.gl-form-group.gl-md-form-input-lg
@ -161,7 +168,9 @@
= s_("Profiles|Achievements")
= f.gitlab_ui_checkbox_component :achievements_enabled,
s_('Profiles|Display achievements on your profile')
.js-hide-when-nothing-matches-search.gl-border-t.gl-py-6
.gl-pb-7
.js-hide-when-nothing-matches-search.settings-sticky-footer
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
= render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
= s_('TagsPage|Cancel')

View File

@ -1,8 +0,0 @@
---
name: comment_on_files
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121429
rollout_issue_url:
milestone: '16.1'
type: development
group: group::code review
default_enabled: true

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
return unless File.exist?(Rails.root.join('config/click_house.yml'))
raw_config = Rails.application.config_for(:click_house)
return if raw_config.blank?
ClickHouse::Client.configure do |config|
raw_config.each do |database_identifier, db_config|
config.register_database(database_identifier,
database: db_config[:database],
url: db_config[:url],
username: db_config[:username],
password: db_config[:password],
variables: db_config[:variables] || {}
)
end
config.json_parser = Gitlab::Json
config.http_post_proc = ->(url, headers, body) do
options = {
headers: headers,
body: body,
allow_local_requests: Rails.env.development? || Rails.env.test?
}
response = Gitlab::HTTP.post(url, options)
ClickHouse::Client::Response.new(response.body, response.code)
end
end

View File

@ -9934,6 +9934,29 @@ The connection type for [`MergeRequest`](#mergerequest).
| <a id="mergerequestconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
| <a id="mergerequestconnectiontotaltimetomerge"></a>`totalTimeToMerge` | [`Float`](#float) | Total sum of time to merge, in seconds, for the collection of merge requests. |
#### `MergeRequestDiffConnection`
The connection type for [`MergeRequestDiff`](#mergerequestdiff).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestdiffconnectionedges"></a>`edges` | [`[MergeRequestDiffEdge]`](#mergerequestdiffedge) | A list of edges. |
| <a id="mergerequestdiffconnectionnodes"></a>`nodes` | [`[MergeRequestDiff]`](#mergerequestdiff) | A list of nodes. |
| <a id="mergerequestdiffconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `MergeRequestDiffEdge`
The edge type for [`MergeRequestDiff`](#mergerequestdiff).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestdiffedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="mergerequestdiffedgenode"></a>`node` | [`MergeRequestDiff`](#mergerequestdiff) | The item at the end of the edge. |
#### `MergeRequestDiffLlmSummaryConnection`
The connection type for [`MergeRequestDiffLlmSummary`](#mergerequestdiffllmsummary).
@ -10014,6 +10037,29 @@ The edge type for [`MergeRequestParticipant`](#mergerequestparticipant).
| <a id="mergerequestparticipantedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="mergerequestparticipantedgenode"></a>`node` | [`MergeRequestParticipant`](#mergerequestparticipant) | The item at the end of the edge. |
#### `MergeRequestReviewLlmSummaryConnection`
The connection type for [`MergeRequestReviewLlmSummary`](#mergerequestreviewllmsummary).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestreviewllmsummaryconnectionedges"></a>`edges` | [`[MergeRequestReviewLlmSummaryEdge]`](#mergerequestreviewllmsummaryedge) | A list of edges. |
| <a id="mergerequestreviewllmsummaryconnectionnodes"></a>`nodes` | [`[MergeRequestReviewLlmSummary]`](#mergerequestreviewllmsummary) | A list of nodes. |
| <a id="mergerequestreviewllmsummaryconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `MergeRequestReviewLlmSummaryEdge`
The edge type for [`MergeRequestReviewLlmSummary`](#mergerequestreviewllmsummary).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestreviewllmsummaryedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="mergerequestreviewllmsummaryedgenode"></a>`node` | [`MergeRequestReviewLlmSummary`](#mergerequestreviewllmsummary) | The item at the end of the edge. |
#### `MergeRequestReviewerConnection`
The connection type for [`MergeRequestReviewer`](#mergerequestreviewer).
@ -17575,6 +17621,7 @@ Defines which user roles, users, or groups can merge into a protected branch.
| <a id="mergerequestmergecommitsha"></a>`mergeCommitSha` | [`String`](#string) | SHA of the merge request commit (set once merged). |
| <a id="mergerequestmergeerror"></a>`mergeError` | [`String`](#string) | Error message due to a merge error. |
| <a id="mergerequestmergeongoing"></a>`mergeOngoing` | [`Boolean!`](#boolean) | Indicates if a merge is currently occurring. |
| <a id="mergerequestmergerequestdiffs"></a>`mergeRequestDiffs` **{warning-solid}** | [`MergeRequestDiffConnection`](#mergerequestdiffconnection) | **Introduced** in 16.2. This feature is an Experiment. It can be changed or removed at any time. Diff versions of a merge request. |
| <a id="mergerequestmergestatus"></a>`mergeStatus` **{warning-solid}** | [`String`](#string) | **Deprecated** in 14.0. This was renamed. Use: [`MergeRequest.mergeStatusEnum`](#mergerequestmergestatusenum). |
| <a id="mergerequestmergestatusenum"></a>`mergeStatusEnum` | [`MergeStatus`](#mergestatus) | Merge status of the merge request. |
| <a id="mergerequestmergetrainscount"></a>`mergeTrainsCount` | [`Int`](#int) | Number of merge requests in the merge train. |
@ -18267,6 +18314,19 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="mergerequestauthorworkspacesincludeactualstates"></a>`includeActualStates` | [`[String!]`](#string) | Includes all workspaces that match any of the actual states. |
| <a id="mergerequestauthorworkspacesprojectids"></a>`projectIds` | [`[ProjectID!]`](#projectid) | Filter workspaces by project id. |
### `MergeRequestDiff`
A diff version of a merge request.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestdiffcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the diff was created. |
| <a id="mergerequestdiffdiffllmsummary"></a>`diffLlmSummary` | [`MergeRequestDiffLlmSummary`](#mergerequestdiffllmsummary) | Diff summary generated by AI. |
| <a id="mergerequestdiffreviewllmsummaries"></a>`reviewLlmSummaries` | [`MergeRequestReviewLlmSummaryConnection`](#mergerequestreviewllmsummaryconnection) | Review summaries generated by AI. (see [Connections](#connections)) |
| <a id="mergerequestdiffupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the diff was updated. |
### `MergeRequestDiffLlmSummary`
A diff summary generated by AI.
@ -18595,6 +18655,22 @@ Check permissions for the current user on a merge request.
| <a id="mergerequestpermissionsrevertoncurrentmergerequest"></a>`revertOnCurrentMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `revert_on_current_merge_request` on this resource. |
| <a id="mergerequestpermissionsupdatemergerequest"></a>`updateMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `update_merge_request` on this resource. |
### `MergeRequestReviewLlmSummary`
A review summary generated by AI.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestreviewllmsummarycontent"></a>`content` | [`String!`](#string) | Content of the review summary. |
| <a id="mergerequestreviewllmsummarycreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the review summary was created. |
| <a id="mergerequestreviewllmsummarymergerequestdiffid"></a>`mergeRequestDiffId` | [`ID!`](#id) | ID of the Merge Request diff associated with the review summary. |
| <a id="mergerequestreviewllmsummaryprovider"></a>`provider` | [`String!`](#string) | AI provider that generated the summary. |
| <a id="mergerequestreviewllmsummaryreviewer"></a>`reviewer` | [`UserCore`](#usercore) | User who authored the review associated with the review summary. |
| <a id="mergerequestreviewllmsummaryupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the review summary was updated. |
| <a id="mergerequestreviewllmsummaryuser"></a>`user` | [`UserCore`](#usercore) | User associated with the review summary. |
### `MergeRequestReviewer`
A user assigned to a merge request as a reviewer.

View File

@ -19,6 +19,7 @@ in the following table.
| Scope | Description |
| ----- | ----------- |
| `api` | Allows read-write access to the repository files. |
| `read_api` | Allows read access to the repository files. |
| `read_repository` | Allows read-access to the repository files. |
## Get file from repository

View File

@ -22,6 +22,21 @@ This function takes pagination parameters `page` and `per_page` to restrict the
GET /users
```
| Attribute | Type | Required | Description |
| ------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `username` | string | no | Get a single user with a specific username. |
| `extern_uid` | string | no | Get a single user with a specific external authentication provider UID. |
| `provider` | string | no | The external provider. |
| `search` | string | no | Search for a username. |
| `active` | boolean | no | Filters only active users. Default is `false`. |
| `external` | boolean | no | Filters only external users. Default is `false`. |
| `exclude_external` | boolean | no | Filters only non external users. Default is `false`. |
| `blocked` | boolean | no | Filters only blocked users. Default is `false`. |
| `created_after` | DateTime| no | Returns users created after specified time. |
| `created_before` | DateTime| no | Returns users created before specified time. |
| `exclude_internal` | boolean | no | Filters only non internal users. Default is `false`. |
| `without_projects_bots`| boolean | no | Filters user without project bots. Default is `false`. |
```json
[
{
@ -121,6 +136,8 @@ GET /users?without_project_bots=true
GET /users
```
You can use all [parameters available for everyone](#for-non-administrator-users) plus these additional parameters available only for administrators.
| Attribute | Type | Required | Description |
| ------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |

View File

@ -0,0 +1,119 @@
---
status: proposed
creation-date: "2023-06-21"
authors: [ "@fabiopitino" ]
coach: [ ]
approvers: [ ]
owning-stage: ""
---
# Defining bounded contexts
## Status quo
Today the GitLab codebase doesn't have a clear domain structure.
We have [forced the creation of some modules](https://gitlab.com/gitlab-org/gitlab/-/issues/212156)
as a first step but we don't have a well defined strategy for doing it consistently.
The majority of the code is not properly namespaced and organized:
- Ruby namespaces used don't always represent the SSoT. We have overlapping concepts spread across multiple
namespaces. For example: `Abuse::` and `Spam::` or `Security::Orchestration::` and `Security::SecurityOrchestration`.
- Domain code related to the same bounded context is scattered across multiple directories.
- Domain code is present in `lib/` directory under namespaces that differ from the same domain under `app/`.
- Some namespaces are very shallow, containing a few classes while other namespaces are very deep and large.
- A lot of the old code is not namespaced, making it difficult to understand the context where it's used.
## Goal
1. Define a list of characteristics that bounded contexts should have. For example: must relate to at least 1 product category.
1. Have a list of top-level bounded contexts where all domain code is broken down into.
1. Engineers can clearly see the list of available bounded contexts and can make an easy decision where to add
new classes and modules.
1. Define a process for adding a new bounded context to the application. This should occur quite infrequently
and new bounded contexts need to adhere to the characteristics defined previously.
1. Enforce the list of bounded contexts so that no new top-level namespaces can be used aside from the authorized ones.
## Iterations
### 0. Extract libraries out of the codebase
In June 2023 we've started extracing gems out of the main codebase, into
[`gems/` directory inside the monorepo](https://gitlab.com/gitlab-org/gitlab/-/blob/4c6e120069abe751d3128c05ade45ea749a033df/doc/development/gems.md).
This is our first step towards modularization: externalize code that can be
extracted to prevent coupling from being introduced into modules that have been
designed as separate components.
These gems as still part of the monorepo.
### 1. What makes a bounded context?
From the research in [Proposal: split GitLab monolith into components](https://gitlab.com/gitlab-org/gitlab/-/issues/365293)
it seems that following [product categories](https://about.gitlab.com/handbook/product/categories/#hierarchy), as a guideline,
would be much better than translating organization structure into folder structure (e.g. `app/modules/verify/pipeline-execution/...`).
However, this guideline alone is not sufficient and we need a more specific strategy:
- Product categories can change ownership and we have seen some pretty frequent changes, even back and forth.
Moving code every time a product category changes ownership adds too much maintenance overhead.
- Teams and organization changes should just mean relabelling the ownership of specific modules.
- Bounded contexts (top level modules) should be [sufficiently deep](../../../development/software_design.md#use-namespaces-to-define-bounded-contexts)
to encapsulate implementation details and provide a smaller interface.
- Some product categories, such as Browser Performance Testing, are just too small to represent a bounded context on their own.
We should have a strategy for grouping product categories together when makes sense.
- Product categories don't necessarily translate into clean boundaries.
`Category:Pipeline Composition` and `Category:Continuous Integration` are some examples where Pipeline Authoring team
and Pipeline Execution team share a lot of code.
- Some parts of the code might not have a clear product category associated to it.
Despite the above, product categories provide a rough view of the bounded contexts at play in the application.
One idea could be to use product categories to sketch the initial set of bounded contexts.
Then, group related or strongly coupled categories under the same bounded context and create new bounded contexts if missing.
### 2. Identify existing bounded contexts
Start with listing all the Ruby files in a spreadsheet and categorize them into components following the guidelines above.
Some of them are already pretty explicit like Ci::, Packages::, etc. Components should follow our
[existing naming guide](../../../development/software_design.md#use-namespaces-to-define-bounded-contexts).
This could be a short-lived Working Group with representative members of each DevOps stage (e.g. Senior+ engineers).
The WG would help defining high-level components and will be the DRIs for driving the changes in their respective DevOps stage.
### 3. Publish the list of bounded contexts
The list of bounded contexts (top-level namespaces) extracted from the codebase should be defined statically so it can be
used programmatically.
```yaml
# file: config/bounded_contexts.yml
bounded_contexts:
continuous_integration:
dir: modules/ci
namespace: 'Ci::'
packages: ...
merge_requests: ...
git: ...
```
With this static list we could:
- Document the existing bounded contexts for engineers to see the big picture.
- Understand where to place new classes and modules.
- Enforce if any top-level namespaces are used that are not in the list of bounded contexts.
- Autoload non-standard Rails directories based on the given list.
## Glossary
- `modules` are Ruby modules and can be used to nest code hierarchically.
- `namespaces` are unique hierarchies of Ruby constants. E.g. `Ci::` but also `Ci::JobArtifacts::` or `Ci::Pipeline::Chain::`.
- `packages` are Packwerk packages to group together related functionalities. These packages can be big or small depending on the design and architecture. Inside a package all constants (classes and modules) have the same namespace. For example:
- In a package `ci`, all the classes would be nested under `Ci::` namespace. There can be also nested namespaces like `Ci::PipelineProcessing::`.
- In a package `ci-pipeline_creation` all classes are nested under `Ci::PipelineCreation`, like `Ci::PipelineCreation::Chain::Command`.
- In a package `ci` a class named `MergeRequests::UpdateHeadPipelineService` would not be allowed because it would not match the package's namespace.
- This can be enforced easily with [Packwerk's based Rubocop Cops](https://github.com/rubyatscale/rubocop-packs/blob/main/lib/rubocop/cop/packs/root_namespace_is_pack_name.rb).
- `bounded context` is a top-level Packwerk package that represents a macro aspect of the domain. For example: `Ci::`, `MergeRequests::`, `Packages::`, etc.
- A bounded context is represented by a single Ruby module/namespace. E.g. `Ci::` and not `Ci::JobArtifacts::`.
- A bounded context can be made of 1 or multiple Packwerk packages. Nested packages would be recommended if the domain is quite complex and we want to enforce privacy among all the implementation details. For example: `Ci::PipelineProcessing::` and `Ci::PipelineCreation::` could be separate packages of the same bounded context and expose their public API while keeping implementation details private.
- A new bounded context like `RemoteDevelopment::` can be represented a single package while large and complex bounded contexts like `Ci::` would need to be organized into smaller/nested packages.

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@ -0,0 +1,132 @@
---
status: proposed
creation-date: "2023-05-22"
authors: [ "@fabiopitino" ]
coach: [ ]
approvers: [ ]
owning-stage: ""
---
# Hexagonal Rails Monolith
## Summary
**TL;DR:** Change the Rails monolith from a [big ball of mud](https://en.wikipedia.org/wiki/Big_ball_of_mud) state to
a [modular monolith](https://www.thereformedprogrammer.net/my-experience-of-using-modular-monolith-and-ddd-architectures)
that uses an [Hexagonal architecture](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) (or ports and adapters architecture).
Extract cohesive functional domains into separate directory structure using Domain-Driven Design practices.
Extract infrastructure code (logging, database tools, instrumentation, etc.) into gems, essentially remove the need for `lib/` directory.
Define what parts of the functional domains (for example application services) are of public use for integration (the ports)
and what parts are instead private encapsulated details.
Define Web, Sidekiq, REST, GraphQL, and Action Cable as the adapters in the external layer of the architecture.
Use [Packwerk](https://github.com/Shopify/packwerk) to enforce privacy and dependency between modules of the monolith.
![Hexagonal Architecture for GitLab monolith](hexagonal_architecture.png)
## Details
### Application domain
The application core (functional domains) is divided into separate top-level bounded contexts called after the
[feature category](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/categories.yml) they represent.
A bounded-context is represented in the form of a Ruby module.
This follows the existing [guideline on naming namespaces](../../../../development/software_design.md#use-namespaces-to-define-bounded-contexts) but puts more structure to it.
Modules should:
- Be deep enough to encapsulate a lot of the internal logic, state and data.
- Have a public interface that is as small as possible, safe to use by other bounded contexts and well documented.
- Be cohesive and represent the SSoT (single source of truth) of the feature it describes.
Feature categories represent a product area that is large enough for the module to be deep, so we don't have a proliferation
of small top-level modules. It also helps the codebase to follow the
[ubiquitous language](../../../../development/software_design.md#use-ubiquitous-language-instead-of-crud-terminology).
A team can be responsible for multiple feature categories, hence owning the vision for multiple bounded contexts.
While feature categories can sometimes change ownership, this change of mapping the bounded context to new owners
is very cheap.
Using feature categories also helps new contributors, either as GitLab team members of members of the wider community,
to navigate the codebase.
If multiple feature categories are strongly related, they may be grouped under a single bounded context.
If a feature category is only relevant in the context of a parent feature category, it may be included in the
parent's bounded context. For example: Build artifacts existing in the context of Continuous Integration feature category
and they may be merged under a single bounded context.
### Application adapters
>>>
_Adapters are the glue between components and the outside world._
_They tailor the exchanges between the external world and the ports that represent the requirements of the inside_
_of the application component. There can be several adapters for one port, for example, data can be provided by_
_a user through a GUI or a command-line interface, by an automated data source, or by test scripts._ -
[Wikipedia](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)#Principle)
>>>
Application adapters would be:
- Web UI (Rails controllers, view, JS and Vue client)
- REST API endpoints
- GraphQL Endpoints
- Action Cable
TODO: continue describing how adapters are organized and why they are separate from the domain code.
### Platform code
For platform code we consider any classes and modules that are required by the application domain and/or application
adapters to work.
The Rails' `lib/` directory today contains multiple categories of code that could live somewhere else,
most of which is platform code:
- REST API endpoints could be part of the [application adapters](#application-adapters).
- domain code (both large domain code such as `Gitlab::Ci` and small such as `Gitlab::JiraImport`) should be
moved inside the [application domain](#application-domain).
- The rest could be extracted as separate single-purpose gems under the `gems/` directory inside the monolith.
This can include utilities such as logging, error reporting and metrics, rate limiters,
infrastructure code like `Gitlab::ApplicationRateLimiter`, `Gitlab::Redis`, `Gitlab::Database`
and generic subdomains like `Banzai`.
Base classes to extend Rails framework such as `ApplicationRecord` or `ApplicationWorker` as well as GitLab base classes
such as `BaseService` could be implemented as gem extensions.
This means that aside from the Rails framework code, the rest of the platform code resides in `gems/`.
Eventually all code inside `gems/` could potentially be extracted in a separate repository or open sourced.
Placing platform code inside `gems/` makes it clear that its purpose is to serve the application code.
### Why Packwerk?
TODO:
- boundaries not enforced at runtime. Ruby code will still work as being all loaded in the same memory space.
- can be introduced incrementally. Not everything requires to be moved to packs for the Rails autoloader to work.
Companies like Gusto have been developing and maintaining a list of [development and engineering tools](https://github.com/rubyatscale)
for organizations that want to move to using a Rails modular monolith around Packwerk.
### EE and JH extensions
TODO:
## Challenges
- Such changes require a shift in the development mindset to understand the benefits of the modular
architecture and not fallback into legacy practices.
- Changing the application architecture is a challenging task. It takes time, resources and commitment
but most importantly it requires buy-in from engineers.
- This may require us to have a medium-long term team of engineers or a Working Group that makes progresses
on the architecture evolution plan, foster discussions in various engineering channels and resolve adoption challenges.
- We need to ensure we build standards and guidelines and not silos.
- We need to ensure we have clear guidelines on where new code should be placed. We must not recreate junk drawer folders like `lib/`.
## Opportunities
The move to a modular monolith architecture enables a lot of opportunities that we could explore in the future:
- We could align the concept of domain expert with explicitly owning specific modules of the monolith.
- The use of static analysis tool (such as Packwerk, Rubocop) can catch design violations in development and CI, ensuring
that best practices are honored.
- By defining dependencies between modules explicitly we could speed up CI by testing only the parts that are affected by
the changes.
- Such modular architecture could help to further decompose modules into separate services if needed.

View File

@ -0,0 +1,112 @@
---
status: proposed
creation-date: "2023-05-22"
authors: [ "@grzesiek", "@fabiopitino" ]
coach: [ ]
approvers: [ ]
owning-stage: ""
participating-stages: []
---
<!-- vale gitlab.FutureTense = NO -->
# GitLab Modular Monolith
## Summary
The main [GitLab Rails](https://gitlab.com/gitlab-org/gitlab)
project has been implemented as a large monolithic application, using
[Ruby on Rails](https://rubyonrails.org/) framework. It has over 2.2 million
lines of Ruby code and hundreds of engineers contributing to it every day.
The application has been growing in complexity for more than a decade. The
monolithic architecture has served us well during this time, making it possible
to keep high development velocity and great engineering productivity.
Even though we strive for having [an approachable open-core architecture](https://about.gitlab.com/blog/2022/07/14/open-core-is-worse-than-plugins/)
we need to strengthen the boundaries between domains to retain velocity and
increase development predictability.
As we grow as an engineering organization, we want to explore a slightly
different, but related, architectural paradigm:
[a modular monolith design](https://en.wikipedia.org/wiki/Modular_programming),
while still using a [monolithic architecture](https://en.wikipedia.org/wiki/Monolithic_application)
with satellite services.
This should allow us to increase engineering efficiency, reduce the cognitive
load, and eventually decouple internal components to the extend that will allow
us to deploy and run them separately if needed.
## Motivation
Working with a large and tightly coupled monolithic application is challenging:
Engineering:
- Onboarding engineers takes time. It takes a while before engineers feel
productive due to the size of the context and the amount of coupling.
- We need to use `CODEOWNERS` file feature for several domains but
[these rules are complex](https://gitlab.com/gitlab-org/gitlab/-/blob/409228f064a950af8ff2cecdd138fc9da41c8e63/.gitlab/CODEOWNERS#L1396-1457).
- It is difficult for engineers to build a mental map of the application due to its size.
Even apparently isolated changes can have [far-reaching repercussions](https://about.gitlab.com/handbook/engineering/development/#reducing-the-impact-of-far-reaching-work)
on other parts of the monolith.
- Attrition/retention of engineering talent. It is fatiguing and demoralizing for
engineers to constantly deal with the obstacles to productivity.
Architecture:
- There is little structure inside the monolith. We have attempted to enforce
the creation [of some modules](https://gitlab.com/gitlab-org/gitlab/-/issues/212156)
but have no company-wide strategy on what the functional parts of the
monolith should be, and how code should be organized.
- There is no isolation between existing modules. Ruby does not provide
out-of-the-box tools to effectively enforce boundaries. Everything lives
under the same memory space.
- We rarely build abstractions that can boost our efficiency.
- Moving stable parts of the application into separate services is impossible
due to high coupling.
- We are unable to deploy changes to specific domains separately and isolate
failures that are happening inside them.
Productivity:
- High median-time-to-production for complex changes.
- It can be overwhelming for the wider-community members to contribute.
- Reducing testing times requires diligent and persistent efforts.
## Goals
- Increase the development velocity and predicability through separation of concerns.
- Improve code quality by reducing coupling and introducing useful abstractions.
- Build abstractions required to deploy and run GitLab components separately.
## How do we get there?
While we do recognize that modularization is a significant technical endeavor,
we believe that the main challenge is organizational, rather than technical. We
not only need to design separation in a way that modules are decoupled in a
pragmatic way which works well on GitLab.com but also on self-managed
instances, but we need to align modularization with the way in which we want to
work at GitLab.
There are many aspects and details required to make modularization of our
monolith successful. We will work on the aspects listed below, refine them, and
add more important details as we move forward towards the goal:
1. [Deliver modularization proof-of-concepts that will deliver key insights](proof_of_concepts.md)
1. [Align modularization plans to the organizational structure](bounded_contexts.md)
1. Start a training program for team members on how to work with decoupled domains (TODO)
1. Build tools that will make it easier to build decoupled domains through inversion of control (TODO)
1. Separate domains into modules that will reflect organizational structure (TODO)
1. Build necessary services to align frontend and backend modularization (TODO)
1. [Introduce hexagonal architecture within the monolith](hexagonal_monolith/index.md)
1. Introduce clean architecture with one-way-dependencies and host application (TODO)
1. Build abstractions that will make it possible to run and deploy domains separately (TODO)
## Status
In progress.
## References
[List of references](references.md)

View File

@ -0,0 +1,44 @@
---
status: proposed
creation-date: "2023-07-05"
authors: [ "@grzesiek" ]
coach: [ ]
owners: [ ]
---
# Modular Monolithk: PoCs
Modularization of our monolith is a complex project. There will be many
unknowns. One thing that can help us mitigate the risks and deliver key
insights are Proof-of-Concepts that we could deliver early on, to better
understand what will need to be done.
## Inter-module communicaton PoC
First PoC that we plan to deliver is a PoC of inter-module communication. We do
recognize the need to separate modules, but still allow them to communicate
together using a well defined interface. Modules can communicate through a
facade classes (like libraries usually do), or through evening system. Both
ways are important.
The main question is: how do we want to define the interface and how to design
the communication channels.
It is one of our goals to make it possible to plug modules out, and operate
some of them as separate services. This will make it easier deploy GitLab.com
in the future and scale key domains. One possible way to achieve this goal
would be to design the inter-module communication using a protobuf as an
interface and gRPC as a communication channel. When modules are plugged-in, we
would bypass gRPC and serialization and use in-process communication primitives
(while still using protobuf as an interface). When a module gets plugged-out,
gRPC would carry messages between modules.
## Frontend sorting hat PoC
Frontend sorting-hat is a PoC for combining multiple domains to render a full
page of GitLab (with menus, and items that come from multiple separate
domains).
## Frontend assets aggregation PoC
Frontend assets aggregation is a PoC for a possible separation of micro-frontends.

View File

@ -0,0 +1,70 @@
---
status: proposed
creation-date: "2023-06-21"
authors: [ "@fabiopitino" ]
coach: [ ]
approvers: [ ]
owning-stage: ""
---
# References
## Related design docs
- [Composable codebase design doc](../composable_codebase_using_rails_engines/index.md)
## Related Issues
- [Split GitLab monolith into components](https://gitlab.com/gitlab-org/gitlab/-/issues/365293)
- [Make it simple to build and use "Decoupled Services"](https://gitlab.com/gitlab-org/gitlab/-/issues/31121)
- [Use nested structure to organize CI classes](https://gitlab.com/gitlab-org/gitlab/-/issues/209745)
- [Create new models / classes within a module / namespace](https://gitlab.com/gitlab-org/gitlab/-/issues/212156)
- [Make teams to be maintainers of their code](https://gitlab.com/gitlab-org/gitlab/-/issues/25872)
- [Add backend guide for Dependency Injection](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73644)
## Internal Slack Channels
- [`#modular_monolith`](https://gitlab.slack.com/archives/C03NTK6HZBM)
- [`#architecture`](https://gitlab.slack.com/archives/CJ4DB7517)
## Reference Implementations / Guides
Gusto / RubyAtScale:
- [RubyAtScale toolchain for modularization](https://github.com/rubyatscale)
- [Gusto's engineering blog](https://engineering.gusto.com/laying-the-cultural-and-technical-foundation-for-big-rails/)
- [Gradual modularization](https://gradualmodularization.com/) (successor to CBRA)
- [Component-Based Rails Applications](https://cbra.info) ("deprecated")
Shopify:
- [Packwerk](https://github.com/Shopify/packwerk)
- [Shopify's jurney to modularization](https://shopify.engineering/shopify-monolith)
- [Internal GitLab doc transcript of an AMA with a Shopify engineer](https://docs.google.com/document/d/1uZbcaK8Aqs-D_n7_uQ5XE295r5UWDJEBwA6g5bTjcwc/edit#heading=h.d1tml5rlzrpa)
Domain-Driven Rails / Rails Event Store:
Rails Event Store is relevant because it is a mechanism to achieve many
of the goals discussed here, and is based upon patterns used by Arkency
to build production applications.
This doesn't mean we need to use this specific framework or approach.
However, the general concepts of DDD/ES/CQRS are important and in some
cases maybe necessary to achieve the goals of this blueprint, so it's
useful to have concrete production-proven implementations of those
concepts to look at as an example.
- [Arkency's domain-driven Rails](https://products.arkency.com/domain-driven-rails/)
- [Arkency's Rails Event Store](https://railseventstore.org)
App Continuum:
An illustration of how an application can evolve from a small, unstructed app, through various
stages including a modular well-structured, monolith, all the way to a microservices architecture.
Includes discussion of why you might want to stop at various stages, and specifically the
challenges/concerns with making the jump to microservices, and why sticking with a
well-structured monolith may be preferable in many cases.
- [App Continuum](https://www.appcontinuum.io)

View File

@ -0,0 +1,203 @@
---
stage: Verify
group: Pipeline Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: tutorial
---
# Tutorial: Update HashiCorp Vault configuration to use ID Tokens **(PREMIUM)**
This tutorial demonstrates how to convert your existing CI/CI secrets configuration to use [ID Tokens](../secrets/id_token_authentication.md).
The `CI_JOB_JWT` variables are deprecated, but updating to ID tokens requires some important configuration changes to work with Vault. If you have more than a handful of jobs, converting everything at once is a daunting task.
From GitLab 15.9 to 15.11, [enable the automatic ID token authentication](../secrets/id_token_authentication.md#enable-automatic-id-token-authentication-deprecated)
setting to enable ID Tokens and disable `CI_JOB_JWT` tokens.
In GitLab 16.0 and later you can use ID tokens without any settings changes.
Jobs that use `secrets:vault` automatically do not have `CI_JOB_JWT` tokens available,
Jobs that don't use `secrets:vault` can still use `CI_JOB_JWT` tokens.
This tutorial will focus on v16 onwards, if you are running a slightly older version you will need to toggle the `Limit JSON Web Token (JWT) access` setting as appropriate.
To update your vault configuration to use ID tokens:
1. [Create a second JWT authentication path in Vault](#create-a-second-jwt-authentication-path-in-vault)
1. [Recreate roles to use the new authentication path](#recreate-roles-to-use-the-new-authentication-path)
1. [Update your CI/CD Jobs](#update-your-cicd-jobs)
## Prerequisites
This tutorial assumes you are familiar with GitLab CI/CD and Vault.
To follow along, you must have:
- An instance running GitLab 15.9 or later, or be on GitLab.com.
- A Vault server that you are already using.
- CI/CD jobs retrieving secrets from Vault with `CI_JOB_JWT`.
In the examples below, replace `vault.example.com` with the URL of your Vault server,
and `gitlab.example.com` with the URL of your GitLab instance.
## Create a second JWT authentication path in Vault
As part of the transition from `CI_JOB_JWT` to ID tokens, you must update the `bound_issuer` in Vault to include `https://`:
```shell
$ vault write auth/jwt/config \
jwks_url="https://gitlab.example.com/-/jwks" \
bound_issuer="https://gitlab.example.com"
```
After you make this change, jobs that use `CI_JOB_JWT` start to fail.
You can create multiple authentication paths in Vault, which enable you to transition to IT Tokens on a project by job basis without disruption.
1. Configure a new authentication path with the name `jwt_v2`, run:
```shell
vault auth enable -path jwt_v2 jwt
```
You can choose a different name, but the rest of these examples assume you used `jwt_v2`, so update the examples as needed.
1. Configure the new authentication path for your instance:
```shell
$ vault write auth/jwt_v2/config \
jwks_url="https://gitlab.example.com/-/jwks" \
bound_issuer="https://gitlab.example.com"
```
## Recreate roles to use the new authentication path
Roles are bound to a specific authentication path so you need to add new roles for each job.
1. Recreate the role for staging named `myproject-staging`:
```shell
$ vault write auth/jwt_v2/role/myproject-staging - <<EOF
{
"role_type": "jwt",
"policies": ["myproject-staging"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_claims": {
"project_id": "22",
"ref": "master",
"ref_type": "branch"
}
}
EOF
```
1. Recreate the role for production named `myproject-production`:
```shell
$ vault write auth/jwt_v2/role/myproject-production - <<EOF
{
"role_type": "jwt",
"policies": ["myproject-production"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_claims_type": "glob",
"bound_claims": {
"project_id": "22",
"ref_protected": "true",
"ref_type": "branch",
"ref": "auto-deploy-*"
}
}
EOF
```
You only need to update `jwt` to `jwt_v2` in the `vault` command, do not change the `role_type` inside the role.
## Update your CI/CD Jobs
Vault has two different [KV Secrets Engines](https://developer.hashicorp.com/vault/docs/secrets/kv) and the version you are using impacts how you define secrets in CI/CD.
Check the [Which Version is my Vault KV Mount?](https://support.hashicorp.com/hc/en-us/articles/4404288741139-Which-Version-is-my-Vault-KV-Mount-) article on HashiCorp's support portal to check your Vault server.
Also, if needed you can review the CI/CD documentation for:
- [`secrets:`](../yaml/index.md#secrets)
- [`id_tokens:`](../yaml/index.md#id_tokens)
The following examples show how to obtain the staging database password written to the `password` field in `secret/myproject/staging/db`
### KV Secrets Engine v1
The [`secrets:vault`](../yaml/index.md#secretsvault) keyword defaults to v2 of the KV Mount, so you need to explicitly configure the job to use the v1 engine:
```yaml
job:
variables:
VAULT_SERVER_URL: https://vault.example.com
VAULT_AUTH_PATH: jwt_v2
VAULT_AUTH_ROLE: myproject-staging
id_tokens:
VAULT_ID_TOKEN:
aud: https://gitlab.example.com
secrets:
PASSWORD:
vault:
engine:
name: kv-v1
path: secret
field: password
path: myproject/staging/db
file: false
```
Both `VAULT_SERVER_URL` and `VAULT_AUTH_PATH` can be [defined as project or group CI/CD variables](../../ci/variables/index.md#define-a-cicd-variable-in-the-ui),
if preferred.
We use [`secrets:file:false`](../../ci/yaml/index.md#secretsfile) because ID tokens place secrets in a file by default, but we need it to work as a regular variable to match the old behavior.
### KV Secrets Engine v2
There are two formats you can use for the v2 engine.
Long format:
```yaml
job:
variables:
VAULT_SERVER_URL: https://vault.example.com
VAULT_AUTH_PATH: jwt_v2
VAULT_AUTH_ROLE: myproject-staging
id_tokens:
VAULT_ID_TOKEN:
aud: https://gitlab.example.com
secrets:
PASSWORD:
vault:
engine:
name: kv-v2
path: secret
field: password
path: myproject/staging/db
file: false
```
This is the same as the example for the v1 engine but `secrets:vault:engine:name:` is set to `kv-v2` to match the engine.
You can also use a short format:
```yaml
job:
variables:
VAULT_SERVER_URL: https://vault.example.com
VAULT_AUTH_PATH: jwt_v2
VAULT_AUTH_ROLE: myproject-staging
id_tokens:
VAULT_ID_TOKEN:
aud: https://gitlab.example.com
secrets:
PASSWORD:
vault: myproject/staging/db/password@secret
file: false
```
After you commit the updated CI/CD configuration, your jobs will be fetching secrets with ID Tokens, congratulations!

View File

@ -139,6 +139,9 @@ manual_authentication:
You can use ID tokens to automatically fetch secrets from HashiCorp Vault with the
[`secrets`](../yaml/index.md#secrets) keyword.
If you previously used `CI_JOB_JWT` to fetch secrets from Vault, learn how to switch
to ID tokens with the [Update HashiCorp Vault configuration to use ID Tokens](convert-to-id-tokens.md) tutorial.
### Configure automatic ID Token authentication
If one ID token is defined, the `secrets` keyword automatically uses it to authenticate with Vault. For example:

View File

@ -97,17 +97,7 @@ You can see example adding a new gem: [!121676](https://gitlab.com/gitlab-org/gi
```yaml
inherit_from:
- ../../.rubocop.yml
CodeReuse/ActiveRecord:
Enabled: false
AllCops:
TargetRubyVersion: 3.0
Naming/FileName:
Exclude:
- spec/**/*.rb
- ../config/rubocop.yml
```
1. Configure CI for a newly added Gem:

View File

@ -53,8 +53,26 @@ For more details, see [Secure your application](../user/application_security/ind
## Security partners
You can integrate GitLab with several security partners. For more information, see
[Security partner integrations](security_partners/index.md).
You can integrate GitLab with the following security partners:
<!-- vale gitlab.Spelling = NO -->
- [Anchore](https://docs.anchore.com/current/docs/configuration/integration/ci_cd/gitlab/)
- [Bridgecrew](https://docs.bridgecrew.io/docs/integrate-with-gitlab-self-managed)
- [Checkmarx](https://checkmarx.atlassian.net/wiki/spaces/SD/pages/1929937052/GitLab+Integration)
- [Deepfactor](https://www.deepfactor.io/docs/integrate-deepfactor-scanner-in-your-ci-cd-pipelines/#gitlab)
- [Fortify](https://www.microfocus.com/en-us/fortify-integrations/gitlab)
- [GrammaTech](https://www.grammatech.com/codesonar-gitlab-integration)
- [Indeni](https://docs.cloudrail.app/#/integrations/gitlab)
- [Jscrambler](https://docs.jscrambler.com/code-integrity/documentation/gitlab-ci-integration)
- [Mend](https://www.mend.io/gitlab/)
- [Semgrep](https://semgrep.dev/for/gitlab)
- [StackHawk](https://docs.stackhawk.com/continuous-integration/gitlab.html)
- [Tenable](https://docs.tenable.com/tenableio/Content/ContainerSecurity/GetStarted.htm)
- [Venafi](https://marketplace.venafi.com/xchange/620d2d6ed419fb06a5c5bd36/solution/6292c2ef7550f2ee553cf223)
- [Veracode](https://community.veracode.com/s/knowledgeitem/gitlab-ci-MCEKSYPRWL35BRTGOVI55SK5RI4A)
<!-- vale gitlab.Spelling = YES -->
## Continuous integration

View File

@ -93,7 +93,6 @@ For more information about how Smart Commits work and what commands are availabl
- [Process issues with Smart Commits](https://support.atlassian.com/jira-software-cloud/docs/process-issues-with-smart-commits/)
- [Using Smart Commits](https://confluence.atlassian.com/fisheye/using-smart-commits-960155400.html)
## Troubleshooting
## Related topics
To troubleshoot the Jira development panel on your own server, see the
[Atlassian documentation](https://confluence.atlassian.com/jirakb/troubleshoot-the-development-panel-in-jira-server-574685212.html).
- [Troubleshoot the development panel in Jira Server](https://confluence.atlassian.com/jirakb/troubleshoot-the-development-panel-in-jira-server-574685212.html)

View File

@ -1,30 +1,11 @@
---
stage: Secure
group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
type: index
redirect_to: '../index.md#security-partners'
remove_date: '2023-10-05'
---
# Security partners **(FREE)**
This document was moved to [another location](../index.md#security-partners).
You can integrate GitLab with its security partners. This page has information on how do this with
each security partner:
<!-- vale gitlab.Spelling = NO -->
- [Anchore](https://docs.anchore.com/current/docs/configuration/integration/ci_cd/gitlab/)
- [Bridgecrew](https://docs.bridgecrew.io/docs/integrate-with-gitlab-self-managed)
- [Checkmarx](https://checkmarx.atlassian.net/wiki/spaces/SD/pages/1929937052/GitLab+Integration)
- [Deepfactor](https://www.deepfactor.io/docs/integrate-deepfactor-scanner-in-your-ci-cd-pipelines/#gitlab)
- [GrammaTech](https://www.grammatech.com/codesonar-gitlab-integration)
- [Indeni](https://docs.cloudrail.app/#/integrations/gitlab)
- [JScrambler](https://docs.jscrambler.com/code-integrity/documentation/gitlab-ci-integration)
- [Mend](https://www.mend.io/gitlab/)
- [Semgrep](https://semgrep.dev/for/gitlab)
- [StackHawk](https://docs.stackhawk.com/continuous-integration/gitlab.html)
- [Tenable](https://docs.tenable.com/tenableio/Content/ContainerSecurity/GetStarted.htm)
- [Venafi](https://marketplace.venafi.com/xchange/620d2d6ed419fb06a5c5bd36/solution/6292c2ef7550f2ee553cf223)
- [Veracode](https://community.veracode.com/s/knowledgeitem/gitlab-ci-MCEKSYPRWL35BRTGOVI55SK5RI4A)
- [Fortify](https://www.microfocus.com/en-us/fortify-integrations/gitlab)
<!-- vale gitlab.Spelling = YES -->
<!-- This redirect file can be deleted after <2023-10-05>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -360,6 +360,33 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
1. [Re-run database migrations](../administration/raketasks/maintenance.md#run-incomplete-database-migrations).
- You might also encounter the following error while upgrading to GitLab 15.10 or later:
```shell
"exception.class": "ActiveRecord::StatementInvalid",
"exception.message": "PG::SyntaxError: ERROR: zero-length delimited identifier at or near \"\"\"\"\nLINE 1: ...COALESCE(\"lock_version\", 0) + 1 WHERE \"ci_builds\".\"\" IN (SEL...\n
```
This error is caused by a [batched background migration introduced in GitLab 14.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81410)
not being finalized before upgrading to GitLab 15.10 or later. To resolve this error, it is safe to [mark the migration as complete](background_migrations.md#mark-a-batched-migration-finished):
```ruby
# Start the rails console
connection = Ci::ApplicationRecord.connection
Gitlab::Database::SharedModel.using_connection(connection) do
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(
Gitlab::Database.gitlab_schemas_for_connection(connection), 'NullifyOrphanRunnerIdOnCiBuilds', :ci_builds, :id, [])
# mark all jobs completed
migration.batched_jobs.update_all(status: Gitlab::Database::BackgroundMigration::BatchedJob.state_machine.states[:succeeded].value)
migration.update_attribute(:status, Gitlab::Database::BackgroundMigration::BatchedMigration.state_machine.states[:finished].value)
end
```
For more information, see [issue 415724](https://gitlab.com/gitlab-org/gitlab/-/issues/415724).
### 15.9.0
- **Upgrade to patch release 15.9.3 or later**. This provides fixes for two database migration bugs:

View File

@ -148,6 +148,9 @@ Getting help has never been easier. If you have a question about how the GitLab
To give feedback, select the **Give Feedback** link.
NOTE:
Only the last 50 messages in the chat history are retained. The chat history expires 3 days after last use.
### Summarize merge request changes **(ULTIMATE SAAS)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10400) in GitLab 16.0 as an [Experiment](../policy/experiment-beta-support.md#experiment).

View File

@ -28,6 +28,13 @@ To view project insights:
1. Select **Analyze > Insights**.
1. To view a report, select the **Select report** dropdown list.
### Access Insights reports with deep links
You can direct users to a specific report in Insights by using the deep-linked URL.
To create a deep link, append the report key to the end of the Insights report URL.
For example, a GitLab report with the key `bugsCharts` has the deep link URL `https://gitlab.com/gitlab-org/gitlab/insights/#/bugsCharts`.
## Configure project insights
Prerequisites:

View File

@ -279,6 +279,7 @@ For more information about supported events for webhooks, see [webhook events](w
> - `X-Gitlab-Event-UUID` header [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329743) in GitLab 14.8.
> - `X-Gitlab-Instance` header [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31333) in GitLab 15.5.
> - `X-Gitlab-Webhook-UUID` header [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230830) in GitLab 16.2.
Webhook requests to your endpoint include the following headers:
@ -286,6 +287,7 @@ Webhook requests to your endpoint include the following headers:
| ------ | ------ | ------ |
| `User-Agent` | In the format `"Gitlab/<VERSION>"`. | `"GitLab/15.5.0-pre"` |
| `X-Gitlab-Instance` | Hostname of the GitLab instance that sent the webhook. | `"https://gitlab.com"` |
| `X-Gitlab-Webhook-UUID` | Unique ID per webhook. If a webhook request fails and retries, the second request has a new ID. | `"02affd2d-2cba-4033-917d-ec22d5dc4b38"` |
| `X-Gitlab-Event` | Name of the webhook type. Corresponds to [event types](webhook_events.md) but in the format `"<EVENT> Hook"`. | `"Push Hook"` |
| `X-Gitlab-Event-UUID` | Unique ID per webhook that is not recursive. A hook is recursive if triggered by an earlier webhook that hit the GitLab instance. Recursive webhooks have the same value for this header. | `"13792a34-cac6-4fda-95a8-c58e00a3954e"` |

View File

@ -146,11 +146,8 @@ per conflicted file on the merge request diff:
## Add a comment to a merge request file
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123515) in GitLab 16.1 [with a flag](../../../administration/feature_flags.md) named `comment_on_files`. Enabled by default.
FLAG:
On self-managed GitLab, by default this feature is available. To disable it, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `comment_on_files`.
On GitLab.com, this feature is available.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123515) in GitLab 16.1 [with a flag](../../../administration/feature_flags.md) named `comment_on_files`. Enabled by default.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125130) in GitLab 16.2.
You can add comments to a merge request diff file. These comments persist across
rebases and file changes.

View File

@ -1,15 +1,6 @@
inherit_from:
- ../../.rubocop.yml
- ../config/rubocop.yml
CodeReuse/ActiveRecord:
# FIXME
Gitlab/RSpec/AvoidSetup:
Enabled: false
Rails/ApplicationRecord:
Enabled: false
AllCops:
TargetRubyVersion: 2.7
Naming/FileName:
Exclude:
- spec/**/*.rb

View File

@ -21,24 +21,24 @@ GEM
ast (2.4.2)
concurrent-ruby (1.2.2)
diff-lcs (1.5.0)
gitlab-styles (10.0.0)
rubocop (~> 1.43.0)
gitlab-styles (10.1.0)
rubocop (~> 1.50.2)
rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
rubocop-rspec (~> 2.18)
rubocop-rspec (~> 2.22)
i18n (1.13.0)
concurrent-ruby (~> 1.0)
json (2.6.3)
minitest (5.18.0)
parallel (1.22.1)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
racc (1.6.2)
rack (3.0.4.1)
racc (1.7.1)
rack (3.0.8)
rainbow (3.1.1)
regexp_parser (2.8.0)
regexp_parser (2.8.1)
rexml (3.2.5)
rspec (3.12.0)
rspec-core (~> 3.12.0)
@ -53,33 +53,36 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.0)
rubocop (1.43.0)
rubocop (1.50.2)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.24.1, < 2.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.26.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.17.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-graphql (0.19.0)
rubocop (>= 0.87, < 2)
rubocop-performance (1.16.0)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.17.4)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.18.1)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
ruby-progressbar (1.11.0)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
@ -90,9 +93,10 @@ PLATFORMS
DEPENDENCIES
activerecord-gitlab!
gitlab-styles (~> 10.0.0)
rspec (~> 3.0)
rubocop (~> 1.21)
gitlab-styles (~> 10.1.0)
rspec (~> 3.12)
rubocop (~> 1.50)
rubocop-rspec (~> 2.22)
BUNDLED WITH
2.4.14

View File

@ -8,19 +8,20 @@ Gem::Specification.new do |spec|
spec.authors = ["group::tenant-scale"]
spec.email = ["engineering@gitlab.com"]
spec.summary = "GitLab's ActiveRecord patches"
spec.summary = "GitLab ActiveRecord patches"
spec.description = "GitLab stores any patches relating to ActiveRecord here"
spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/activerecord-gitlab"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.7"
spec.required_ruby_version = ">= 3.0"
spec.metadata["rubygems_mfa_required"] = "true"
spec.files = Dir['lib/**/*.rb']
spec.test_files = Dir['spec/**/*']
spec.files = Dir["lib/**/*.rb"]
spec.require_paths = ["lib"]
spec.add_runtime_dependency "activerecord", ">= 6.1.7.3"
spec.add_development_dependency "gitlab-styles", "~> 10.0.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rubocop", "~> 1.21"
spec.add_development_dependency "gitlab-styles", "~> 10.1.0"
spec.add_development_dependency "rspec", "~> 3.12"
spec.add_development_dependency "rubocop", "~> 1.50"
spec.add_development_dependency "rubocop-rspec", "~> 2.22"
end

View File

@ -0,0 +1,4 @@
include:
- local: gems/gem.gitlab-ci.yml
inputs:
gem_name: "click_house-client"

View File

@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper

View File

@ -0,0 +1,17 @@
inherit_from:
- ../../.rubocop.yml
CodeReuse/ActiveRecord:
Enabled: false
Rails/ApplicationRecord:
Enabled: false
AllCops:
TargetRubyVersion: 3.0
Naming/FileName:
Exclude:
- spec/**/*.rb
- lib/gitlab/rspec.rb
- lib/gitlab/rspec/all.rb

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
gemspec

View File

@ -0,0 +1,102 @@
PATH
remote: .
specs:
click_house-client (0.1.0)
activesupport (< 8)
addressable (~> 2.8)
json (~> 2.6.3)
GEM
remote: https://rubygems.org/
specs:
activesupport (7.0.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
concurrent-ruby (1.2.2)
diff-lcs (1.5.0)
gitlab-styles (10.1.0)
rubocop (~> 1.50.2)
rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
rubocop-rspec (~> 2.22)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
minitest (5.18.1)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
public_suffix (5.0.1)
racc (1.7.1)
rack (3.0.8)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
rexml (3.2.5)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.1)
rubocop (1.50.2)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-graphql (0.19.0)
rubocop (>= 0.87, < 2)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
PLATFORMS
x86_64-linux
DEPENDENCIES
click_house-client!
gitlab-styles (~> 10.1.0)
rake (~> 13.0)
rspec (~> 3.0)
rubocop
rubocop-rspec
BUNDLED WITH
2.4.15

View File

@ -0,0 +1,3 @@
ClickHouse::Client
This Gem provides a simple way to query ClickHouse databases using the HTTP interface.

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
Gem::Specification.new do |spec|
spec.name = "click_house-client"
spec.version = "0.1.0"
spec.authors = ["group::optimize"]
spec.email = ["engineering@gitlab.com"]
spec.summary = "GitLab's client to interact with ClickHouse"
spec.description = "This Gem provides a simple way to query ClickHouse databases using the HTTP interface."
spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/click_house-client"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0"
spec.add_runtime_dependency "activesupport", "< 8"
spec.add_runtime_dependency "addressable", "~> 2.8"
spec.add_runtime_dependency 'json', '~> 2.6.3'
spec.add_development_dependency 'gitlab-styles', '~> 10.1.0'
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'rubocop-rspec'
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'addressable'
require 'json'
require 'active_support/time'
require_relative "client/database"
require_relative "client/configuration"
require_relative "client/formatter"
require_relative "client/response"
module ClickHouse
module Client
class << self
def configuration
@configuration ||= Configuration.new
end
def configure
yield(configuration)
configuration.validate!
end
end
Error = Class.new(StandardError)
ConfigurationError = Class.new(Error)
DatabaseError = Class.new(Error)
def self.execute(query, database, configuration = self.configuration)
db = configuration.databases[database]
raise ConfigurationError, "The database '#{database}' is not configured" unless db
response = configuration.http_post_proc.call(
db.uri.to_s,
db.headers,
"#{query} FORMAT JSON" # always return JSON
)
raise DatabaseError, response.body unless response.success?
Formatter.format(configuration.json_parser.parse(response.body))
end
end
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
module ClickHouse
module Client
class Configuration
# Configuration options:
#
# *register_database* (method): registers a database, the following arguments are required:
# - database: database name
# - url: URL and port to the HTTP interface
# - username
# - password
# - variables (optional): configuration for the client
#
# *http_post_proc*: A callable object for invoking the HTTP request.
# The object must handle the following parameters: url, headers, body
# and return a Gitlab::ClickHouse::Client::Response object.
#
# *json_parser*: object for parsing JSON strings, it should respond to the "parse" method
#
# Example:
#
# Gitlab::ClickHouse::Client.configure do |c|
# c.register_database(:main,
# database: 'gitlab_clickhouse_test',
# url: 'http://localhost:8123',
# username: 'default',
# password: 'clickhouse',
# variables: {
# join_use_nulls: 1 # treat JOINs as per SQL standard
# }
# )
#
# c.http_post_proc = lambda do |url, headers, body|
# options = {
# headers: headers,
# body: body,
# allow_local_requests: false
# }
#
# response = Gitlab::HTTP.post(url, options)
# Gitlab::ClickHouse::Client::Response.new(response.body, response.code)
# end
#
# c.json_parser = JSON
# end
attr_accessor :http_post_proc, :json_parser
attr_reader :databases
def initialize
@databases = {}
@http_post_proc = nil
@json_parser = JSON
end
def register_database(name, **args)
raise ConfigurationError, "The database '#{name}' is already registered" if @databases.key?(name)
@databases[name] = Database.new(**args)
end
def validate!
raise ConfigurationError, "The 'http_post_proc' option is not configured" unless @http_post_proc
raise ConfigurationError, "The 'json_parser' option is not configured" unless @json_parser
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module ClickHouse
module Client
class Database
attr_reader :database
def initialize(database:, url:, username:, password:, variables: {})
@database = database
@url = url
@username = username
@password = password
@variables = variables.merge(database: database).freeze
end
def uri
@uri ||= begin
parsed = Addressable::URI.parse(@url)
parsed.query_values = @variables
parsed
end
end
def headers
@headers ||= {
'X-ClickHouse-User' => @username,
'X-ClickHouse-Key' => @password
}.freeze
end
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module ClickHouse
module Client
class Formatter
DEFAULT = ->(value) { value }
TYPE_CASTERS = {
'UInt64' => ->(value) { Integer(value) },
"DateTime64(6, 'UTC')" => ->(value) { ActiveSupport::TimeZone["UTC"].parse(value) }
}.freeze
def self.format(result)
name_type_mapping = result['meta'].each_with_object({}) do |column, hash|
hash[column['name']] = column['type']
end
result['data'].map do |row|
row.each_with_object({}) do |(column, value), casted_row|
caster = TYPE_CASTERS.fetch(name_type_mapping[column], DEFAULT)
casted_row[column] = caster.call(value)
end
end
end
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module ClickHouse
module Client
class Response
attr_reader :body
def initialize(body, http_status_code)
@body = body
@http_status_code = http_status_code
end
def success?
@http_status_code == 200
end
end
end
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ClickHouse::Client::Configuration do
subject(:configuration) do
config = described_class.new
config.http_post_proc = -> {}
config.json_parser = Object
config
end
describe '#register_database' do
let(:database_options) do
{
database: 'test_db',
url: 'http://localhost:3333',
username: 'user',
password: 'pass',
variables: {
join_use_nulls: 1
}
}
end
it 'registers a database' do
configuration.register_database(:my_db, **database_options)
expect(configuration.databases.size).to eq(1)
database = configuration.databases[:my_db]
expect(database.uri.to_s).to eq('http://localhost:3333?database=test_db&join_use_nulls=1')
end
context 'when adding the same DB multiple times' do
it 'raises error' do
configuration.register_database(:my_db, **database_options)
expect do
configuration.register_database(:my_db, **database_options)
end.to raise_error(ClickHouse::Client::ConfigurationError, /database 'my_db' is already registered/)
end
end
end
describe '#validate!' do
context 'when `http_post_proc` option is not configured' do
it 'raises error' do
configuration.http_post_proc = nil
expect do
configuration.validate!
end.to raise_error(ClickHouse::Client::ConfigurationError, /'http_post_proc' option is not configured/)
end
end
context 'when `json_parser` option is not configured' do
it 'raises error' do
configuration.json_parser = nil
expect do
configuration.validate!
end.to raise_error(ClickHouse::Client::ConfigurationError, /'json_parser' option is not configured/)
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ClickHouse::Client::Database do
subject(:database) do
described_class.new(
database: 'test_db',
url: 'http://localhost:3333',
username: 'user',
password: 'pass',
variables: {
join_use_nulls: 1
}
)
end
describe '#uri' do
it 'builds the correct URL' do
expect(database.uri.to_s).to eq('http://localhost:3333?database=test_db&join_use_nulls=1')
end
end
describe '#headers' do
it 'returns the correct headers' do
expect(database.headers).to eq({
'X-ClickHouse-User' => 'user',
'X-ClickHouse-Key' => 'pass'
})
end
end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
RSpec.describe ClickHouse::Client do
describe '#execute' do
# Assuming we have a DB table with the following schema
#
# CREATE TABLE issues (
# `id` UInt64,
# `title` String DEFAULT '',
# `description` Nullable(String),
# `created_at` DateTime64(6, 'UTC') DEFAULT now(),
# `updated_at` DateTime64(6, 'UTC') DEFAULT now()
# )
# ENGINE = ReplacingMergeTree(updated_at)
# ORDER BY (id)
let(:query_result_fixture) { File.expand_path('../fixtures/query_result.json', __dir__) }
let(:database_config) do
{
database: 'test_db',
url: 'http://localhost:3333',
username: 'user',
password: 'pass',
variables: {
join_use_nulls: 1
}
}
end
let(:configuration) do
ClickHouse::Client::Configuration.new.tap do |config|
config.register_database(:test_db, **database_config)
config.http_post_proc = ->(_url, _headers, _query) {
body = File.read(query_result_fixture)
ClickHouse::Client::Response.new(body, 200)
}
end
end
it 'parses the results and returns the data as array of hashes' do
result = described_class.execute('SELECT * FROM issues', :test_db, configuration)
timestamp1 = ActiveSupport::TimeZone["UTC"].parse('2023-06-21 13:33:44')
timestamp2 = ActiveSupport::TimeZone["UTC"].parse('2023-06-21 13:33:50')
timestamp3 = ActiveSupport::TimeZone["UTC"].parse('2023-06-21 13:33:40')
expect(result).to eq([
{
'id' => 2,
'title' => 'Title 2',
'description' => 'description',
'created_at' => timestamp1,
'updated_at' => timestamp1
},
{
'id' => 3,
'title' => 'Title 3',
'description' => nil,
'created_at' => timestamp2,
'updated_at' => timestamp2
},
{
'id' => 1,
'title' => 'Title 1',
'description' => 'description',
'created_at' => timestamp3,
'updated_at' => timestamp3
}
])
end
context 'when the DB is not configured' do
it 'raises erro' do
expect do
described_class.execute('SELECT * FROM issues', :different_db, configuration)
end.to raise_error(ClickHouse::Client::ConfigurationError, /not configured/)
end
end
context 'when error response is returned' do
let(:configuration) do
ClickHouse::Client::Configuration.new.tap do |config|
config.register_database(:test_db, **database_config)
config.http_post_proc = ->(_url, _headers, _query) {
ClickHouse::Client::Response.new('some error', 404)
}
end
end
it 'raises error' do
expect do
described_class.execute('SELECT * FROM issues', :test_db, configuration)
end.to raise_error(ClickHouse::Client::DatabaseError, 'some error')
end
end
end
end

View File

@ -0,0 +1,53 @@
{
"meta": [
{
"name": "id",
"type": "UInt64"
},
{
"name": "title",
"type:": "String"
},
{
"name": "description",
"type": "Nullable(String)"
},
{
"name": "created_at",
"type": "DateTime64(6, 'UTC')"
},
{
"name": "updated_at",
"type": "DateTime64(6, 'UTC')"
}
],
"data": [
{
"id": "2",
"title": "Title 2",
"description": "description",
"created_at": "2023-06-21 13:33:44.000000",
"updated_at": "2023-06-21 13:33:44.000000"
},
{
"id": "3",
"title": "Title 3",
"description": null,
"created_at": "2023-06-21 13:33:50.000000",
"updated_at": "2023-06-21 13:33:50.000000"
},
{
"id": "1",
"title": "Title 1",
"description": "description",
"created_at": "2023-06-21 13:33:40.000000",
"updated_at": "2023-06-21 13:33:40.000000"
}
],
"rows": 3,
"statistics": {
"elapsed": 0.001581789,
"rows_read": 3,
"bytes_read": 172
}
}

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require "click_house/client"
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
end

73
gems/config/rubocop.yml Normal file
View File

@ -0,0 +1,73 @@
inherit_gem:
gitlab-styles:
- rubocop-default.yml
require:
- ../../rubocop/rubocop
- rubocop-rspec
inherit_mode:
merge:
- Include
- Exclude
- AllowedPatterns
AllCops:
# Target the current Ruby version. For example, "3.0" or "3.1".
TargetRubyVersion: <%= RUBY_VERSION[/^\d+\.\d+/, 0] %>
# This cop doesn't make sense in the context of gems
CodeReuse/ActiveRecord:
Enabled: false
# This cop doesn't make sense in the context of gems
Cop/PutGroupRoutesUnderScope:
Enabled: false
# This cop doesn't make sense in the context of gems
Cop/PutProjectRoutesUnderScope:
Enabled: false
Gemspec/AvoidExecutingGit:
Enabled: true
# We disable this since we support multiple Ruby versions
Gemspec/RequiredRubyVersion:
Enabled: false
# This cop doesn't make sense in the context of gems
Gitlab/DocUrl:
Enabled: false
# This cop doesn't make sense in the context of gems
Gitlab/NamespacedClass:
Enabled: false
# This cop doesn't make sense in the context of gems
Graphql/AuthorizeTypes:
Enabled: false
# This cop doesn't make sense in the context of gems
Graphql/Descriptions:
Enabled: false
Naming/FileName:
Exclude:
- spec/**/*.rb
# This cop doesn't make sense in the context of gems
RSpec/MissingFeatureCategory:
Enabled: false
# Enable once we drop 3.0 support
Style/HashSyntax:
Enabled: false
Style/RegexpLiteralMixedPreserve:
Enabled: true
SupportedStyles:
- slashes
- percent_r
- mixed
- mixed_preserve
EnforcedStyle: mixed_preserve

View File

@ -1,14 +1,2 @@
inherit_from:
- ../../.rubocop.yml
CodeReuse/ActiveRecord:
Enabled: false
AllCops:
TargetRubyVersion: 3.0
Naming/FileName:
Exclude:
- spec/**/*.rb
- lib/gitlab/rspec.rb
- lib/gitlab/rspec/all.rb
- ../config/rubocop.yml

View File

@ -43,12 +43,12 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
gitlab-styles (10.0.0)
rubocop (~> 1.43.0)
gitlab-styles (10.1.0)
rubocop (~> 1.50.2)
rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
rubocop-rspec (~> 2.18)
rubocop-rspec (~> 2.22)
i18n (1.13.0)
concurrent-ruby (~> 1.0)
json (2.6.3)
@ -86,7 +86,7 @@ GEM
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.0)
regexp_parser (2.8.1)
rexml (3.2.5)
rspec (3.12.0)
rspec-core (~> 3.12.0)
@ -125,32 +125,35 @@ GEM
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.0)
rubocop (1.43.0)
rubocop (1.50.2)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.24.1, < 2.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.1)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-graphql (0.19.0)
rubocop (>= 0.87, < 2)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.19.1)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.18.1)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
thor (1.2.2)
tzinfo (2.0.6)
@ -167,12 +170,12 @@ PLATFORMS
DEPENDENCIES
factory_bot_rails (~> 6.2.0)
gitlab-rspec!
gitlab-styles (~> 10.0.0)
gitlab-styles (~> 10.1.0)
rspec-benchmark (~> 0.6.0)
rspec-parameterized (~> 1.0)
rspec-rails (~> 6.0.1)
rubocop (~> 1.21)
rubocop-rspec (~> 2.18.1)
rubocop (~> 1.50)
rubocop-rspec (~> 2.22)
BUNDLED WITH
2.4.4

View File

@ -21,10 +21,10 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency "rspec", "~> 3.0"
spec.add_development_dependency "factory_bot_rails", "~> 6.2.0"
spec.add_development_dependency "gitlab-styles", "~> 10.0.0"
spec.add_development_dependency "gitlab-styles", "~> 10.1.0"
spec.add_development_dependency "rspec-benchmark", "~> 0.6.0"
spec.add_development_dependency "rspec-parameterized", "~> 1.0"
spec.add_development_dependency "rspec-rails", "~> 6.0.1"
spec.add_development_dependency "rubocop", "~> 1.21"
spec.add_development_dependency "rubocop-rspec", "~> 2.18.1"
spec.add_development_dependency "rubocop", "~> 1.50"
spec.add_development_dependency "rubocop-rspec", "~> 2.22"
end

View File

@ -2,3 +2,8 @@
require "rspec"
require_relative "rspec/version"
module Gitlab
module Rspec
end
end

View File

@ -36,7 +36,7 @@ module StubENV
end
def env_stubbed?
ENV[STUBBED_KEY]
ENV.fetch(STUBBED_KEY, false)
end
def init_stub

View File

@ -1,26 +1,5 @@
inherit_from:
- ../../.rubocop.yml
CodeReuse/ActiveRecord:
Enabled: false
Gitlab/DocUrl:
Enabled: false
Gitlab/NamespacedClass:
Enabled: false
AllCops:
TargetRubyVersion: 3.0
Naming/FileName:
Exclude:
- spec/**/*.rb
- lib/gitlab/utils/all.rb
Lint/AmbiguousRegexpLiteral:
Exclude:
- spec/**/*.rb
- ../config/rubocop.yml
RSpec/InstanceVariable:
Exclude:
@ -29,3 +8,24 @@ RSpec/InstanceVariable:
Lint/BinaryOperatorWithIdenticalOperands:
Exclude:
- spec/**/*.rb
# We use EnforcedStyle of comparison here due to it being better
# performing code as seen in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36221#note_375659681
Style/NumericPredicate:
EnforcedStyle: comparison
# FIXME
Gitlab/RSpec/AvoidSetup:
Enabled: false
# FIXME: When enabled, there's a spec failure in ee/spec/requests/api/graphql/mutations/merge_requests/update_approval_rule_spec.rb:51,
# due to the `default_value` of `remove_hidden_groups` set to `[]`, most probably instead of `false`, in ee/app/graphql/mutations/merge_requests/update_approval_rule.rb.
# The problem is that `Object#=~` exists (even though it's deprecated), hence calling it on an `Array` doesn't blow up, but `Array#match?` doesn't exist.
Performance/RegexpMatch:
Exclude:
- lib/gitlab/utils.rb
Rails/OutputSafety:
Details: grace period
Exclude:
- 'lib/gitlab/utils.rb'

View File

@ -55,12 +55,12 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
gitlab-styles (10.0.0)
rubocop (~> 1.43.0)
gitlab-styles (10.1.0)
rubocop (~> 1.50.2)
rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
rubocop-rspec (~> 2.18)
rubocop-rspec (~> 2.22)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
@ -73,9 +73,10 @@ GEM
nokogiri (1.15.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
parallel (1.22.1)
parser (3.2.0.0)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
proc_to_ast (0.1.0)
coderay
parser
@ -100,7 +101,7 @@ GEM
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.6.0)
regexp_parser (2.8.1)
rexml (3.2.5)
rspec (3.12.0)
rspec-core (~> 3.12.0)
@ -139,33 +140,36 @@ GEM
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-support (3.12.0)
rubocop (1.43.0)
rubocop (1.50.2)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.24.1, < 2.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.24.1)
parser (>= 3.1.1.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-graphql (0.19.0)
rubocop (>= 0.87, < 2)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.19.1)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.18.1)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
ruby-progressbar (1.11.0)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
thor (1.2.2)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
@ -181,13 +185,14 @@ PLATFORMS
DEPENDENCIES
factory_bot_rails (~> 6.2.0)
gitlab-rspec!
gitlab-styles (~> 10.0.0)
gitlab-styles (~> 10.1.0)
gitlab-utils!
rspec (~> 3.12)
rspec-benchmark (~> 0.6.0)
rspec-parameterized (~> 1.0)
rspec-rails (~> 6.0.1)
rubocop (~> 1.21)
rubocop-rspec (~> 2.18.1)
rubocop (~> 1.50)
rubocop-rspec (~> 2.22)
BUNDLED WITH
2.4.4

View File

@ -25,10 +25,11 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency "rake", "~> 13.0"
spec.add_development_dependency "factory_bot_rails", "~> 6.2.0"
spec.add_development_dependency "gitlab-styles", "~> 10.0.0"
spec.add_development_dependency "gitlab-styles", "~> 10.1.0"
spec.add_development_dependency "rspec", "~> 3.12"
spec.add_development_dependency "rspec-benchmark", "~> 0.6.0"
spec.add_development_dependency "rspec-parameterized", "~> 1.0"
spec.add_development_dependency "rspec-rails", "~> 6.0.1"
spec.add_development_dependency "rubocop", "~> 1.21"
spec.add_development_dependency "rubocop-rspec", "~> 2.18.1"
spec.add_development_dependency "rubocop", "~> 1.50"
spec.add_development_dependency "rubocop-rspec", "~> 2.22"
end

View File

@ -12,13 +12,16 @@ module Gitlab
MAX_VERSION_LENGTH = 128
def self.parse(str, parse_suffix: false)
if str.is_a?(self)
str
elsif str && str.length <= MAX_VERSION_LENGTH && m = str.match(VERSION_REGEX)
VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i, parse_suffix ? m.post_match : nil)
else
VersionInfo.new
return str if str.is_a?(self)
if str && str.length <= MAX_VERSION_LENGTH
match = str.match(VERSION_REGEX)
if match
return VersionInfo.new(match[1].to_i, match[2].to_i, match[3].to_i, parse_suffix ? match.post_match : nil)
end
end
VersionInfo.new
end
def initialize(major = 0, minor = 0, patch = 0, suffix = nil) # rubocop:disable Metrics/ParameterLists
@ -71,7 +74,7 @@ module Gitlab
def suffix
@suffix ||= @suffix_s.strip.gsub('-', '.pre.').scan(/\d+|[a-z]+/i).map do |s|
/^\d+$/ =~ s ? s.to_i : s
/^\d+$/.match?(s) ? s.to_i : s
end.freeze
end

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
# rubocop:disable GitlabSecurity/PublicSend
require 'spec_helper'
require 'active_support/testing/time_helpers'
@ -23,7 +21,7 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
end
def method_name
strong_memoize(:method_name) do # rubocop: disable Gitlab/StrongMemoizeAttr
strong_memoize(:method_name) do # rubocop:disable Gitlab/StrongMemoizeAttr
trace << value
value
end
@ -111,12 +109,12 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
it_behaves_like 'caching the value'
it 'raises exception for invalid type as key' do
expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/
expect { object.strong_memoize(10) { 20 } }.to raise_error(/Invalid type of '10'/)
end
it 'raises exception for invalid characters in key' do
expect { object.strong_memoize(:enabled?) { 20 } }
.to raise_error /is not allowed as an instance variable name/
.to raise_error(/is not allowed as an instance variable name/)
end
end
end
@ -129,7 +127,7 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
end
[:method_name, "method_name"].each do |argument|
context "for #{argument.class}" do
context "when argument is a #{argument.class}" do
it 'does allocate exactly one string when fetching value' do
expect do
object.strong_memoize(argument) { 10 }
@ -157,12 +155,12 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
it_behaves_like 'caching the value'
it 'raises exception for invalid type as key' do
expect { object.strong_memoize_with_expiration(10, 1) { 20 } }.to raise_error /Invalid type of '10'/
expect { object.strong_memoize_with_expiration(10, 1) { 20 } }.to raise_error(/Invalid type of '10'/)
end
it 'raises exception for invalid characters in key' do
expect { object.strong_memoize_with_expiration(:enabled?, 1) { 20 } }
.to raise_error /is not allowed as an instance variable name/
.to raise_error(/is not allowed as an instance variable name/)
end
end
end
@ -217,19 +215,19 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
describe '#strong_memoized?' do
shared_examples 'memoization check' do |method_name|
context "for #{method_name}" do
context "when method is :#{method_name}" do
let(:value) { :anything }
subject { object.strong_memoized?(method_name) }
it 'returns false if the value is uncached' do
is_expected.to be(false)
expect(subject).to be(false)
end
it 'returns true if the value is cached' do
object.public_send(method_name)
is_expected.to be(true)
expect(subject).to be(true)
end
end
end
@ -309,7 +307,7 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
subject { klass.strong_memoize_attr(:nonexistent_method) }
it 'fails when strong-memoizing a nonexistent method' do
expect { subject }.to raise_error(NameError, %r{undefined method `nonexistent_method' for class})
expect { subject }.to raise_error(NameError, /undefined method `nonexistent_method' for class/)
end
end
@ -355,7 +353,7 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
it { is_expected.to eq(output) }
if params[:valid]
if params[:valid] # rubocop:disable RSpec/AvoidConditionalStatements
it 'is a valid ivar name' do
expect { instance_variable_defined?(ivar) }.not_to raise_error
end
@ -368,5 +366,3 @@ RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do
end
end
end
# rubocop:enable GitlabSecurity/PublicSend

View File

@ -4,5 +4,6 @@ module Gitlab
module WebHooks
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
GITLAB_INSTANCE_HEADER = 'X-Gitlab-Instance'
GITLAB_UUID_HEADER = 'X-Gitlab-Webhook-UUID'
end
end

View File

@ -5398,6 +5398,9 @@ msgstr ""
msgid "Analytics|Visualization Type"
msgstr ""
msgid "Analytics|Visualization designer"
msgstr ""
msgid "Analytics|Visualization was saved successfully"
msgstr ""
@ -34785,6 +34788,9 @@ msgstr ""
msgid "ProductAnalytics|Page Views"
msgstr ""
msgid "ProductAnalytics|Product analytics onboarding"
msgstr ""
msgid "ProductAnalytics|Repeat Visit Percentage"
msgstr ""

View File

@ -10,7 +10,7 @@ module RuboCop
class FeatureCategory < RuboCop::Cop::Base
include MigrationHelpers
FEATURE_CATEGORIES_FILE_PATH = "config/feature_categories.yml"
FEATURE_CATEGORIES_FILE_PATH = File.expand_path("../../../config/feature_categories.yml", __dir__)
MSG = "'feature_category' should be defined to better assign the ownership for batched migration jobs. " \
"For more details refer: " \

View File

@ -59,6 +59,8 @@ module RuboCop
PATTERN
def on_send(node)
return unless in_rake_file?(node)
method, file = require_method(node)
return unless method
@ -70,6 +72,14 @@ module RuboCop
private
def in_rake_file?(node)
File.extname(filepath(node)) == '.rake'
end
def filepath(node)
node.location.expression.source_buffer.name
end
# Allow `require "foo/rake_task"`
def requires_task?(file)
file.source.include?('task')

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative '../../usage_data_helpers'
module RuboCop
module Cop
module UsageData
@ -13,6 +15,8 @@ module RuboCop
# distinct_count(Ci::Build, :commit_id)
#
class DistinctCountByLargeForeignKey < RuboCop::Cop::Base
include UsageDataHelpers
MSG = 'Avoid doing `%s` on foreign keys for large tables having above 100 million rows.'
def_node_matcher :distinct_count?, <<-PATTERN
@ -20,6 +24,8 @@ module RuboCop
PATTERN
def on_send(node)
return unless in_usage_data_file?(node)
distinct_count?(node) do |method_name, method_arguments|
next unless method_arguments && method_arguments.length >= 2
next if batch_set_to_false?(method_arguments[2])

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative '../../usage_data_helpers'
module RuboCop
module Cop
module UsageData
@ -12,6 +14,8 @@ module RuboCop
# histogram(Issue, buckets: 1..100)
# histogram(User.active, buckets: 1..100)
class HistogramWithLargeTable < RuboCop::Cop::Base
include UsageDataHelpers
MSG = 'Avoid histogram method on %{model_name}'
# Match one level const as Issue, Gitlab
@ -31,6 +35,8 @@ module RuboCop
PATTERN
def on_send(node)
return unless in_usage_data_file?(node)
one_level_matches = one_level_node(node)
two_level_matches = two_level_node(node)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative '../../usage_data_helpers'
module RuboCop
module Cop
module UsageData
@ -17,6 +19,8 @@ module RuboCop
# # ...
# end
class InstrumentationSuperclass < RuboCop::Cop::Base
include UsageDataHelpers
MSG = "Instrumentation classes should subclass one of the following: %{allowed_classes}."
BASE_PATTERN = "(const nil? !#allowed_class?)"
@ -32,12 +36,16 @@ module RuboCop
PATTERN
def on_class(node)
return unless in_instrumentation_file?(node)
class_definition(node) do
register_offense(node.children[1])
end
end
def on_send(node)
return unless in_instrumentation_file?(node)
class_new_definition(node) do
register_offense(node.children.last)
end

View File

@ -1,9 +1,13 @@
# frozen_string_literal: true
require_relative '../../usage_data_helpers'
module RuboCop
module Cop
module UsageData
class LargeTable < RuboCop::Cop::Base
include UsageDataHelpers
# This cop checks that batch count and distinct_count are used in usage_data.rb files in metrics based on ActiveRecord models.
#
# @example
@ -38,6 +42,8 @@ module RuboCop
PATTERN
def on_send(node)
return unless in_usage_data_file?(node)
one_level_matches = one_level_node(node)
two_level_matches = two_level_node(node)

View File

@ -1,8 +1,5 @@
UsageData/LargeTable:
Enabled: true
Include:
- 'lib/gitlab/usage_data.rb'
- 'ee/lib/ee/gitlab/usage_data.rb'
NonRelatedClasses:
- :ActionMailer::Base
- :Date
@ -41,9 +38,6 @@ UsageData/LargeTable:
- :maximum
UsageData/HistogramWithLargeTable:
Enabled: true
Include:
- 'lib/gitlab/usage_data.rb'
- 'ee/lib/ee/gitlab/usage_data.rb'
HighTrafficModels: &high_traffic_models # models for all high traffic tables in Migration/UpdateLargeTable
- 'AuditEvent'
- 'CommitStatus'
@ -98,9 +92,6 @@ UsageData/HistogramWithLargeTable:
- 'WebHookLog'
UsageData/DistinctCountByLargeForeignKey:
Enabled: true
Include:
- 'lib/gitlab/usage_data.rb'
- 'ee/lib/ee/gitlab/usage_data.rb'
AllowedForeignKeys:
- 'agent_id'
- 'author_id'
@ -116,8 +107,6 @@ UsageData/DistinctCountByLargeForeignKey:
- 'requirement_id'
UsageData/InstrumentationSuperclass:
Enabled: true
Include:
- 'lib/gitlab/usage/metrics/instrumentations/**/*.rb'
AllowedClasses:
- :DatabaseMetric
- :GenericMetric

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module RuboCop
module UsageDataHelpers
def in_usage_data_file?(node)
filepath(node).end_with?('gitlab/usage_data.rb')
end
def in_instrumentation_file?(node)
filepath(node).start_with?('lib/gitlab/usage/metrics/instrumentations') && File.extname(filepath(node)) == '.rb'
end
private
def filepath(node)
node.location.expression.source_buffer.name
end
end
end

View File

@ -34,7 +34,7 @@ if ([ "${CI_PROJECT_NAME}" = "gitlab" ] && [ "${FOSS_ONLY}" != "1" ]) || ([ "${C
fi
# Generate this image for https://jihulab.com/gitlab-cn/gitlab and https://gitlab.com/gitlab-jh/jh-team/gitlab
if (["${CI_PROJECT_NAMESPACE}" = "gitlab-cn"] || ["${CI_PROJECT_NAMESPACE}" = "gitlab-jh"]); then
if ([ "${CI_PROJECT_NAMESPACE}" = "gitlab-cn" ] || [ "${CI_PROJECT_NAMESPACE}" = "gitlab-jh" ]); then
ASSETS_IMAGE_NAME="gitlab-assets-jh"
fi

View File

@ -1,5 +0,0 @@
#!/bin/sh
# Rubocop doesn't have a good way to run excluded files without a separate invocation:
# https://github.com/rubocop/rubocop/issues/6323
find gems vendor/gems -name \*.gemspec | xargs bundle exec rubocop --only Gemspec/AvoidExecutingGit

View File

@ -51,8 +51,7 @@ class StaticAnalysis
Task.new(%w[yarn run block-dependencies], 1),
Task.new(%w[yarn run check-dependencies], 1),
Task.new(%w[scripts/lint-rugged], 1),
Task.new(%w[scripts/gemfile_lock_changed.sh], 1),
Task.new(%w[scripts/lint-vendored-gems.sh], 1)
Task.new(%w[scripts/gemfile_lock_changed.sh], 1)
].compact.freeze
def run_tasks!(options = {})

View File

@ -102,15 +102,16 @@ RSpec.describe 'Markdown keyboard shortcuts', :js, feature_category: :team_plann
it_behaves_like 'keyboard shortcuts'
it_behaves_like 'no side effects'
end
context 'Haml markdown editor' do
let(:path_to_visit) { new_project_issue_path(project) }
let(:markdown_field) { find_field('Description') }
let(:non_markdown_field) { find_field('Title') }
context 'if preview is toggled before shortcuts' do
before do
click_button "Preview"
click_button "Continue editing"
end
it_behaves_like 'keyboard shortcuts'
it_behaves_like 'no side effects'
it_behaves_like 'keyboard shortcuts'
it_behaves_like 'no side effects'
end
end
def type_and_select(text)

View File

@ -644,22 +644,14 @@ describe('DiffFileHeader component', () => {
);
});
it.each`
commentOnFiles | exists | existsText
${false} | ${false} | ${'does not'}
${true} | ${true} | ${'does'}
`(
'$existsText render comment on files button when commentOnFiles is $commentOnFiles',
({ commentOnFiles, exists }) => {
window.gon = { current_user_id: 1 };
createComponent({
props: {
addMergeRequestButtons: true,
},
options: { provide: { glFeatures: { commentOnFiles } } },
});
it('should render the comment on files button', () => {
window.gon = { current_user_id: 1 };
createComponent({
props: {
addMergeRequestButtons: true,
},
});
expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(exists);
},
);
expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(true);
});
});

View File

@ -572,7 +572,6 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
expect(wrapper.find('[data-testid="file-discussions"]').exists()).toEqual(exists);
@ -593,7 +592,6 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
expect(wrapper.find('[data-testid="file-note-form"]').exists()).toEqual(exists);
@ -612,7 +610,6 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
expect(wrapper.find('[data-testid="diff-file-discussions"]').exists()).toEqual(exists);

View File

@ -1,12 +1,10 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
@ -42,9 +40,8 @@ describe('WorkItemTree', () => {
children,
canUpdate,
},
stubs: { WidgetWrapper },
});
wrapper.vm.$refs.wrapper.show = jest.fn();
};
it('displays Add button', () => {

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'ClickHouse::Client', feature_category: :database do
it 'does not have any databases configured' do
databases = ClickHouse::Client.configuration.databases
expect(databases).to be_empty
end
end

View File

@ -250,6 +250,7 @@ merge_request_diff:
- merge_request_diff_detail
- merge_request_diff_files
- merge_request_diff_llm_summary
- merge_request_review_llm_summaries
merge_request_diff_commits:
- merge_request_diff
- commit_author

View File

@ -40,7 +40,7 @@ RSpec.describe RuboCop::Cop::QA::ElementWithPattern do
end
end
context 'outside of a migration spec file' do
context 'when outside of a QA spec file' do
it "does not register an offense" do
expect_no_offenses(<<-RUBY)
describe 'foo' do

View File

@ -7,54 +7,84 @@ require_relative '../../../../rubocop/cop/rake/require'
RSpec.describe RuboCop::Cop::Rake::Require do
let(:msg) { described_class::MSG }
it 'registers an offenses for require methods' do
expect_offense(<<~RUBY)
require 'json'
^^^^^^^^^^^^^^ #{msg}
require_relative 'gitlab/json'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
RUBY
describe '#in_rake_file?' do
context 'in a Rake file' do
let(:node) { double(location: double(expression: double(source_buffer: double(name: 'foo/bar.rake')))) } # rubocop:disable RSpec/VerifiedDoubles
it { expect(subject.__send__(:in_rake_file?, node)).to be(true) }
end
context 'when outside of a Rake file' do
let(:node) { double(location: double(expression: double(source_buffer: double(name: 'foo/bar.rb')))) } # rubocop:disable RSpec/VerifiedDoubles
it { expect(subject.__send__(:in_rake_file?, node)).to be(false) }
end
end
it 'does not register offense inside `task` definition' do
expect_no_offenses(<<~RUBY)
task :parse do
require 'json'
end
context 'in a Rake file' do
before do
allow(cop).to receive(:in_rake_file?).and_return(true)
end
namespace :some do
task parse: :env do
require_relative 'gitlab/json'
it 'registers an offenses for require methods' do
expect_offense(<<~RUBY)
require 'json'
^^^^^^^^^^^^^^ #{msg}
require_relative 'gitlab/json'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
RUBY
end
it 'does not register offense inside `task` definition' do
expect_no_offenses(<<~RUBY)
task :parse do
require 'json'
end
end
RUBY
namespace :some do
task parse: :env do
require_relative 'gitlab/json'
end
end
RUBY
end
it 'does not register offense inside a block definition' do
expect_no_offenses(<<~RUBY)
RSpec::Core::RakeTask.new(:parse_json) do |t, args|
require 'json'
end
RUBY
end
it 'does not register offense inside a method definition' do
expect_no_offenses(<<~RUBY)
def load_deps
require 'json'
end
task :parse do
load_deps
end
RUBY
end
it 'does not register offense when require task related files' do
expect_no_offenses(<<~RUBY)
require 'rubocop/rake_tasks'
require 'gettext_i18n_rails/tasks'
require_relative '../../rubocop/check_graceful_task'
RUBY
end
end
it 'does not register offense inside a block definition' do
expect_no_offenses(<<~RUBY)
RSpec::Core::RakeTask.new(:parse_json) do |t, args|
require 'json'
end
RUBY
end
context 'when outside of a Rake file' do
before do
allow(cop).to receive(:in_rake_file?).and_return(false)
end
it 'does not register offense inside a method definition' do
expect_no_offenses(<<~RUBY)
def load_deps
require 'json'
end
task :parse do
load_deps
end
RUBY
end
it 'does not register offense when require task related files' do
expect_no_offenses(<<~RUBY)
require 'rubocop/rake_tasks'
require 'gettext_i18n_rails/tasks'
require_relative '../../rubocop/check_graceful_task'
RUBY
it 'registers an offenses for require methods' do
expect_no_offenses("require 'json'")
end
end
end

View File

@ -13,33 +13,45 @@ RSpec.describe RuboCop::Cop::UsageData::DistinctCountByLargeForeignKey do
})
end
context 'when counting by disallowed key' do
it 'registers an offense' do
expect_offense(<<~CODE)
distinct_count(Issue, :creator_id)
^^^^^^^^^^^^^^ #{msg}
CODE
context 'in an usage data file' do
before do
allow(cop).to receive(:in_usage_data_file?).and_return(true)
end
it 'does not register an offense when batch is false' do
expect_no_offenses('distinct_count(Issue, :creator_id, batch: false)')
context 'when counting by disallowed key' do
it 'registers an offense' do
expect_offense(<<~CODE)
distinct_count(Issue, :creator_id)
^^^^^^^^^^^^^^ #{msg}
CODE
end
it 'does not register an offense when batch is false' do
expect_no_offenses('distinct_count(Issue, :creator_id, batch: false)')
end
it 'registers an offense when batch is true' do
expect_offense(<<~CODE)
distinct_count(Issue, :creator_id, batch: true)
^^^^^^^^^^^^^^ #{msg}
CODE
end
end
it 'registers an offense when batch is true' do
expect_offense(<<~CODE)
distinct_count(Issue, :creator_id, batch: true)
^^^^^^^^^^^^^^ #{msg}
CODE
context 'when calling by allowed key' do
it 'does not register an offense with symbol' do
expect_no_offenses('distinct_count(Issue, :author_id)')
end
it 'does not register an offense with string' do
expect_no_offenses("distinct_count(Issue, 'merge_requests.target_project_id')")
end
end
end
context 'when calling by allowed key' do
it 'does not register an offense with symbol' do
expect_no_offenses('distinct_count(Issue, :author_id)')
end
it 'does not register an offense with string' do
expect_no_offenses("distinct_count(Issue, 'merge_requests.target_project_id')")
context 'when outside of an usage data file' do
it 'does not register an offense' do
expect_no_offenses('distinct_count(Issue, :creator_id)')
end
end
end

View File

@ -14,93 +14,105 @@ RSpec.describe RuboCop::Cop::UsageData::HistogramWithLargeTable do
})
end
context 'with large tables' do
context 'with one-level constants' do
context 'when calling histogram(Issue)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Issue, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
context 'in an usage data file' do
before do
allow(cop).to receive(:in_usage_data_file?).and_return(true)
end
context 'with large tables' do
context 'with one-level constants' do
context 'when calling histogram(Issue)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Issue, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
end
end
context 'when calling histogram(::Issue)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Issue, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
end
end
context 'when calling histogram(Issue.closed)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Issue.closed, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
end
end
context 'when calling histogram(::Issue.closed)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Issue.closed, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
end
end
end
context 'when calling histogram(::Issue)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Issue, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
context 'with two-level constants' do
context 'when calling histogram(::Ci::Build)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Ci::Build, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
end
end
context 'when calling histogram(Issue.closed)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Issue.closed, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
context 'when calling histogram(::Ci::Build.active)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Ci::Build.active, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
end
end
context 'when calling histogram(::Issue.closed)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Issue.closed, :project_id, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
CODE
context 'when calling histogram(Ci::Build)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Ci::Build, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
end
context 'when calling histogram(Ci::Build.active)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Ci::Build.active, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
end
end
end
context 'with two-level constants' do
context 'when calling histogram(::Ci::Build)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Ci::Build, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
context 'with non related class' do
it 'does not register an offense' do
expect_no_offenses('histogram(MergeRequest, buckets: 1..100)')
end
end
context 'when calling histogram(::Ci::Build.active)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(::Ci::Build.active, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
end
context 'when calling histogram(Ci::Build)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Ci::Build, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
end
context 'when calling histogram(Ci::Build.active)' do
it 'registers an offense' do
expect_offense(<<~CODE)
histogram(Ci::Build.active, buckets: 1..100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
CODE
end
context 'with non related method' do
it 'does not register an offense' do
expect_no_offenses('count(Issue, buckets: 1..100)')
end
end
end
context 'with non related class' do
context 'when outside of an usage data file' do
it 'does not register an offense' do
expect_no_offenses('histogram(MergeRequest, buckets: 1..100)')
end
end
context 'with non related method' do
it 'does not register an offense' do
expect_no_offenses('count(Issue, buckets: 1..100)')
expect_no_offenses('histogram(Issue, :project_id, buckets: 1..100)')
end
end
end

View File

@ -14,49 +14,63 @@ RSpec.describe RuboCop::Cop::UsageData::InstrumentationSuperclass do
})
end
context 'with class definition' do
context 'when inheriting from allowed superclass' do
it 'does not register an offense' do
expect_no_offenses('class NewMetric < GenericMetric; end')
context 'when in an instrumentation file' do
before do
allow(cop).to receive(:in_instrumentation_file?).and_return(true)
end
context 'with class definition' do
context 'when inheriting from allowed superclass' do
it 'does not register an offense' do
expect_no_offenses('class NewMetric < GenericMetric; end')
end
end
context 'when inheriting from some other superclass' do
it 'registers an offense' do
expect_offense(<<~CODE)
class NewMetric < BaseMetric; end
^^^^^^^^^^ #{msg}
CODE
end
end
context 'when not inheriting' do
it 'does not register an offense' do
expect_no_offenses('class NewMetric; end')
end
end
end
context 'when inheriting from some other superclass' do
it 'registers an offense' do
expect_offense(<<~CODE)
class NewMetric < BaseMetric; end
^^^^^^^^^^ #{msg}
CODE
context 'with dynamic class definition' do
context 'when inheriting from allowed superclass' do
it 'does not register an offense' do
expect_no_offenses('NewMetric = Class.new(GenericMetric)')
end
end
end
context 'when not inheriting' do
it 'does not register an offense' do
expect_no_offenses('class NewMetric; end')
context 'when inheriting from some other superclass' do
it 'registers an offense' do
expect_offense(<<~CODE)
NewMetric = Class.new(BaseMetric)
^^^^^^^^^^ #{msg}
CODE
end
end
context 'when not inheriting' do
it 'does not register an offense' do
expect_no_offenses('NewMetric = Class.new')
end
end
end
end
context 'with dynamic class definition' do
context 'when inheriting from allowed superclass' do
it 'does not register an offense' do
expect_no_offenses('NewMetric = Class.new(GenericMetric)')
end
end
context 'when inheriting from some other superclass' do
it 'registers an offense' do
expect_offense(<<~CODE)
NewMetric = Class.new(BaseMetric)
^^^^^^^^^^ #{msg}
CODE
end
end
context 'when not inheriting' do
it 'does not register an offense' do
expect_no_offenses('NewMetric = Class.new')
end
context 'when outside of an instrumentation file' do
it "does not register an offense" do
expect_no_offenses(<<-RUBY)
class NewMetric < BaseMetric; end
RUBY
end
end
end

View File

@ -18,9 +18,9 @@ RSpec.describe RuboCop::Cop::UsageData::LargeTable do
})
end
context 'when in usage_data files' do
context 'in an usage data file' do
before do
allow(cop).to receive(:usage_data_files?).and_return(true)
allow(cop).to receive(:in_usage_data_file?).and_return(true)
end
context 'with large tables' do
@ -76,4 +76,10 @@ RSpec.describe RuboCop::Cop::UsageData::LargeTable do
end
end
end
context 'when outside of an usage data file' do
it 'does not register an offense' do
expect_no_offenses('Issue.active.count')
end
end
end

View File

@ -69,20 +69,23 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
end
describe '#execute' do
let!(:uuid) { SecureRandom.uuid }
let(:uuid) { SecureRandom.uuid }
let!(:recursion_uuid) { SecureRandom.uuid }
let(:headers) do
{
'Content-Type' => 'application/json',
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
'X-Gitlab-Webhook-UUID' => uuid,
'X-Gitlab-Event' => 'Push Hook',
'X-Gitlab-Event-UUID' => uuid,
'X-Gitlab-Event-UUID' => recursion_uuid,
'X-Gitlab-Instance' => Gitlab.config.gitlab.base_url
}
end
before do
# Set a stable value for the `X-Gitlab-Event-UUID` header.
Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
# Set stable values for the `X-Gitlab-Webhook-UUID` and `X-Gitlab-Event-UUID` headers.
allow(SecureRandom).to receive(:uuid).and_return(uuid)
Gitlab::WebHooks::RecursionDetection.set_request_uuid(recursion_uuid)
end
context 'when there is an interpolation error' do

View File

@ -54,39 +54,40 @@ module SimpleCovEnv
SimpleCov.configure do
load_profile 'test_frameworks'
add_filter '/bin/'
add_filter 'db/fixtures/development/' # Matches EE files as well
add_filter %r|db/migrate/\d{14}_init_schema\.rb\z| # Matches EE files as well
add_filter '/gems/'
add_filter '/vendor/'
add_filter %r{^/(ee/)?(bin|gems|vendor)}
add_filter %r{^/(ee/)?db/fixtures/development}
add_filter %r{^/(ee/)?db/migrate/\d{14}_init_schema\.rb\z}
add_group 'Channels', 'app/channels' # Matches EE files as well
add_group 'Components', 'app/components' # Matches EE files as well
add_group 'Config', %w[/config /ee/config]
add_group 'Controllers', 'app/controllers' # Matches EE files as well
add_group 'Elastic migrations', 'ee/elastic'
add_group 'Enums', 'app/enums' # Matches EE files as well
add_group 'Events', 'app/events' # Matches EE files as well
add_group 'Experiments', 'app/experiments' # Matches EE files as well
add_group 'Finders', 'app/finders' # Matches EE files as well
add_group 'Fixtures', 'db/fixtures' # Matches EE files as well
add_group 'GraphQL', 'app/graphql' # Matches EE files as well
add_group 'Helpers', 'app/helpers' # Matches EE files as well
add_group 'Mailers', 'app/mailers' # Matches EE files as well
add_group 'Models', 'app/models' # Matches EE files as well
add_group 'Policies', 'app/policies' # Matches EE files as well
add_group 'Presenters', 'app/presenters' # Matches EE files as well
add_group 'Replicators', 'app/replicators' # Matches EE files as well
add_group 'Serializers', 'app/serializers' # Matches EE files as well
add_group 'Services', 'app/services' # Matches EE files as well
add_group 'Uploaders', 'app/uploaders' # Matches EE files as well
add_group 'Validators', 'app/validators' # Matches EE files as well
add_group 'Views', 'app/views' # Matches EE files as well
add_group 'Workers', 'app/workers' # Matches EE files as well
add_group 'Initializers', %w[config/initializers config/initializers_before_autoloader] # Matches EE files as well
add_group 'Migrations', %w[db/migrate db/optional_migrations db/post_migrate db/geo/migrate db/geo/post_migrate] # Matches EE files as well
add_group 'Libraries', %w[/lib /ee/lib]
add_group 'Tooling', %w[/haml_lint /rubocop /scripts /tooling]
add_group 'Channels', %r{^/(ee/)?app/channels}
add_group 'Components', %r{^/(ee/)?app/components}
add_group 'Config', %r{^/(ee/)?config}
add_group 'Controllers', %r{^/(ee/)?app/controllers}
add_group 'Elastic migrations', %r{^/(ee/)?elastic}
add_group 'Enums', %r{^/(ee/)?app/enums}
add_group 'Events', %r{^/(ee/)?app/events}
add_group 'Experiments', %r{^/(ee/)?app/experiments}
add_group 'Finders', %r{^/(ee/)?app/finders}
add_group 'Fixtures', %r{^/(ee/)?db/fixtures}
add_group 'GraphQL', %r{^/(ee/)?app/graphql}
add_group 'Helpers', %r{^/(ee/)?app/helpers}
add_group 'Initializers', %r{^/(ee/)?config/(initializers|initializers_before_autoloader)}
add_group 'Libraries', %r{^/(ee/)?lib}
add_group 'Mailers', %r{^/(ee/)?app/mailers}
add_group 'Metrics server', %r{^/(ee/)?metrics_server}
add_group 'Migrations', %r{^/(ee/)?db/(geo/)?(migrate|optional_migrations|post_migrate)}
add_group 'Models', %r{^/(ee/)?app/models}
add_group 'Policies', %r{^/(ee/)?app/policies}
add_group 'Presenters', %r{^/(ee/)?app/presenters}
add_group 'Replicators', %r{^/(ee/)?app/replicators}
add_group 'Seeds', %r{^/(ee/)?db/seeds}
add_group 'Serializers', %r{^/(ee/)?app/serializers}
add_group 'Services', %r{^/(ee/)?app/services}
add_group 'Sidekiq cluster', %r{^/(ee/)?sidekiq_cluster}
add_group 'Tooling', %r{^/(ee/)?(danger|haml_lint|rubocop|scripts|tooling)}
add_group 'Uploaders', %r{^/(ee/)?app/uploaders}
add_group 'Validators', %r{^/(ee/)?app/validators}
add_group 'Views', %r{^/(ee/)?app/views}
add_group 'Workers', %r{^/(ee/)?app/workers}
merge_timeout 365 * 24 * 3600
end