Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a5d7e614fe
commit
995bcca3fc
|
|
@ -109,7 +109,6 @@ linters:
|
|||
# These cops should eventually get enabled
|
||||
- Cop/LineBreakAfterGuardClauses
|
||||
- Cop/ProjectPathHelper
|
||||
- Gitlab/FeatureAvailableUsage
|
||||
- Gitlab/Json
|
||||
- GitlabSecurity/PublicSend
|
||||
- Layout/EmptyLineAfterGuardClause
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ Gitlab/FeatureAvailableUsage:
|
|||
- 'ee/app/models/approval_state.rb'
|
||||
- 'ee/app/models/concerns/ee/approvable.rb'
|
||||
- 'ee/app/models/concerns/ee/project_security_scanners_information.rb'
|
||||
- 'ee/app/models/concerns/ee/protected_ref_access.rb'
|
||||
- 'ee/app/models/concerns/insights_feature.rb'
|
||||
- 'ee/app/models/ee/board.rb'
|
||||
- 'ee/app/models/ee/ci/build.rb'
|
||||
|
|
@ -68,7 +67,6 @@ Gitlab/FeatureAvailableUsage:
|
|||
- 'ee/app/services/ci/audit_variable_change_service.rb'
|
||||
- 'ee/app/services/dashboard/projects/create_service.rb'
|
||||
- 'ee/app/services/dashboard/projects/list_service.rb'
|
||||
- 'ee/app/services/ee/alert_management/http_integrations/create_service.rb'
|
||||
- 'ee/app/services/ee/audit_event_service.rb'
|
||||
- 'ee/app/services/ee/boards/issues/list_service.rb'
|
||||
- 'ee/app/services/ee/boards/lists/create_service.rb'
|
||||
|
|
@ -91,6 +89,31 @@ Gitlab/FeatureAvailableUsage:
|
|||
- 'ee/app/services/projects/mark_for_deletion_service.rb'
|
||||
- 'ee/app/services/requirements_management/process_test_reports_service.rb'
|
||||
- 'ee/app/services/security/store_scans_service.rb'
|
||||
- 'ee/app/views/compliance_management/compliance_framework/_project_settings.html.haml.rb'
|
||||
- 'ee/app/views/groups/epics/index.html.haml.rb'
|
||||
- 'ee/app/views/product_analytics/_project_settings.html.haml.rb'
|
||||
- 'ee/app/views/projects/_merge_request_settings.html.haml.rb'
|
||||
- 'ee/app/views/projects/_merge_request_settings_description_text.html.haml.rb'
|
||||
- 'ee/app/views/projects/_remove.html.haml.rb'
|
||||
- 'ee/app/views/projects/audit_events/index.html.haml.rb'
|
||||
- 'ee/app/views/projects/blob/_header_file_locks.html.haml.rb'
|
||||
- 'ee/app/views/projects/branch_defaults/_branch_names_help.html.haml.rb'
|
||||
- 'ee/app/views/projects/merge_requests/show.html.haml.rb'
|
||||
- 'ee/app/views/projects/push_rules/_index.html.haml.rb'
|
||||
- 'ee/app/views/projects/quality/test_cases/index.html.haml.rb'
|
||||
- 'ee/app/views/projects/settings/_default_issue_template.html.haml.rb'
|
||||
- 'ee/app/views/projects/settings/_restore.html.haml.rb'
|
||||
- 'ee/app/views/projects/settings/ci_cd/_auto_rollback.html.haml.rb'
|
||||
- 'ee/app/views/projects/settings/ci_cd/_pipeline_subscriptions.html.haml.rb'
|
||||
- 'ee/app/views/projects/settings/merge_requests/_merge_request_approvals_settings.html.haml.rb'
|
||||
- 'ee/app/views/projects/settings/operations/_status_page.html.haml.rb'
|
||||
- 'ee/app/views/projects/settings/repository/_protected_branches.html.haml.rb'
|
||||
- 'ee/app/views/protected_branches/ee/_code_owner_approval_form.html.haml.rb'
|
||||
- 'ee/app/views/protected_branches/ee/_code_owner_approval_table.html.haml.rb'
|
||||
- 'ee/app/views/protected_branches/ee/_code_owner_approval_table_head.html.haml.rb'
|
||||
- 'ee/app/views/shared/labels/_create_label_help_text.html.haml.rb'
|
||||
- 'ee/app/views/shared/promotions/_promote_mr_features.html.haml.rb'
|
||||
- 'ee/app/views/shared/promotions/_promote_repository_features.html.haml.rb'
|
||||
- 'ee/app/workers/analytics/code_review_metrics_worker.rb'
|
||||
- 'ee/app/workers/group_saml_group_sync_worker.rb'
|
||||
- 'ee/lib/ee/api/entities/approval_state.rb'
|
||||
|
|
|
|||
|
|
@ -687,7 +687,6 @@ Layout/EmptyLineAfterMagicComment:
|
|||
- 'spec/requests/lfs_http_spec.rb'
|
||||
- 'spec/rubocop/cop/migration/complex_indexes_require_name_spec.rb'
|
||||
- 'spec/rubocop/cop/migration/refer_to_index_by_name_spec.rb'
|
||||
- 'spec/rubocop/formatter/todo_formatter_spec.rb'
|
||||
- 'spec/scripts/lib/glfm/parse_examples_spec.rb'
|
||||
- 'spec/scripts/lib/glfm/shared_spec.rb'
|
||||
- 'spec/scripts/lib/glfm/update_example_snapshots_spec.rb'
|
||||
|
|
|
|||
|
|
@ -5370,9 +5370,7 @@ RSpec/MissingFeatureCategory:
|
|||
- 'spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb'
|
||||
- 'spec/rubocop/cop/usage_data/large_table_spec.rb'
|
||||
- 'spec/rubocop/cop/user_admin_spec.rb'
|
||||
- 'spec/rubocop/cop_todo_spec.rb'
|
||||
- 'spec/rubocop/formatter/graceful_formatter_spec.rb'
|
||||
- 'spec/rubocop/formatter/todo_formatter_spec.rb'
|
||||
- 'spec/rubocop/migration_helpers_spec.rb'
|
||||
- 'spec/rubocop/qa_helpers_spec.rb'
|
||||
- 'spec/rubocop/todo_dir_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import { sprintf, __, formatNumber } from '~/locale';
|
||||
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
|
||||
|
|
@ -49,6 +49,12 @@ export default {
|
|||
managersCount() {
|
||||
return this.runner.managers?.count || 0;
|
||||
},
|
||||
firstIpAddress() {
|
||||
return this.runner.managers?.nodes?.[0]?.ipAddress || null;
|
||||
},
|
||||
additionalIpAddressCount() {
|
||||
return this.managersCount - 1;
|
||||
},
|
||||
jobCount() {
|
||||
return formatJobCount(this.runner.jobCount);
|
||||
},
|
||||
|
|
@ -63,6 +69,9 @@ export default {
|
|||
return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatNumber,
|
||||
},
|
||||
i18n: {
|
||||
I18N_NO_DESCRIPTION,
|
||||
I18N_LOCKED_RUNNER_DESCRIPTION,
|
||||
|
|
@ -120,8 +129,11 @@ export default {
|
|||
</gl-sprintf>
|
||||
</runner-summary-field>
|
||||
|
||||
<runner-summary-field v-if="runner.ipAddress" icon="disk" :tooltip="__('IP Address')">
|
||||
{{ runner.ipAddress }}
|
||||
<runner-summary-field v-if="firstIpAddress" icon="disk" :tooltip="__('IP Address')">
|
||||
{{ firstIpAddress }}
|
||||
<template v-if="additionalIpAddressCount"
|
||||
>(+{{ formatNumber(additionalIpAddressCount) }})</template
|
||||
>
|
||||
</runner-summary-field>
|
||||
|
||||
<runner-summary-field icon="pipeline" data-testid="job-count" :tooltip="__('Jobs')">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ fragment ListItemShared on CiRunner {
|
|||
runnerType
|
||||
shortSha
|
||||
version
|
||||
ipAddress
|
||||
paused
|
||||
locked
|
||||
jobCount
|
||||
|
|
@ -22,8 +21,11 @@ fragment ListItemShared on CiRunner {
|
|||
updateRunner
|
||||
deleteRunner
|
||||
}
|
||||
managers {
|
||||
managers(first: 1) {
|
||||
count
|
||||
nodes {
|
||||
ipAddress
|
||||
}
|
||||
}
|
||||
groups(first: 1) {
|
||||
nodes {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const initMrMoreDropdown = () => {
|
|||
const {
|
||||
mergeRequest,
|
||||
projectPath,
|
||||
url,
|
||||
editUrl,
|
||||
isCurrentUser,
|
||||
isLoggedIn,
|
||||
|
|
@ -20,7 +21,6 @@ export const initMrMoreDropdown = () => {
|
|||
sourceProjectMissing,
|
||||
clipboardText,
|
||||
reportedUserId,
|
||||
reportedFromUrl,
|
||||
} = el.dataset;
|
||||
|
||||
let mr;
|
||||
|
|
@ -41,6 +41,7 @@ export const initMrMoreDropdown = () => {
|
|||
props: {
|
||||
mr,
|
||||
projectPath,
|
||||
url,
|
||||
editUrl,
|
||||
isCurrentUser,
|
||||
isLoggedIn: Boolean(isLoggedIn),
|
||||
|
|
@ -50,7 +51,6 @@ export const initMrMoreDropdown = () => {
|
|||
sourceProjectMissing,
|
||||
clipboardText,
|
||||
reportedUserId: Number(reportedUserId),
|
||||
reportedFromUrl,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ export default {
|
|||
default: '',
|
||||
required: false,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
editUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
|
@ -116,11 +121,6 @@ export default {
|
|||
default: 0,
|
||||
required: false,
|
||||
},
|
||||
reportedFromUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -156,7 +156,7 @@ export default {
|
|||
this.isLoadingDraft = true;
|
||||
|
||||
axios
|
||||
.put(`?merge_request[wip_event]=${this.draftState}`, null, {
|
||||
.put(`${this.url}?merge_request[wip_event]=${this.draftState}`, null, {
|
||||
params: { format: 'json' },
|
||||
})
|
||||
.then(({ data }) => {
|
||||
|
|
@ -353,7 +353,7 @@ export default {
|
|||
<abuse-category-selector
|
||||
v-if="!isCurrentUser && isReportAbuseDrawerOpen"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedFromUrl"
|
||||
:reported-from-url="url"
|
||||
:show-drawer="isReportAbuseDrawerOpen"
|
||||
@close-drawer="reportAbuseAction(false)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@
|
|||
&:not(.note-form).internal-note .timeline-content,
|
||||
&:not(.note-form).draft-note .timeline-content {
|
||||
background-color: $orange-50 !important;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.timeline-entry-inner {
|
||||
|
|
|
|||
|
|
@ -43,3 +43,15 @@
|
|||
.branches-list .branch-item:not(:last-of-type) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.branch-item {
|
||||
.issuable-reference {
|
||||
max-width: 92px;
|
||||
}
|
||||
|
||||
.right-block {
|
||||
@media (min-width: map-get($grid-breakpoints, md)) {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,20 +63,6 @@ class PlanLimits < ApplicationRecord
|
|||
|
||||
update(limits_history: limits_history)
|
||||
end
|
||||
|
||||
def limit_attribute_changes(attribute)
|
||||
limit_history = limits_history[attribute]
|
||||
return [] unless limit_history
|
||||
|
||||
limit_history.map do |entry|
|
||||
{
|
||||
timestamp: entry[:timestamp],
|
||||
value: entry[:value],
|
||||
username: entry[:username],
|
||||
user_id: entry[:user_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
PlanLimits.prepend_mod_with('PlanLimits')
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
= render_if_exists 'projects/branches/diverged_from_upstream', branch: branch
|
||||
|
||||
.block-truncated
|
||||
.gl-text-truncate
|
||||
- if commit
|
||||
= render 'projects/branches/commit', commit: commit, project: @project
|
||||
- else
|
||||
|
|
@ -28,35 +28,33 @@
|
|||
|
||||
.pipeline-status.d-none.d-md-block<
|
||||
- if commit_status
|
||||
= render 'ci/status/icon', size: 16, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5'
|
||||
= render 'ci/status/icon', size: 16, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-3'
|
||||
- elsif show_commit_status
|
||||
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5
|
||||
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-3
|
||||
%svg.s16
|
||||
|
||||
.right-block.gl-display-flex.gl-align-items-center.gl-justify-content-end
|
||||
.gl-mr-3
|
||||
- if mr_status.present?
|
||||
.issuable-reference.gl-display-flex.gl-justify-content-end.gl-overflow-hidden
|
||||
= gl_badge_tag issuable_reference(related_merge_request),
|
||||
{ icon: mr_status[:icon], variant: mr_status[:variant], size: :md, href: merge_request_path(related_merge_request) },
|
||||
{ class: 'gl-display-block gl-text-truncate', title: mr_status[:title], data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } }
|
||||
|
||||
- if mr_status.present?
|
||||
.issuable-reference.gl-display-flex.gl-justify-content-end.gl-min-w-10.gl-ml-5.gl-mr-4
|
||||
= gl_badge_tag issuable_reference(related_merge_request),
|
||||
{ icon: mr_status[:icon], variant: mr_status[:variant], size: :md, href: merge_request_path(related_merge_request) },
|
||||
{ class: 'gl-mr-2', title: mr_status[:title], data: { toggle: 'tooltip', container: 'body', qa_selector: 'badge_content' } }
|
||||
- elsif mr_status.nil? && create_mr_button?(from: branch.name, source_project: @project)
|
||||
= render Pajamas::ButtonComponent.new(icon: 'merge-request', href: create_mr_path(from: branch.name, source_project: @project), button_options: { class: 'has-tooltip', title: _('New merge request') }) do
|
||||
= _('New')
|
||||
|
||||
.controls.d-none.d-md-block<
|
||||
- if mr_status.nil? && create_mr_button?(from: branch.name, source_project: @project)
|
||||
= render Pajamas::ButtonComponent.new(icon: 'merge-request', href: create_mr_path(from: branch.name, source_project: @project), button_options: { class: 'has-tooltip gl-mr-2!', title: _('New merge request') }) do
|
||||
= _('New')
|
||||
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], css_class: 'gl-mr-2!'
|
||||
|
||||
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], css_class: 'gl-mr-1!'
|
||||
|
||||
- if !is_default_branch
|
||||
.js-branch-more-actions{ data: {
|
||||
branch_name: branch.name,
|
||||
default_branch_name: @repository.root_ref,
|
||||
can_delete_branch: user_access(@project).can_delete_branch?(branch.name).to_s,
|
||||
is_protected_branch: protected_branch?(@project, branch).to_s,
|
||||
merged: merged.to_s,
|
||||
compare_path: project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
|
||||
delete_path: project_branch_path(@project, branch.name),
|
||||
} }
|
||||
- else
|
||||
.gl-display-inline-flex.gl-w-7
|
||||
|
||||
.gl-w-7
|
||||
- if !is_default_branch
|
||||
.js-branch-more-actions{ data: {
|
||||
branch_name: branch.name,
|
||||
default_branch_name: @repository.root_ref,
|
||||
can_delete_branch: user_access(@project).can_delete_branch?(branch.name).to_s,
|
||||
is_protected_branch: protected_branch?(@project, branch).to_s,
|
||||
merged: merged.to_s,
|
||||
compare_path: project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
|
||||
delete_path: project_branch_path(@project, branch.name),
|
||||
} }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.branch-commit.gl-font-sm.gl-text-gray-500
|
||||
.branch-commit.gl-font-sm.gl-text-gray-500.gl-text-truncate
|
||||
= link_to commit.short_id, project_commit_path(project, commit.id), class: "commit-sha"
|
||||
·
|
||||
%span.str-truncated
|
||||
%span
|
||||
= link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message gl-text-gray-500!"
|
||||
·
|
||||
%span.gl-text-secondary= time_ago_with_tooltip(commit.committed_date)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
- if can_push_code
|
||||
= link_to new_project_branch_path(@project), class: 'gl-button btn btn-confirm' do
|
||||
= s_('Branches|New branch')
|
||||
.js-delete-merged-branches{ data: {
|
||||
.js-delete-merged-branches.gl-w-7{ data: {
|
||||
default_branch: @project.repository.root_ref,
|
||||
form_path: project_merged_branches_path(@project) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
.js-mr-more-dropdown{ data: {
|
||||
merge_request: @merge_request.to_json,
|
||||
project_path: @project.full_path,
|
||||
url: merge_request_url(@merge_request),
|
||||
edit_url: edit_project_merge_request_path(@project, @merge_request),
|
||||
is_current_user: issuable_author_is_current_user(@merge_request),
|
||||
is_logged_in: current_user,
|
||||
|
|
@ -11,5 +12,4 @@
|
|||
clipboard_text: @merge_request.to_reference(full: true),
|
||||
report_abuse_path: add_category_abuse_reports_path,
|
||||
reported_user_id: @merge_request.author.id,
|
||||
reported_from_url: merge_request_url(@merge_request),
|
||||
} }
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_includable_files_interpolation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113211
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/399146
|
||||
milestone: '15.11'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: phone_verification_for_low_risk_users
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124090
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/415674
|
||||
milestone: '16.2'
|
||||
type: experiment
|
||||
group: group::anti-abuse
|
||||
default_enabled: false
|
||||
|
|
@ -4,6 +4,23 @@ require 'gitlab/redis'
|
|||
|
||||
Redis.raise_deprecations = true unless Rails.env.production?
|
||||
|
||||
# rubocop:disable Gitlab/NoCodeCoverageComment
|
||||
# :nocov: This snippet is for local development only, reloading in specs would raise NameError
|
||||
if Rails.env.development?
|
||||
# reset all pools in the event of a reload
|
||||
# This makes sure that there are no stale references to classes in the `Gitlab::Redis` namespace
|
||||
# that also got reloaded.
|
||||
Gitlab::Application.config.to_prepare do
|
||||
Gitlab::Redis::ALL_CLASSES.each do |redis_instance|
|
||||
redis_instance.instance_variable_set(:@pool, nil)
|
||||
end
|
||||
|
||||
Rails.cache = ActiveSupport::Cache::RedisCacheStore.new(**Gitlab::Redis::Cache.active_support_config)
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
# rubocop:enable Gitlab/NoCodeCoverageComment
|
||||
|
||||
Redis::Client.prepend(Gitlab::Instrumentation::RedisInterceptor)
|
||||
Redis::Cluster::NodeLoader.prepend(Gitlab::Patch::NodeLoader)
|
||||
Redis::Cluster.prepend(Gitlab::Patch::RedisCluster)
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@ GET /projects/:id/releases/:tag_name/downloads/:direct_asset_path
|
|||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/downloads/bin/asset.exe"
|
||||
curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/downloads/bin/asset.exe"
|
||||
```
|
||||
|
||||
### Get the latest release
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
status: proposed
|
||||
status: implemented
|
||||
creation-date: "2022-11-23"
|
||||
authors: [ "@shinya.maeda" ]
|
||||
coach: "@DylanGriffith"
|
||||
approvers: [ "@nagyv-gitlab", "@cbalane", "@hustewart", "@hfyngvason" ]
|
||||
owning-stage: "~devops::release"
|
||||
participating-stages: [Configure, Release]
|
||||
owning-stage: "~devops::deploy"
|
||||
participating-stages: [Environments]
|
||||
---
|
||||
|
||||
<!-- vale gitlab.FutureTense = NO -->
|
||||
|
|
@ -29,8 +29,6 @@ This blueprint describes how the association is established and how these domain
|
|||
- The cluster resources and events can be visualized per [GitLab Environment](../../../ci/environments/index.md).
|
||||
An environment-specific view scoped to the resources managed either directly or indirectly by a deployment commit.
|
||||
- Support both [GitOps mode](../../../user/clusters/agent/gitops.md#gitops-configuration-reference) and [CI Access mode](../../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent).
|
||||
- NOTE: At the moment, we focus on the solution for CI Access mode. GitOps mode will have significant architectural changes _outside of_ this blueprint,
|
||||
such as [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947) and [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704). In order to derisk potential rework, we'll revisit the GitOps mode after these upstream changes have been settled.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
|
|
@ -41,22 +39,22 @@ This blueprint describes how the association is established and how these domain
|
|||
|
||||
### Overview
|
||||
|
||||
- GitLab Environment and Agent-managed Resource Group have 1-to-1 relationship.
|
||||
- Agent-managed Resource Group tracks all resources produced by the connected [agent](../../../user/clusters/agent/index.md). This includes not only resources written in manifest files but also subsequently generated resources (e.g. `Pod`s created by `Deployment` manifest file).
|
||||
- Agent-managed Resource Group renders dependency graph, such as `Deployment` => `ReplicaSet` => `Pod`. This is for providing ArgoCD-style resource view.
|
||||
- Agent-managed Resource Group has the Resource Health status that represents a summary of resource statuses, such as `Healthy`, `Progressing` or `Degraded`.
|
||||
- GitLab Environment and GitLab Agent For Kubernetes have 1-to-1 relationship.
|
||||
- GitLab Environment tracks all resources produced by the connected [agent](../../../user/clusters/agent/index.md). This includes not only resources written in manifest files but also subsequently generated resources (for example, `Pod`s created by `Deployment` manifest file).
|
||||
- GitLab Environment renders dependency graph, such as `Deployment` => `ReplicaSet` => `Pod`. This is for providing ArgoCD-style resource view.
|
||||
- GitLab Environment has the Resource Health status that represents a summary of resource statuses, such as `Healthy`, `Progressing` or `Degraded`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Kubernetes["Kubernetes"]
|
||||
subgraph ResourceGroupProduction["ResourceGroup"]
|
||||
subgraph ResourceGroupProduction["Production"]
|
||||
direction LR
|
||||
ResourceGroupProductionService(["Service"])
|
||||
ResourceGroupProductionDeployment(["Deployment"])
|
||||
ResourceGroupProductionPod1(["Pod1"])
|
||||
ResourceGroupProductionPod2(["Pod2"])
|
||||
end
|
||||
subgraph ResourceGroupStaging["ResourceGroup"]
|
||||
subgraph ResourceGroupStaging["Staging"]
|
||||
direction LR
|
||||
ResourceGroupStagingService(["Service"])
|
||||
ResourceGroupStagingDeployment(["Deployment"])
|
||||
|
|
@ -90,26 +88,18 @@ flowchart LR
|
|||
- GitLab Project and Agent have 1-to-many _direct_ relationship. Only one project can own a specific agent.
|
||||
- [GitOps mode](../../../user/clusters/agent/gitops.md#gitops-configuration-reference)
|
||||
- GitLab Project and Agent do _NOT_ have many-to-many _indirect_ relationship yet. This will be supported in [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704).
|
||||
- Agent and Agent-managed Resource Group have 1-to-1 relationship. Inventory IDs are used to group Kubernetes resources. This might be changed in [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947).
|
||||
- [CI Access mode](../../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent)
|
||||
- GitLab Project and Agent have many-to-many _indirect_ relationship. The project owning the agent can [share the access with the other proejcts](../../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent-to-access-projects-in-your-groups). (NOTE: Technically, only running jobs inside the project are allowed to access the cluster due to job-token authentication.)
|
||||
- Agent and Agent-managed Resource Group do _NOT_ have relationships yet.
|
||||
|
||||
### Issues
|
||||
|
||||
- Agent-managed Resource Group should have environment ID as the foreign key, which must be unique across resource groups.
|
||||
- Agent-managed Resource Group should have parameters how to group resources in the associated cluster, for example, `namespace`, `lable` and `inventory-id` (GitOps mode only) can passed as parameters.
|
||||
- Agent-managed Resource Group should be able to fetch all relevant resources, including both default resource kinds and other [Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/).
|
||||
- Agent-managed Resource Group should be aware of dependency graph.
|
||||
- Agent-managed Resource Group should be able to compute Resource Health status from the associated resources.
|
||||
- GitLab Environment should have ID of GitLab Agent For Kubernetes as the foreign key.
|
||||
- GitLab Environment should have parameters how to group resources in the associated cluster, for example, `namespace`, `lable` and `inventory-id` (GitOps mode only) can passed as parameters.
|
||||
- GitLab Environment should be able to fetch all relevant resources, including both default resource kinds and other [Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/).
|
||||
- GitLab Environment should be aware of dependency graph.
|
||||
- GitLab Environment should be able to compute Resource Health status from the associated resources.
|
||||
|
||||
### Example: Pull-based deployment (GitOps mode)
|
||||
|
||||
NOTE:
|
||||
At the moment, we focus on the solution for CI Access mode. GitOps mode will have significant architectural changes _outside of_ this blueprint,
|
||||
such as [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947) and [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704). In order to derisk potential rework, we'll revisit the GitOps mode after these upstream changes have been settled.
|
||||
|
||||
### Example: Push-based deployment (CI access mode)
|
||||
### Example
|
||||
|
||||
This is an example of how the architecture works in push-based deployment.
|
||||
The feature is documented [here](../../../user/clusters/agent/ci_cd_workflow.md) as CI access mode.
|
||||
|
|
@ -117,21 +107,21 @@ The feature is documented [here](../../../user/clusters/agent/ci_cd_workflow.md)
|
|||
```mermaid
|
||||
flowchart LR
|
||||
subgraph ProductionKubernetes["Production Kubernetes"]
|
||||
subgraph ResourceGroupProductionFrontend["ResourceGroup"]
|
||||
subgraph ResourceGroupProductionFrontend["Production"]
|
||||
direction LR
|
||||
ResourceGroupProductionFrontendService(["Service"])
|
||||
ResourceGroupProductionFrontendDeployment(["Deployment"])
|
||||
ResourceGroupProductionFrontendPod1(["Pod1"])
|
||||
ResourceGroupProductionFrontendPod2(["Pod2"])
|
||||
end
|
||||
subgraph ResourceGroupProductionBackend["ResourceGroup"]
|
||||
subgraph ResourceGroupProductionBackend["Staging"]
|
||||
direction LR
|
||||
ResourceGroupProductionBackendService(["Service"])
|
||||
ResourceGroupProductionBackendDeployment(["Deployment"])
|
||||
ResourceGroupProductionBackendPod1(["Pod1"])
|
||||
ResourceGroupProductionBackendPod2(["Pod2"])
|
||||
end
|
||||
subgraph ResourceGroupProductionPrometheus["ResourceGroup"]
|
||||
subgraph ResourceGroupProductionPrometheus["Monitoring"]
|
||||
direction LR
|
||||
ResourceGroupProductionPrometheusService(["Service"])
|
||||
ResourceGroupProductionPrometheusDeployment(["Deployment"])
|
||||
|
|
@ -202,21 +192,21 @@ The microservice project setup can be improved by [Multi-Project Deployment Pipe
|
|||
```mermaid
|
||||
flowchart LR
|
||||
subgraph ProductionKubernetes["Production Kubernetes"]
|
||||
subgraph ResourceGroupProductionFrontend["ResourceGroup"]
|
||||
subgraph ResourceGroupProductionFrontend["Frontend"]
|
||||
direction LR
|
||||
ResourceGroupProductionFrontendService(["Service"])
|
||||
ResourceGroupProductionFrontendDeployment(["Deployment"])
|
||||
ResourceGroupProductionFrontendPod1(["Pod1"])
|
||||
ResourceGroupProductionFrontendPod2(["Pod2"])
|
||||
end
|
||||
subgraph ResourceGroupProductionBackend["ResourceGroup"]
|
||||
subgraph ResourceGroupProductionBackend["Backend"]
|
||||
direction LR
|
||||
ResourceGroupProductionBackendService(["Service"])
|
||||
ResourceGroupProductionBackendDeployment(["Deployment"])
|
||||
ResourceGroupProductionBackendPod1(["Pod1"])
|
||||
ResourceGroupProductionBackendPod2(["Pod2"])
|
||||
end
|
||||
subgraph ResourceGroupProductionPrometheus["ResourceGroup"]
|
||||
subgraph ResourceGroupProductionPrometheus["Monitoring"]
|
||||
direction LR
|
||||
ResourceGroupProductionPrometheusService(["Service"])
|
||||
ResourceGroupProductionPrometheusDeployment(["Deployment"])
|
||||
|
|
@ -266,104 +256,18 @@ flowchart LR
|
|||
DeploymentPipelines -- "Deploy" --> ResourceGroupProductionBackend
|
||||
```
|
||||
|
||||
#### View all Agent-managed Resource Groups on production environment
|
||||
|
||||
At the group-level, we can accumulate all environments match a specific tier, for example,
|
||||
listing all environments with `production` tier from subsequent projects.
|
||||
This is useful to see the entire Agent-managed Resource Groups on production environment.
|
||||
The following diagram examplifies the relationship between GitLab group and Kubernetes resources:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Kubernetes["Kubernetes"]
|
||||
subgraph ResourceGroupProduction["ResourceGroup"]
|
||||
direction LR
|
||||
ResourceGroupProductionService(["Service"])
|
||||
ResourceGroupProductionDeployment(["Deployment"])
|
||||
ResourceGroupProductionPod1(["Pod1"])
|
||||
ResourceGroupProductionPod2(["Pod2"])
|
||||
end
|
||||
subgraph ResourceGroupStaging["ResourceGroup"]
|
||||
direction LR
|
||||
ResourceGroupStagingService(["Service"])
|
||||
ResourceGroupStagingDeployment(["Deployment"])
|
||||
ResourceGroupStagingPod1(["Pod1"])
|
||||
ResourceGroupStagingPod2(["Pod2"])
|
||||
end
|
||||
end
|
||||
|
||||
subgraph GitLab
|
||||
subgraph Organization
|
||||
OrganizationProduction["All resources on production"]
|
||||
subgraph Frontend project
|
||||
FrontendEnvironmentProduction["production environment"]
|
||||
end
|
||||
subgraph Backend project
|
||||
BackendEnvironmentProduction["production environment"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
FrontendEnvironmentProduction --- ResourceGroupProduction
|
||||
BackendEnvironmentProduction --- ResourceGroupStaging
|
||||
ResourceGroupProductionService -.- ResourceGroupProductionDeployment
|
||||
ResourceGroupProductionDeployment -.- ResourceGroupProductionPod1
|
||||
ResourceGroupProductionDeployment -.- ResourceGroupProductionPod2
|
||||
ResourceGroupStagingService -.- ResourceGroupStagingDeployment
|
||||
ResourceGroupStagingDeployment -.- ResourceGroupStagingPod1
|
||||
ResourceGroupStagingDeployment -.- ResourceGroupStagingPod2
|
||||
OrganizationProduction --- FrontendEnvironmentProduction
|
||||
OrganizationProduction --- BackendEnvironmentProduction
|
||||
```
|
||||
|
||||
A few notes:
|
||||
|
||||
- In the future, we'd have more granular filters for resource search.
|
||||
For example, there are two environments `production/us-region` and `production/eu-region` in each project
|
||||
and show only resources in US region at the group-level.
|
||||
This could be achivable by query filtering in PostgreSQL or label/namespace filtering in Kubernetes.
|
||||
- Please see [Add dynamically populated organization-level environments page](https://gitlab.com/gitlab-org/gitlab/-/issues/241506) for more information.
|
||||
|
||||
## Design and implementation details
|
||||
|
||||
NOTE:
|
||||
The following solution might be only applicable for CI Access mode. GitOps mode will have significant architectural changes _outside of_ this blueprint,
|
||||
such as [Flux switching](https://gitlab.com/gitlab-org/gitlab/-/issues/357947) and [Manifest projects outside of the Agent configuration project](https://gitlab.com/groups/gitlab-org/-/epics/7704). In order to derisk potential rework, we'll revisit the GitOps mode after these upstream changes have been settled.
|
||||
|
||||
### Associate Environment with Agent
|
||||
|
||||
As a preliminary step, we allow users to explicitly define "which deployment job" uses "which agent" and deploy to "which namespace". The following keywords are supported in `.gitlab-ci.yml`.
|
||||
Users can explicitly set a GitLab Agent For Kubernetes to a GitLab Environment in setting UI.
|
||||
Frontend will use this associated agent for authenticating/authorizing the user access, which is described in a latter section.
|
||||
|
||||
- `environment:kubernetes:agent` ... Define which agent the deployment job uses. It can select the appropriate context from the `KUBE_CONFIG`.
|
||||
- `environment:kubernetes:namespace` ... Define which namespace the deployment job deploys to. It injects `KUBE_NAMESPACE` predefined variable into the job. This keyword already [exists](../../../ci/yaml/index.md#environmentkubernetes).
|
||||
|
||||
Here is an example of `.gitlab-ci.yml`.
|
||||
|
||||
```yaml
|
||||
deploy-production:
|
||||
environment:
|
||||
name: production
|
||||
kubernetes:
|
||||
agent: path/to/agent/repository:agent-name
|
||||
namespace: default
|
||||
script:
|
||||
- helm --context="$KUBE_CONTEXT" --namespace="$KUBE_NAMESPACE" upgrade --install
|
||||
```
|
||||
|
||||
When a deployment job is created, GitLab persists the relationship of specified agent, namespace and deployment job. If the CI job is NOT authorized to access the agent (Please refer [`Clusters::Agents::FilterAuthorizationsService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/clusters/agents/filter_authorizations_service.rb) for more details), this relationship aren't recorded. This process happens in [`Deployments::CreateForBuildService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/deployments/create_for_build_service.rb). The database table scheme is:
|
||||
|
||||
```plaintext
|
||||
agent_deployments:
|
||||
- deployment_id (bigint/FK/NOT NULL/Unique)
|
||||
- agent_id (bigint/FK/NOT NULL)
|
||||
- kubernetes_namespace (character varying(255)/NOT NULL)
|
||||
```
|
||||
|
||||
To idenfity an associated agent for a specific environment, `environment.last_deployment.agent` can be used in Rails.
|
||||
We need to adjust the `read_cluster_agent` permission in DeclarivePolicy for supporting agents shared by an external project (also known as the Agent management project).
|
||||
|
||||
### Fetch resources through `user_access`
|
||||
|
||||
When user visits an environment page, GitLab frontend fetches an environment via GraphQL. Frontend additionally fetches the associated agent-ID and namespace through deployment relationship, which being tracked by the `agent_deployments` table.
|
||||
When user visits an environment page, GitLab frontend fetches an environment via GraphQL. Frontend additionally fetches the associated agent-ID and namespace.
|
||||
|
||||
Here is an example of GraphQL query:
|
||||
|
||||
|
|
@ -373,12 +277,12 @@ Here is an example of GraphQL query:
|
|||
id
|
||||
environment(name: "<environment-name>") {
|
||||
slug
|
||||
lastDeployment(status: SUCCESS) {
|
||||
agent {
|
||||
id
|
||||
kubernetesNamespace
|
||||
clusterAgent {
|
||||
id
|
||||
name
|
||||
project {
|
||||
name
|
||||
project
|
||||
kubernetesNamespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -388,12 +292,9 @@ Here is an example of GraphQL query:
|
|||
|
||||
GitLab frontend authenticate/authorize the user access with [browser cookie](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_user_access.md#browser-cookie-on-gitlab-frontend). If the access is forbidden, frontend shows an error message that `You don't have access to an agent that deployed to this environment. Please contact agent administrator if you are allowed in "user_access" in agent config file. See <troubleshooting-doc-link>`.
|
||||
|
||||
After the user gained access to the agent, GitLab frontend fetches available API Resource list in the Kubernetes and fetches the resources with the following parameters:
|
||||
After the user gained access to the agent, GitLab frontend fetches specific Resource kinds (for example, `Deployment`, `Pod`) in the Kubernetes with the following parameters:
|
||||
|
||||
- `namespace` ... `#{environment.lastDeployment.agent.kubernetesNamespace}`
|
||||
- `labels`
|
||||
- `app.gitlab.com/project_id=#{project.id}` _AND_
|
||||
- `app.gitlab.com/environment_slug: #{environment.slug}`
|
||||
- `namespace` ... `#{environment.kubernetesNamespace}`
|
||||
|
||||
If no resources are found, this is likely that the users have not embedded these lables into their resources. In this case, frontend shows an warning message `There are no resources found for the environment. Do resources have GitLab preserved labels? See <troubleshooting-doc-link>`.
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,43 @@ For features that use the embedding database, additional setup is needed.
|
|||
1. Run `gdk reconfigure`
|
||||
1. Run database migrations to create the embedding database
|
||||
|
||||
### Set up GitLab Chat
|
||||
|
||||
1. [Enable Anthropic API features](#configure-anthropic-access).
|
||||
1. [Enable OpenAI support](#configure-openai-access).
|
||||
1. [Ensure the embedding database is configured](#set-up-the-embedding-database).
|
||||
1. Enable feature specific feature flag.
|
||||
|
||||
```ruby
|
||||
Feature.enable(:gitlab_duo)
|
||||
Feature.enable(:tanuki_bot)
|
||||
Feature.enable(:ai_redis_cache)
|
||||
```
|
||||
|
||||
1. Ensure that your current branch is up-to-date with `master`.
|
||||
1. To access the GitLab Chat interface, in the lower-left corner of any page, select **Help** and **Ask GitLab Chat**.
|
||||
|
||||
#### Tips for local development
|
||||
|
||||
1. When responses are taking too long to appear in the user interface, consider restarting Sidekiq by running `gdk restart rails-background-jobs`. If that doesn't work, try `gdk kill` and then `gdk start`.
|
||||
1. Alternatively, bypass Sidekiq entirely and run the chat service syncronously. This can help with debugging errors as GraphQL errors are now available in the network inspector instead of the Sidekiq logs.
|
||||
|
||||
```diff
|
||||
diff --git a/ee/app/services/llm/chat_service.rb b/ee/app/services/llm/chat_service.rb
|
||||
index 5fa7ae8a2bc1..5fe996ba0345 100644
|
||||
--- a/ee/app/services/llm/chat_service.rb
|
||||
+++ b/ee/app/services/llm/chat_service.rb
|
||||
@@ -5,7 +5,7 @@ class ChatService < BaseService
|
||||
private
|
||||
|
||||
def perform
|
||||
- worker_perform(user, resource, :chat, options)
|
||||
+ worker_perform(user, resource, :chat, options.merge(sync: true))
|
||||
end
|
||||
|
||||
def valid?
|
||||
```
|
||||
|
||||
### Setup for GitLab documentation chat (legacy chat)
|
||||
|
||||
To populate the embedding database for GitLab chat:
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ is the [lib/](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/) folder.
|
|||
|
||||
The **lib/** folder is a mix of code that is generic/universal, GitLab-specific, and tightly integrated with the rest of the codebase.
|
||||
|
||||
Ask yourself the question: **can this code also be used in a Ruby project other than the current project?**.
|
||||
Ask yourself the question: **is this code generic or universal that can be done as a separate and small project?**.
|
||||
If the answer is **Yes** you should strongly consider starting with creating a new Gem [in the same repo](#in-the-same-repo)
|
||||
and eventually evaluate whether to extract it as a separate repository.
|
||||
and eventually evaluate whether to extract it as a separate repository if its meant to be used by other projects.
|
||||
|
||||
## Advantages of using Gems
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,32 @@ bundle exec guard
|
|||
|
||||
When using spring and guard together, use `SPRING=1 bundle exec guard` instead to make use of spring.
|
||||
|
||||
### General guidelines
|
||||
|
||||
- Use a single, top-level `RSpec.describe ClassName` block.
|
||||
- Use `.method` to describe class methods and `#method` to describe instance
|
||||
methods.
|
||||
- Use `context` to test branching logic (`RSpec/AvoidConditionalStatements` Rubocop Cop - [MR](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117152)).
|
||||
- Try to match the ordering of tests to the ordering in the class.
|
||||
- Try to follow the [Four-Phase Test](https://thoughtbot.com/blog/four-phase-test) pattern, using newlines
|
||||
to separate phases.
|
||||
- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`.
|
||||
- Don't assert against the absolute value of a sequence-generated attribute (see
|
||||
[Gotchas](../gotchas.md#do-not-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
|
||||
- Avoid using `expect_any_instance_of` or `allow_any_instance_of` (see
|
||||
[Gotchas](../gotchas.md#avoid-using-expect_any_instance_of-or-allow_any_instance_of-in-rspec)).
|
||||
- Don't supply the `:each` argument to hooks because it's the default.
|
||||
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`.
|
||||
- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
|
||||
use a Capybara matcher beforehand (such as `find('.js-foo')`) to ensure the element actually exists.
|
||||
- Use `focus: true` to isolate parts of the specs you want to run.
|
||||
- Use [`:aggregate_failures`](https://rspec.info/features/3-12/rspec-core/expectation-framework-integration/aggregating-failures/) when there is more than one expectation in a test.
|
||||
- For [empty test description blocks](https://github.com/rubocop-hq/rspec-style-guide#it-and-specify), use `specify` rather than `it do` if the test is self-explanatory.
|
||||
- Use `non_existing_record_id`/`non_existing_record_iid`/`non_existing_record_access_level`
|
||||
when you need an ID/IID/access level that doesn't actually exist. Using 123, 1234,
|
||||
or even 999 is brittle as these IDs could actually exist in the database in the
|
||||
context of a CI run.
|
||||
|
||||
### Eager loading the application code
|
||||
|
||||
By default, the application code:
|
||||
|
|
@ -132,9 +158,44 @@ We should reduce test dependencies, and avoiding
|
|||
capabilities also reduces the amount of set-up needed.
|
||||
|
||||
`:js` is particularly important to avoid. This must only be used if the feature
|
||||
test requires JavaScript reactivity in the browser. Using a headless
|
||||
test requires JavaScript reactivity in the browser (e.g. clicking a Vue.js component). Using a headless
|
||||
browser is much slower than parsing the HTML response from the app.
|
||||
|
||||
#### Profiling: see where your test spend its time
|
||||
|
||||
[`rspec-stackprof`](https://github.com/dkhroad/rspec-stackprof) can be used to generate a flame graph that shows you where you test spend its time.
|
||||
|
||||
The gem generates a JSON report that we can upload to <https://www.speedscope.app> for an interactive visualization.
|
||||
|
||||
##### Installation
|
||||
|
||||
`stackprof` gem is [already installed with GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/695fcee0c5541b4ead0a90eb9b8bf0b69bee796c/Gemfile#L487), and we also have a script available that generates the JSON report (`bin/rspec-stackprof`).
|
||||
|
||||
```shell
|
||||
# Optional: install the `speedscope` package to easily upload the JSON report to https://www.speedscope.app
|
||||
npm install -g speedscope
|
||||
```
|
||||
|
||||
##### Generate the JSON report
|
||||
|
||||
```shell
|
||||
bin/rspec-stackprof --speedscope=true <your_slow_spec>
|
||||
# There will be the name of the report displayed when the script ends.
|
||||
|
||||
# Upload the JSON report to speedscope.app
|
||||
speedscope tmp/<your-json-report>.json
|
||||
```
|
||||
|
||||
##### How to interpret the flamegraph
|
||||
|
||||
Below are some useful tips to interpret and navigate the flamegraph:
|
||||
|
||||
- There are [several views available](https://github.com/jlfwong/speedscope#views) for the flamegraph. `Left Heavy` is particularly useful when there are a lot of function calls (e.g. feature specs).
|
||||
- You can zoom in or out! See [the navigation documentation](https://github.com/jlfwong/speedscope#navigation)
|
||||
- If you are working on a slow feature test, search for `Capybara::DSL#` in the search to see the capybara actions that are made, and how long they take!
|
||||
|
||||
See [#414929](https://gitlab.com/gitlab-org/gitlab/-/issues/414929#note_1425239887) or [#375004](https://gitlab.com/gitlab-org/gitlab/-/issues/375004#note_1109867718) for some analysis examples.
|
||||
|
||||
#### Optimize factory usage
|
||||
|
||||
A common cause of slow tests is excessive creation of objects, and thus
|
||||
|
|
@ -240,9 +301,21 @@ There are various ways to create objects and store them in variables in your tes
|
|||
- `let` lazily creates the object. It isn't created until the object is called. `let` is generally inefficient as it creates a new object for every example. `let` is fine for simple values. However, more efficient variants of `let` are best when dealing with database models such as factories.
|
||||
- `let_it_be_with_refind` works similar to `let_it_be_with_reload`, but the [former calls `ActiveRecord::Base#find`](https://github.com/test-prof/test-prof/blob/936b29f87b36f88a134e064aa6d8ade143ae7a13/lib/test_prof/ext/active_record_refind.rb#L15) instead of `ActiveRecord::Base#reload`. `reload` is usually faster than `refind`.
|
||||
- `let_it_be_with_reload` creates an object one time for all examples in the same context, but after each example, the database changes are rolled back, and `object.reload` will be called to restore the object to its original state. This means you can make changes to the object before or during an example. However, there are cases where [state leaks across other models](https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#state-leakage-detection) can occur. In these cases, `let` may be an easier option, especially if only a few examples exist.
|
||||
- `let_it_be` creates an immutable object one time for all of the examples in the same context. This is a great alternative to `let` and `let!` for objects that do not need to change from one example to another. Using `let_it_be` can dramatically speed up tests that create database models. See <https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#let-it-be> for more details and examples.
|
||||
- `let_it_be` creates an object one time for all of the examples in the same context. This is a great alternative to `let` and `let!` for objects that do not need to change from one example to another. Using `let_it_be` can dramatically speed up tests that create database models. See <https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#let-it-be> for more details and examples.
|
||||
|
||||
`let_it_be` is the most optimized option since it instantiates an object once and does not change it. If you find yourself needing `let` instead of `let_it_be`, try `let_it_be_with_reload`.
|
||||
Pro-tip: When writing tests, it is best to consider the objects inside a `let_it_be` as **immutable**, as there are some important caveats when modifying objects inside a `let_it_be` declaration ([1](https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#database-is-rolled-back-to-a-pristine-state-but-the-objects-are-not), [2](https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#modifiers)). To make your `let_it_be` objects immutable, consider using `.freeze`:
|
||||
|
||||
```shell
|
||||
# Before
|
||||
let_it_be(:namespace) { create_default(:namespace)
|
||||
|
||||
# After
|
||||
let_it_be(:namespace) { create_default(:namespace).freeze
|
||||
```
|
||||
|
||||
See <https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#state-leakage-detection> for more information on `let_it_be` freezing.
|
||||
|
||||
`let_it_be` is the most optimized option since it instantiates an object once and shares its instance across examples. If you find yourself needing `let` instead of `let_it_be`, try `let_it_be_with_reload`.
|
||||
|
||||
```ruby
|
||||
# Old
|
||||
|
|
@ -336,6 +409,35 @@ NOTE:
|
|||
Refrain from using this stub helper if the test code relies on persisting
|
||||
`project_authorizations` or `Member` records. Use `Project#add_member` or `Group#add_member` instead.
|
||||
|
||||
#### Additional profiling metrics
|
||||
|
||||
We can use the `rspec_profiling` gem to diagnose, for instance, the number of SQL queries we're making when running a test.
|
||||
|
||||
This could be caused by some application side SQL queries **triggered by a test that could mock parts that are not under test** (e.g. [!123810](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123810)).
|
||||
|
||||
[See the instructions in the performance docs](../performance.md#rspec-profiling).
|
||||
|
||||
#### Troubleshoot slow feature test
|
||||
|
||||
A slow feature test can generally be optimized the same way as any other test. However, there are some specific techniques that can make the troubleshooting session more fruitful.
|
||||
|
||||
##### See what the feature test is doing in the UI
|
||||
|
||||
```shell
|
||||
# Before
|
||||
bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
|
||||
|
||||
# After
|
||||
WEBDRIVER_HEADLESS=0 bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
|
||||
```
|
||||
|
||||
See [Run `:js` spec in a visible browser](#run-js-spec-in-a-visible-browser) for more info.
|
||||
|
||||
##### Search for `Capybara::DSL#` when using profiling
|
||||
|
||||
<!-- TODO: Add the search keywords -->
|
||||
When using [`stackprof` flamegraphs](#profiling-see-where-your-test-spend-its-time), search for `Capybara::DSL#` in the search to see the capybara actions that are made, and how long they take!
|
||||
|
||||
#### Identify slow tests
|
||||
|
||||
Running a spec with profiling is a good way to start optimizing a spec. This can
|
||||
|
|
@ -437,32 +539,6 @@ performance gains.
|
|||
When combining tests, consider using `:aggregate_failures`, so that the full
|
||||
results are available, and not just the first failure.
|
||||
|
||||
### General guidelines
|
||||
|
||||
- Use a single, top-level `RSpec.describe ClassName` block.
|
||||
- Use `.method` to describe class methods and `#method` to describe instance
|
||||
methods.
|
||||
- Use `context` to test branching logic.
|
||||
- Try to match the ordering of tests to the ordering in the class.
|
||||
- Try to follow the [Four-Phase Test](https://thoughtbot.com/blog/four-phase-test) pattern, using newlines
|
||||
to separate phases.
|
||||
- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
|
||||
- Don't assert against the absolute value of a sequence-generated attribute (see
|
||||
[Gotchas](../gotchas.md#do-not-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
|
||||
- Avoid using `expect_any_instance_of` or `allow_any_instance_of` (see
|
||||
[Gotchas](../gotchas.md#avoid-using-expect_any_instance_of-or-allow_any_instance_of-in-rspec)).
|
||||
- Don't supply the `:each` argument to hooks because it's the default.
|
||||
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
|
||||
- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
|
||||
use a Capybara matcher beforehand (such as `find('.js-foo')`) to ensure the element actually exists.
|
||||
- Use `focus: true` to isolate parts of the specs you want to run.
|
||||
- Use [`:aggregate_failures`](https://rspec.info/features/3-12/rspec-core/expectation-framework-integration/aggregating-failures/) when there is more than one expectation in a test.
|
||||
- For [empty test description blocks](https://github.com/rubocop-hq/rspec-style-guide#it-and-specify), use `specify` rather than `it do` if the test is self-explanatory.
|
||||
- Use `non_existing_record_id`/`non_existing_record_iid`/`non_existing_record_access_level`
|
||||
when you need an ID/IID/access level that doesn't actually exists. Using 123, 1234,
|
||||
or even 999 is brittle as these IDs could actually exist in the database in the
|
||||
context of a CI run.
|
||||
|
||||
### Feature category metadata
|
||||
|
||||
You must [set feature category metadata for each RSpec example](../feature_categorization/index.md#rspec-examples).
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ To install a package:
|
|||
- Connect to the Package Registry for your group:
|
||||
|
||||
```shell
|
||||
composer config repositories.<group_id> composer https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/
|
||||
composer config repositories.<group_id> composer https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/packages.json
|
||||
```
|
||||
|
||||
- Set the required package version:
|
||||
|
|
@ -159,7 +159,7 @@ To install a package:
|
|||
"repositories": {
|
||||
"<group_id>": {
|
||||
"type": "composer",
|
||||
"url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/"
|
||||
"url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/packages.json"
|
||||
},
|
||||
...
|
||||
},
|
||||
|
|
@ -243,7 +243,7 @@ To install a package:
|
|||
{
|
||||
...
|
||||
"repositories": [
|
||||
{ "type": "composer", "url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/" }
|
||||
{ "type": "composer", "url": "https://gitlab.example.com/api/v4/group/<group_id>/-/packages/composer/packages.json" }
|
||||
],
|
||||
"config": {
|
||||
...
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ If a user is:
|
|||
|
||||
### Membership and visibility rights
|
||||
|
||||
Depending on their membership type, members of groups or projects are granted different visibility levels
|
||||
Depending on their membership type, members of groups or projects are granted different [visibility levels](../../../user/public_access.md)
|
||||
and rights into the group or project.
|
||||
|
||||
| Action | Direct group member | Inherited group member | Direct shared group member | Inherited shared group member |
|
||||
|
|
|
|||
|
|
@ -7,18 +7,24 @@ type: reference
|
|||
|
||||
# Project and group visibility **(FREE)**
|
||||
|
||||
A project in GitLab can be private, internal, or public.
|
||||
Projects and groups in GitLab can be private, internal, or public.
|
||||
|
||||
The visibility level of the group or project has no influence on whether members within the group or project can see each other.
|
||||
A group or project is an object to allow collaborative work. This is only possible if all members know about each other.
|
||||
|
||||
Group or project members can see all members of the group or project they belong to.
|
||||
Group or project owners can see the origin of membership (the original group or project) of all members.
|
||||
|
||||
## Private projects and groups
|
||||
|
||||
For private projects, only project members can:
|
||||
For private projects, only members of the private project or group can:
|
||||
|
||||
- Clone the project.
|
||||
- View the public access directory (`/public`).
|
||||
|
||||
Users with the Guest role cannot clone the project.
|
||||
|
||||
Private groups can have private subgroups only.
|
||||
Private groups can have only private subgroups.
|
||||
|
||||
## Internal projects and groups **(FREE SELF)**
|
||||
|
||||
|
|
@ -27,6 +33,8 @@ For internal projects, **any authenticated user**, including users with the Gues
|
|||
- Clone the project.
|
||||
- View the public access directory (`/public`).
|
||||
|
||||
Only internal members can view internal content.
|
||||
|
||||
[External users](admin_area/external_users.md) cannot clone the project.
|
||||
|
||||
Internal groups can have internal or private subgroups.
|
||||
|
|
|
|||
|
|
@ -90,15 +90,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def load_and_validate_expanded_hash!
|
||||
context.logger.instrument(:config_file_fetch_content_hash) do
|
||||
content_result # calling the method loads YAML then memoizes the content result
|
||||
end
|
||||
|
||||
context.logger.instrument(:config_file_interpolate_result) do
|
||||
interpolator.interpolate!
|
||||
end
|
||||
|
||||
return validate_interpolation! unless interpolator.valid?
|
||||
return errors.push("`#{masked_location}`: #{content_result.error}") unless content_result.valid?
|
||||
|
||||
context.logger.instrument(:config_file_expand_content_includes) do
|
||||
expanded_content_hash # calling the method expands then memoizes the result
|
||||
|
|
@ -109,36 +101,24 @@ module Gitlab
|
|||
|
||||
protected
|
||||
|
||||
def content_result
|
||||
::Gitlab::Ci::Config::Yaml
|
||||
.load_result!(content, project: context.project)
|
||||
end
|
||||
strong_memoize_attr :content_result
|
||||
|
||||
def content_inputs
|
||||
# TODO: remove support for `with` syntax in 16.1, see https://gitlab.com/gitlab-org/gitlab/-/issues/408369
|
||||
# In the interim prefer `inputs` over `with` while allow either syntax.
|
||||
params.to_h.slice(:inputs, :with).each_value.first
|
||||
end
|
||||
strong_memoize_attr :content_inputs
|
||||
|
||||
def content_hash
|
||||
interpolator.interpolate!
|
||||
|
||||
interpolator.to_hash
|
||||
def content_result
|
||||
context.logger.instrument(:config_file_fetch_content_hash) do
|
||||
::Gitlab::Ci::Config::Yaml::Loader.new(content, inputs: content_inputs, current_user: context.user).load
|
||||
end
|
||||
end
|
||||
strong_memoize_attr :content_hash
|
||||
|
||||
def interpolator
|
||||
Yaml::Interpolator.new(content_result, content_inputs, context)
|
||||
end
|
||||
strong_memoize_attr :interpolator
|
||||
strong_memoize_attr :content_result
|
||||
|
||||
def expanded_content_hash
|
||||
return if content_hash.blank?
|
||||
return if content_result.content.blank?
|
||||
|
||||
strong_memoize(:expanded_content_hash) do
|
||||
expand_includes(content_hash)
|
||||
expand_includes(content_result.content)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -148,12 +128,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def validate_interpolation!
|
||||
return if interpolator.valid?
|
||||
|
||||
errors.push("`#{masked_location}`: #{interpolator.error_message}")
|
||||
end
|
||||
|
||||
def expand_includes(hash)
|
||||
External::Processor.new(hash, context.mutate(expand_context_attrs)).perform
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,21 +4,17 @@ module Gitlab
|
|||
module Ci
|
||||
class Config
|
||||
module Yaml
|
||||
LoadError = Class.new(StandardError)
|
||||
|
||||
class << self
|
||||
def load!(content, project: nil)
|
||||
Loader.new(content, project: project).to_result.then do |result|
|
||||
##
|
||||
# raise an error for backwards compatibility
|
||||
#
|
||||
raise result.error unless result.valid?
|
||||
def load!(content, current_user: nil)
|
||||
Loader.new(content, current_user: current_user).load.then do |result|
|
||||
raise result.error_class, result.error if !result.valid? && result.error_class.present?
|
||||
raise LoadError, result.error unless result.valid?
|
||||
|
||||
result.content
|
||||
end
|
||||
end
|
||||
|
||||
def load_result!(content, project: nil)
|
||||
Loader.new(content, project: project).to_result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,42 +5,23 @@ module Gitlab
|
|||
class Config
|
||||
module Yaml
|
||||
##
|
||||
# Config::Yaml::Interpolation performs includable file interpolation, and surfaces all possible interpolation
|
||||
# Config::Yaml::Interpolator performs CI config file interpolation, and surfaces all possible interpolation
|
||||
# errors. It is designed to provide an external file's validation context too.
|
||||
#
|
||||
class Interpolator
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
attr_reader :config, :args, :current_user, :errors
|
||||
|
||||
attr_reader :config, :args, :ctx, :errors
|
||||
|
||||
def initialize(config, args, ctx = nil)
|
||||
def initialize(config, args, current_user: nil)
|
||||
@config = config
|
||||
@args = args.to_h
|
||||
@ctx = ctx
|
||||
@current_user = current_user
|
||||
@errors = []
|
||||
|
||||
validate!
|
||||
end
|
||||
|
||||
def valid?
|
||||
@errors.none?
|
||||
end
|
||||
|
||||
def ready?
|
||||
##
|
||||
# Interpolation is ready when it has been either interrupted by an error or finished with a result.
|
||||
#
|
||||
@result || @errors.any?
|
||||
end
|
||||
|
||||
def interpolate?
|
||||
enabled? && has_header? && valid?
|
||||
end
|
||||
|
||||
def has_header?
|
||||
config.has_header? && config.header.present?
|
||||
end
|
||||
|
||||
def to_hash
|
||||
@result.to_h
|
||||
end
|
||||
|
|
@ -55,43 +36,25 @@ module Gitlab
|
|||
@errors.first(3).join(', ')
|
||||
end
|
||||
|
||||
##
|
||||
# TODO Add `instrument.logger` instrumentation blocks:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/396722
|
||||
#
|
||||
def interpolate!
|
||||
return {} unless valid?
|
||||
return @result ||= content.to_h unless interpolate?
|
||||
return @errors.push(config.error) unless config.valid?
|
||||
return @result ||= config.content unless config.has_header?
|
||||
|
||||
return @errors.concat(header.errors) unless header.valid?
|
||||
return @errors.concat(inputs.errors) unless inputs.valid?
|
||||
return @errors.concat(context.errors) unless context.valid?
|
||||
return @errors.concat(template.errors) unless template.valid?
|
||||
|
||||
if ctx&.user
|
||||
::Gitlab::UsageDataCounters::HLLRedisCounter.track_event('ci_interpolation_users', values: ctx.user.id)
|
||||
if current_user.present?
|
||||
::Gitlab::UsageDataCounters::HLLRedisCounter
|
||||
.track_event('ci_interpolation_users', values: current_user.id)
|
||||
end
|
||||
|
||||
@result ||= template.interpolated.to_h.deep_symbolize_keys
|
||||
end
|
||||
strong_memoize_attr :interpolate!
|
||||
|
||||
private
|
||||
|
||||
def validate!
|
||||
return errors.push('content does not have a valid YAML syntax') unless config.valid?
|
||||
|
||||
return unless has_header? && !enabled?
|
||||
|
||||
errors.push('can not evaluate included file because interpolation is disabled')
|
||||
end
|
||||
|
||||
def enabled?
|
||||
return false if ctx.nil?
|
||||
|
||||
::Feature.enabled?(:ci_includable_files_interpolation, ctx.project)
|
||||
end
|
||||
|
||||
def header
|
||||
@entry ||= Ci::Config::Header::Root.new(config.header).tap do |header|
|
||||
header.key = 'header'
|
||||
|
|
|
|||
|
|
@ -5,33 +5,45 @@ module Gitlab
|
|||
class Config
|
||||
module Yaml
|
||||
class Loader
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze
|
||||
MAX_DOCUMENTS = 2
|
||||
|
||||
def initialize(content, project: nil)
|
||||
def initialize(content, inputs: {}, current_user: nil)
|
||||
@content = content
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@inputs = inputs
|
||||
end
|
||||
|
||||
def to_result
|
||||
Yaml::Result.new(config: load!, error: nil)
|
||||
rescue ::Gitlab::Config::Loader::FormatError => e
|
||||
Yaml::Result.new(error: e)
|
||||
def load
|
||||
yaml_result = load_uninterpolated_yaml
|
||||
|
||||
return yaml_result unless yaml_result.valid?
|
||||
|
||||
interpolator = Yaml::Interpolator.new(yaml_result, inputs, current_user: current_user)
|
||||
|
||||
interpolator.interpolate!
|
||||
|
||||
if interpolator.valid?
|
||||
# This Result contains only the interpolated config and does not have a header
|
||||
Yaml::Result.new(config: interpolator.to_hash, error: nil)
|
||||
else
|
||||
Yaml::Result.new(error: interpolator.error_message)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :content, :project
|
||||
attr_reader :content, :current_user, :inputs
|
||||
|
||||
def ensure_custom_tags
|
||||
@ensure_custom_tags ||= begin
|
||||
AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
|
||||
|
||||
true
|
||||
end
|
||||
def load_uninterpolated_yaml
|
||||
Yaml::Result.new(config: load_yaml!, error: nil)
|
||||
rescue ::Gitlab::Config::Loader::FormatError => e
|
||||
Yaml::Result.new(error: e.message, error_class: e)
|
||||
end
|
||||
|
||||
def load!
|
||||
def load_yaml!
|
||||
ensure_custom_tags
|
||||
|
||||
::Gitlab::Config::Loader::MultiDocYaml.new(
|
||||
|
|
@ -41,6 +53,14 @@ module Gitlab
|
|||
reject_empty: true
|
||||
).load!
|
||||
end
|
||||
|
||||
def ensure_custom_tags
|
||||
@ensure_custom_tags ||= begin
|
||||
AVAILABLE_TAGS.each { |klass| Psych.add_tag(klass.tag, klass) }
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ module Gitlab
|
|||
class Config
|
||||
module Yaml
|
||||
class Result
|
||||
attr_reader :error
|
||||
attr_reader :error, :error_class
|
||||
|
||||
def initialize(config: nil, error: nil)
|
||||
def initialize(config: nil, error: nil, error_class: nil)
|
||||
@config = Array.wrap(config)
|
||||
@error = error
|
||||
@error_class = error_class
|
||||
end
|
||||
|
||||
def valid?
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ module RuboCop
|
|||
@offense_count += offense_count
|
||||
end
|
||||
|
||||
def add_files(files)
|
||||
@files.merge(files)
|
||||
end
|
||||
|
||||
def autocorrectable?
|
||||
@cop_class&.support_autocorrect?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ module RuboCop
|
|||
class TodoFormatter < BaseFormatter
|
||||
DEFAULT_BASE_DIRECTORY = File.expand_path('../../.rubocop_todo', __dir__)
|
||||
|
||||
# Make sure that HAML exclusions are retained.
|
||||
# This allows enabling cop rules in haml-lint and only exclude HAML files
|
||||
# with offenses.
|
||||
#
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/415330#caveats
|
||||
# on why the entry must end with `.html.haml.rb`.
|
||||
RETAIN_EXCLUSIONS = %r{\.html\.haml\.rb$}
|
||||
|
||||
class << self
|
||||
attr_accessor :base_directory
|
||||
end
|
||||
|
|
@ -31,7 +39,7 @@ module RuboCop
|
|||
@config_inspect_todo_dir = load_config_inspect_todo_dir
|
||||
@config_old_todo_yml = load_config_old_todo_yml
|
||||
check_multiple_configurations!
|
||||
create_empty_todos(@config_inspect_todo_dir)
|
||||
create_todos_retaining_exclusions(@config_inspect_todo_dir)
|
||||
|
||||
super
|
||||
end
|
||||
|
|
@ -81,11 +89,10 @@ module RuboCop
|
|||
raise "Multiple configurations found for cops:\n#{list}\n"
|
||||
end
|
||||
|
||||
# For each inspected cop TODO config create a TODO object to make sure
|
||||
# the cop TODO config will be written even without any offenses.
|
||||
def create_empty_todos(inspected_cop_config)
|
||||
inspected_cop_config.each_key do |cop_name|
|
||||
@todos[cop_name]
|
||||
def create_todos_retaining_exclusions(inspected_cop_config)
|
||||
inspected_cop_config.each do |cop_name, config|
|
||||
todo = @todos[cop_name]
|
||||
todo.add_files(config.fetch('Exclude', []).grep(RETAIN_EXCLUSIONS))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
|
@ -31,8 +32,8 @@ describe('RunnerTypeCell', () => {
|
|||
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
|
||||
.wrappers[0];
|
||||
|
||||
const createComponent = (runner, options) => {
|
||||
wrapper = mountExtended(RunnerSummaryCell, {
|
||||
const createComponent = ({ runner, mountFn = shallowMountExtended, ...options } = {}) => {
|
||||
wrapper = mountFn(RunnerSummaryCell, {
|
||||
propsData: {
|
||||
runner: {
|
||||
...mockRunner,
|
||||
|
|
@ -40,7 +41,7 @@ describe('RunnerTypeCell', () => {
|
|||
},
|
||||
},
|
||||
stubs: {
|
||||
RunnerSummaryField,
|
||||
GlSprintf,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
|
@ -51,6 +52,8 @@ describe('RunnerTypeCell', () => {
|
|||
});
|
||||
|
||||
it('Displays the runner name as id and short token', () => {
|
||||
createComponent({ mountFn: mountExtended });
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
|
||||
);
|
||||
|
|
@ -58,13 +61,16 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Displays no runner manager count', () => {
|
||||
createComponent({
|
||||
managers: { count: 0 },
|
||||
runner: { managers: { nodes: { count: 0 } } },
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
|
||||
expect(findRunnerManagersBadge().html()).toBe('');
|
||||
});
|
||||
|
||||
it('Displays runner manager count', () => {
|
||||
createComponent({ mountFn: mountExtended });
|
||||
|
||||
expect(findRunnerManagersBadge().text()).toBe('2');
|
||||
});
|
||||
|
||||
|
|
@ -74,8 +80,8 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Displays the locked icon for locked runners', () => {
|
||||
createComponent({
|
||||
runnerType: PROJECT_TYPE,
|
||||
locked: true,
|
||||
runner: { runnerType: PROJECT_TYPE, locked: true },
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
|
||||
expect(findLockIcon().exists()).toBe(true);
|
||||
|
|
@ -83,8 +89,8 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Displays the runner type', () => {
|
||||
createComponent({
|
||||
runnerType: INSTANCE_TYPE,
|
||||
locked: true,
|
||||
runner: { runnerType: INSTANCE_TYPE, locked: true },
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE);
|
||||
|
|
@ -101,7 +107,7 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Displays "No description" for missing runner description', () => {
|
||||
createComponent({
|
||||
description: null,
|
||||
runner: { description: null },
|
||||
});
|
||||
|
||||
expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary');
|
||||
|
|
@ -109,7 +115,7 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Displays last contact', () => {
|
||||
createComponent({
|
||||
contactedAt: '2022-01-02',
|
||||
runner: { contactedAt: '2022-01-02' },
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02');
|
||||
|
|
@ -124,20 +130,46 @@ describe('RunnerTypeCell', () => {
|
|||
expect(findRunnerSummaryField('clock').text()).toContain(__('Never'));
|
||||
});
|
||||
|
||||
it('Displays ip address', () => {
|
||||
createComponent({
|
||||
ipAddress: '127.0.0.1',
|
||||
describe('IP address', () => {
|
||||
it('with no managers', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
managers: { count: 0, nodes: [] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('disk')).toBeUndefined();
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1');
|
||||
});
|
||||
it('with no ip', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
managers: { count: 1, nodes: [{ ipAddress: null }] },
|
||||
},
|
||||
});
|
||||
|
||||
it('Displays no ip address', () => {
|
||||
createComponent({
|
||||
ipAddress: null,
|
||||
expect(findRunnerSummaryField('disk')).toBeUndefined();
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('disk')).toBeUndefined();
|
||||
it.each`
|
||||
count | ipAddress | expected
|
||||
${1} | ${'127.0.0.1'} | ${'127.0.0.1'}
|
||||
${2} | ${'127.0.0.2'} | ${'127.0.0.2 (+1)'}
|
||||
${11} | ${'127.0.0.3'} | ${'127.0.0.3 (+10)'}
|
||||
${1001} | ${'127.0.0.4'} | ${'127.0.0.4 (+1,000)'}
|
||||
`(
|
||||
'with $count managers, ip $ipAddress displays $expected',
|
||||
({ count, ipAddress, expected }) => {
|
||||
createComponent({
|
||||
runner: {
|
||||
// `first: 1` is requested, `count` varies when there are more managers
|
||||
managers: { count, nodes: [{ ipAddress }] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('disk').text()).toMatchInterpolatedText(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('Displays job count', () => {
|
||||
|
|
@ -146,7 +178,7 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Formats large job counts', () => {
|
||||
createComponent({
|
||||
jobCount: 1000,
|
||||
runner: { jobCount: 1000 },
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000');
|
||||
|
|
@ -154,7 +186,7 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Formats large job counts with a plus symbol', () => {
|
||||
createComponent({
|
||||
jobCount: 1001,
|
||||
runner: { jobCount: 1001 },
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
|
||||
|
|
@ -165,7 +197,7 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Displays created at ...', () => {
|
||||
createComponent({
|
||||
createdBy: null,
|
||||
runner: { createdBy: null },
|
||||
});
|
||||
|
||||
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
|
||||
|
|
@ -177,12 +209,15 @@ describe('RunnerTypeCell', () => {
|
|||
});
|
||||
|
||||
it('Displays created at ... by ...', () => {
|
||||
createComponent({ mountFn: mountExtended });
|
||||
|
||||
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
|
||||
sprintf(I18N_CREATED_AT_BY_LABEL, {
|
||||
timeAgo: findCreatedTime().text(),
|
||||
avatar: mockRunner.createdBy.username,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
|
||||
});
|
||||
|
||||
|
|
@ -200,7 +235,7 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
it('Displays tag list', () => {
|
||||
createComponent({
|
||||
tagList: ['shell', 'linux'],
|
||||
runner: { tagList: ['shell', 'linux'] },
|
||||
});
|
||||
|
||||
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
|
||||
|
|
@ -209,14 +244,11 @@ describe('RunnerTypeCell', () => {
|
|||
it('Displays a custom runner-name slot', () => {
|
||||
const slotContent = 'My custom runner name';
|
||||
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
slots: {
|
||||
'runner-name': slotContent,
|
||||
},
|
||||
createComponent({
|
||||
slots: {
|
||||
'runner-name': slotContent,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain(slotContent);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
|
|||
it 'is not a valid file' do
|
||||
expect(valid?).to be_falsy
|
||||
expect(file.error_message)
|
||||
.to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: content does not have a valid YAML syntax')
|
||||
.to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: Invalid configuration format')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -128,31 +128,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
|
|||
end
|
||||
end
|
||||
|
||||
context 'when interpolation is disabled but there is a spec header' do
|
||||
before do
|
||||
stub_feature_flags(ci_includable_files_interpolation: false)
|
||||
end
|
||||
|
||||
let(:location) { 'some-location.yml' }
|
||||
|
||||
let(:content) do
|
||||
<<~YAML
|
||||
spec:
|
||||
include:
|
||||
website:
|
||||
---
|
||||
run:
|
||||
script: deploy $[[ inputs.website ]]
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'returns an error saying that interpolation is disabled' do
|
||||
expect(valid?).to be_falsy
|
||||
expect(file.errors)
|
||||
.to include('`some-location.yml`: can not evaluate included file because interpolation is disabled')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation was unsuccessful' do
|
||||
let(:location) { 'some-location.yml' }
|
||||
|
||||
|
|
@ -275,4 +250,17 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
|
|||
it { is_expected.to eq([{ location: location, content: content }, nil, 'HEAD'].hash) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load_and_validate_expanded_hash!' do
|
||||
let(:location) { 'some/file/config.yml' }
|
||||
let(:logger) { instance_double(::Gitlab::Ci::Pipeline::Logger, :instrument) }
|
||||
let(:context_params) { { sha: 'HEAD', variables: variables, project: project, logger: logger } }
|
||||
|
||||
it 'includes instrumentation for loading and expanding the content' do
|
||||
expect(logger).to receive(:instrument).once.ordered.with(:config_file_fetch_content_hash).and_yield
|
||||
expect(logger).to receive(:instrument).once.ordered.with(:config_file_expand_content_includes).and_yield
|
||||
|
||||
file.load_and_validate_expanded_hash!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category:
|
|||
|
||||
it 'is invalid' do
|
||||
expect(subject).to be_falsy
|
||||
expect(external_resource.error_message).to match(/does not have a valid YAML syntax/)
|
||||
expect(external_resource.error_message).to match(/Invalid configuration format/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
|
|||
it 'raises an error' do
|
||||
expect { processor.perform }.to raise_error(
|
||||
described_class::IncludeError,
|
||||
'`lib/gitlab/ci/templates/template.yml`: content does not have a valid YAML syntax'
|
||||
'`lib/gitlab/ci/templates/template.yml`: Invalid configuration format'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeline_composition do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let(:ctx) { instance_double(Gitlab::Ci::Config::External::Context, project: project, user: build(:user, id: 1234)) }
|
||||
let(:current_user) { build(:user, id: 1234) }
|
||||
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) }
|
||||
|
||||
subject { described_class.new(result, arguments, ctx) }
|
||||
subject { described_class.new(result, arguments, current_user: current_user) }
|
||||
|
||||
context 'when input data is valid' do
|
||||
let(:header) do
|
||||
|
|
@ -39,7 +39,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
|||
end
|
||||
|
||||
context 'when config has a syntax error' do
|
||||
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new) }
|
||||
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') }
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
|
|
@ -50,7 +50,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
|||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.error_message).to eq subject.errors.first
|
||||
expect(subject.errors).to include 'content does not have a valid YAML syntax'
|
||||
expect(subject.errors).to include 'Invalid configuration format'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -142,28 +142,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
|||
end
|
||||
|
||||
describe '#to_hash' do
|
||||
context 'when interpolation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_includable_files_interpolation: false)
|
||||
end
|
||||
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.website ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) { {} }
|
||||
|
||||
it 'returns an empty hash' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject.to_hash).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation is not used' do
|
||||
let(:result) do
|
||||
::Gitlab::Ci::Config::Yaml::Result.new(config: content)
|
||||
|
|
@ -202,118 +180,4 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ready?' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.website ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'returns false if interpolation has not been done yet' do
|
||||
expect(subject).not_to be_ready
|
||||
end
|
||||
|
||||
it 'returns true if interpolation has been performed' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).to be_ready
|
||||
end
|
||||
|
||||
context 'when interpolation can not be performed' do
|
||||
let(:result) do
|
||||
::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new)
|
||||
end
|
||||
|
||||
it 'returns true if interpolator has preliminary errors' do
|
||||
expect(subject).to be_ready
|
||||
end
|
||||
|
||||
it 'returns true if interpolation has been attempted' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).to be_ready
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#interpolate?' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
context 'when interpolation can be performed' do
|
||||
it 'will perform interpolation' do
|
||||
expect(subject.interpolate?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_includable_files_interpolation: false)
|
||||
end
|
||||
|
||||
it 'will not perform interpolation' do
|
||||
expect(subject.interpolate?).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an interpolation header is missing' do
|
||||
let(:header) { nil }
|
||||
|
||||
it 'will not perform interpolation' do
|
||||
expect(subject.interpolate?).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolator has preliminary errors' do
|
||||
let(:result) do
|
||||
::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new)
|
||||
end
|
||||
|
||||
it 'will not perform interpolation' do
|
||||
expect(subject.interpolate?).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_header?' do
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
context 'when header is an empty hash' do
|
||||
let(:header) { {} }
|
||||
|
||||
it 'does not have a header available' do
|
||||
expect(subject).not_to have_header
|
||||
end
|
||||
end
|
||||
|
||||
context 'when header is not specified' do
|
||||
let(:header) { nil }
|
||||
|
||||
it 'does not have a header available' do
|
||||
expect(subject).not_to have_header
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,151 +2,58 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_composition do
|
||||
describe '#to_result' do
|
||||
RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_composition do
|
||||
describe '#load' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
subject(:result) { described_class.new(yaml, project: project).to_result }
|
||||
let(:inputs) { { test_input: 'hello test' } }
|
||||
|
||||
context 'when syntax is invalid' do
|
||||
let(:yaml) { 'some: invalid: syntax' }
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
spec:
|
||||
inputs:
|
||||
test_input:
|
||||
---
|
||||
test_job:
|
||||
script:
|
||||
- echo "$[[ inputs.test_input ]]"
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'returns an invalid result object' do
|
||||
subject(:result) { described_class.new(yaml, inputs: inputs, current_user: project.creator).load }
|
||||
|
||||
it 'loads and interpolates CI config YAML' do
|
||||
expected_config = { test_job: { script: ['echo "hello test"'] } }
|
||||
|
||||
expect(result).to be_valid
|
||||
expect(result.content).to eq(expected_config)
|
||||
end
|
||||
|
||||
it 'allows the use of YAML reference tags' do
|
||||
expect(Psych).to receive(:add_tag).once.with(
|
||||
::Gitlab::Ci::Config::Yaml::Tags::Reference.tag,
|
||||
::Gitlab::Ci::Config::Yaml::Tags::Reference
|
||||
)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
context 'when there is an error loading the YAML' do
|
||||
let(:yaml) { 'invalid...yaml' }
|
||||
|
||||
it 'returns an error result' do
|
||||
expect(result).not_to be_valid
|
||||
expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError
|
||||
expect(result.error).to eq('Invalid configuration format')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the first document is a header' do
|
||||
context 'with explicit document start marker' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
spec:
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
context 'when there is an error interpolating the YAML' do
|
||||
let(:inputs) { {} }
|
||||
|
||||
it 'considers the first document as header and the second as content' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result.header).to eq({ spec: nil })
|
||||
expect(result.content).to eq({ b: 2 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first document is empty' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as header and the second as content' do
|
||||
expect(result).not_to have_header
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first document is an empty hash' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
{}
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'returns second document as a content' do
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ b: 2 })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first an array' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
- a
|
||||
- b
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as header and the second as content' do
|
||||
expect(result).not_to have_header
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the first document is not a header' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
a: 1
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content for backwards compatibility' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
|
||||
context 'with explicit document start marker' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
a: 1
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content for backwards compatibility' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the first document is not a header and second document is empty' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
a: 1
|
||||
---
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
|
||||
context 'with explicit document start marker' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
a: 1
|
||||
---
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
it 'returns an error result' do
|
||||
expect(result).not_to be_valid
|
||||
expect(result.error).to eq('`test_input` input: required value has not been provided')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
image: 'image:1.0'
|
||||
texts:
|
||||
nested_key: 'value1'
|
||||
more_text:
|
||||
more_nested_key: 'value2'
|
||||
YAML
|
||||
end
|
||||
|
||||
describe '.load!' do
|
||||
subject(:config) { described_class.load!(yaml) }
|
||||
|
||||
it 'loads a YAML file' do
|
||||
yaml = <<~YAML
|
||||
image: 'image:1.0'
|
||||
texts:
|
||||
nested_key: 'value1'
|
||||
more_text:
|
||||
more_nested_key: 'value2'
|
||||
YAML
|
||||
|
||||
config = described_class.load!(yaml)
|
||||
|
||||
expect(config).to eq({
|
||||
image: 'image:1.0',
|
||||
texts: {
|
||||
|
|
@ -30,156 +32,20 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition
|
|||
let(:yaml) { 'some: invalid: syntax' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { described_class.load!(yaml) }
|
||||
expect { config }
|
||||
.to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.load_result!' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
context 'when given a user' do
|
||||
let(:user) { instance_double(User) }
|
||||
|
||||
subject(:result) { described_class.load_result!(yaml, project: project) }
|
||||
subject(:config) { described_class.load!(yaml, current_user: user) }
|
||||
|
||||
context 'when syntax is invalid' do
|
||||
let(:yaml) { 'some: invalid: syntax' }
|
||||
it 'passes it to Loader' do
|
||||
expect(::Gitlab::Ci::Config::Yaml::Loader).to receive(:new).with(yaml, current_user: user).and_call_original
|
||||
|
||||
it 'returns an invalid result object' do
|
||||
expect(result).not_to be_valid
|
||||
expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the first document is a header' do
|
||||
context 'with explicit document start marker' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
spec:
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as header and the second as content' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result.header).to eq({ spec: nil })
|
||||
expect(result.content).to eq({ b: 2 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first document is empty' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as header and the second as content' do
|
||||
expect(result).not_to have_header
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first document is an empty hash' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
{}
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'returns second document as a content' do
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ b: 2 })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first an array' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
- a
|
||||
- b
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as header and the second as content' do
|
||||
expect(result).not_to have_header
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the first document is not a header' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
a: 1
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content for backwards compatibility' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
|
||||
context 'with explicit document start marker' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
a: 1
|
||||
---
|
||||
b: 2
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content for backwards compatibility' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the first document is not a header and second document is empty' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
a: 1
|
||||
---
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
|
||||
context 'with explicit document start marker' do
|
||||
let(:yaml) do
|
||||
<<~YAML
|
||||
---
|
||||
a: 1
|
||||
---
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'considers the first document as content' do
|
||||
expect(result).to be_valid
|
||||
expect(result.error).to be_nil
|
||||
expect(result).not_to have_header
|
||||
expect(result.content).to eq({ a: 1 })
|
||||
end
|
||||
config
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -364,32 +364,4 @@ RSpec.describe PlanLimits do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#limit_attribute_changes', :freeze_time do
|
||||
let(:user) { create(:user) }
|
||||
let(:current_timestamp) { Time.current.utc.to_i }
|
||||
let(:plan_limits) do
|
||||
create(:plan_limits,
|
||||
limits_history: { 'enforcement_limit' => [
|
||||
{ user_id: user.id, username: user.username, timestamp: current_timestamp,
|
||||
value: 20_000 }, { user_id: user.id, username: user.username, timestamp: current_timestamp,
|
||||
value: 50_000 }
|
||||
] })
|
||||
end
|
||||
|
||||
it 'returns an empty array for attribute with no changes' do
|
||||
changes = plan_limits.limit_attribute_changes(:notification_limit)
|
||||
|
||||
expect(changes).to eq([])
|
||||
end
|
||||
|
||||
it 'returns the changes for a specific attribute' do
|
||||
changes = plan_limits.limit_attribute_changes(:enforcement_limit)
|
||||
|
||||
expect(changes).to eq(
|
||||
[{ timestamp: current_timestamp, value: 20_000, username: user.username, user_id: user.id },
|
||||
{ timestamp: current_timestamp, value: 50_000, username: user.username, user_id: user.id }]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6576,7 +6576,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
|
||||
it 'does not allow access to branches for which the merge request was closed' do
|
||||
create(
|
||||
:merge_request, :closed,
|
||||
:merge_request,
|
||||
:closed,
|
||||
target_project: target_project,
|
||||
target_branch: 'target-branch',
|
||||
source_project: project,
|
||||
|
|
@ -9081,7 +9082,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
|
||||
def create_build(new_pipeline = pipeline, name = 'test')
|
||||
create(
|
||||
:ci_build, :success, :artifacts,
|
||||
:ci_build,
|
||||
:success,
|
||||
:artifacts,
|
||||
pipeline: new_pipeline,
|
||||
status: new_pipeline.status,
|
||||
name: name
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'rubocop_spec_helper'
|
||||
require_relative '../../rubocop/cop_todo'
|
||||
|
||||
RSpec.describe RuboCop::CopTodo do
|
||||
RSpec.describe RuboCop::CopTodo, feature_category: :tooling do
|
||||
let(:cop_name) { 'Cop/Rule' }
|
||||
|
||||
subject(:cop_todo) { described_class.new(cop_name) }
|
||||
|
|
@ -32,6 +32,19 @@ RSpec.describe RuboCop::CopTodo do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#add_files' do
|
||||
it 'adds files' do
|
||||
cop_todo.add_files(%w[a.rb b.rb])
|
||||
cop_todo.add_files(%w[a.rb])
|
||||
cop_todo.add_files(%w[])
|
||||
|
||||
expect(cop_todo).to have_attributes(
|
||||
files: contain_exactly('a.rb', 'b.rb'),
|
||||
offense_count: 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#autocorrectable?' do
|
||||
subject { cop_todo.autocorrectable? }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
|
@ -10,7 +11,7 @@ require 'tmpdir'
|
|||
require_relative '../../../rubocop/formatter/todo_formatter'
|
||||
require_relative '../../../rubocop/todo_dir'
|
||||
|
||||
RSpec.describe RuboCop::Formatter::TodoFormatter do
|
||||
RSpec.describe RuboCop::Formatter::TodoFormatter, feature_category: :tooling do
|
||||
let(:stdout) { StringIO.new }
|
||||
let(:tmp_dir) { Dir.mktmpdir }
|
||||
let(:real_tmp_dir) { File.join(tmp_dir, 'real') }
|
||||
|
|
@ -97,6 +98,36 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
|
|||
YAML
|
||||
end
|
||||
|
||||
context 'with existing HAML exclusions' do
|
||||
before do
|
||||
todo_dir.write('B/TooManyOffenses', <<~YAML)
|
||||
---
|
||||
B/TooManyOffenses:
|
||||
Exclude:
|
||||
- 'd.rb'
|
||||
- 'app/views/project.html.haml.rb'
|
||||
- 'app/views/unrelated.html.haml.rb.ext'
|
||||
- 'app/views/unrelated.html.haml.ext'
|
||||
- 'app/views/unrelated.html.haml'
|
||||
YAML
|
||||
|
||||
todo_dir.inspect_all
|
||||
end
|
||||
|
||||
it 'does not remove them' do
|
||||
run_formatter
|
||||
|
||||
expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
|
||||
---
|
||||
B/TooManyOffenses:
|
||||
Exclude:
|
||||
- 'a.rb'
|
||||
- 'app/views/project.html.haml.rb'
|
||||
- 'c.rb'
|
||||
YAML
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cop previously not explicitly disabled' do
|
||||
before do
|
||||
todo_dir.write('B/TooManyOffenses', <<~YAML)
|
||||
|
|
@ -105,6 +136,8 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
|
|||
Exclude:
|
||||
- 'x.rb'
|
||||
YAML
|
||||
|
||||
todo_dir.inspect_all
|
||||
end
|
||||
|
||||
it 'does not disable cop' do
|
||||
|
|
@ -158,6 +191,8 @@ RSpec.describe RuboCop::Formatter::TodoFormatter do
|
|||
Exclude:
|
||||
- 'x.rb'
|
||||
YAML
|
||||
|
||||
todo_dir.inspect_all
|
||||
end
|
||||
|
||||
it 'keeps cop disabled' do
|
||||
|
|
|
|||
|
|
@ -2035,7 +2035,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
|
||||
expect(pipeline).to be_persisted
|
||||
expect(pipeline.yaml_errors)
|
||||
.to include 'content does not have a valid YAML syntax'
|
||||
.to include 'mapping values are not allowed'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2172,7 +2172,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
|
|||
|
||||
expect(pipeline).to be_persisted
|
||||
expect(pipeline.yaml_errors)
|
||||
.to include 'content does not have a valid YAML syntax'
|
||||
.to include 'mapping values are not allowed'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue