From 7a15fb07cf363079c4c4683850ee131d80e75f75 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 15 Sep 2023 18:11:45 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .gitlab/CODEOWNERS | 1 - GITALY_SERVER_VERSION | 2 +- .../branches/components/new_branch_form.vue | 1 + .../branches/components/source_branch_dropdown.vue | 54 ++++++-- .../javascripts/jira_connect/branches/index.js | 1 + .../javascripts/search/sidebar/components/app.vue | 13 ++ .../sidebar/components/archived_filter/data.js | 1 + .../search/sidebar/components/notes_filters.vue | 18 +++ .../javascripts/search/sidebar/constants/index.js | 1 + .../security_configuration/components/constants.js | 2 + .../components/continuous_vulnerability_scan.vue | 127 ++++++++++++++++++ .../components/feature_card.vue | 7 + ...t_set_continuous_vulnerability_scanning.graphql | 8 ++ .../javascripts/security_configuration/index.js | 2 + .../super_sidebar/components/nav_item.vue | 2 +- app/models/ci/build.rb | 8 +- app/models/concerns/chronic_duration_attribute.rb | 4 +- app/models/container_expiration_policy.rb | 4 +- .../cleanup_tags_base_service.rb | 4 +- app/validators/duration_validator.rb | 2 +- app/views/dashboard/todos/_todo.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 2 +- .../member_expiring_email_notification.yml | 2 +- .../development/update_chronic_duration.yml | 8 -- doc/.vale/gitlab/Substitutions.yml | 1 + doc/api/dependency_list_export.md | 15 ++- doc/api/graphql/reference/index.md | 3 + doc/ci/environments/kubernetes_dashboard.md | 3 - doc/ci/migration/plan_a_migration.md | 8 +- doc/ci/yaml/index.md | 21 +-- doc/development/migration_style_guide.md | 42 ++++++ doc/operations/error_tracking.md | 2 + doc/operations/tracing.md | 8 +- doc/user/packages/composer_repository/index.md | 50 +++++-- .../profile/account/two_factor_authentication.md | 26 ++-- doc/user/project/repository/mirror/index.md | 3 +- doc/user/project/repository/mirror/pull.md | 5 +- doc/user/project/repository/mirror/push.md | 23 ++-- doc/user/usage_quotas.md | 23 ++-- lib/gitlab/ci/build/duration_parser.rb | 2 +- lib/gitlab/ci/config/entry/job.rb | 2 +- .../config/entry/legacy_validation_helpers.rb | 6 +- lib/gitlab/time_tracking_formatter.rb | 2 +- locale/gitlab.pot | 12 ++ qa/qa/page/dashboard/todos.rb | 14 +- .../merge_train_spec_with_user_prep.rb | 80 ----------- .../components/source_branch_dropdown_spec.js | 146 +++++++++++++++------ spec/frontend/jira_connect/branches/mock_data.js | 15 +++ .../frontend/search/sidebar/components/app_spec.js | 74 +++++++---- .../sidebar/components/notes_filters_spec.js | 28 ++++ .../sidebar/components/projects_filters_spec.js | 28 ++++ .../sidebar/components/projects_filters_specs.js | 28 ---- .../continuous_vulnerability_scan_spec.js | 124 +++++++++++++++++ .../components/feature_card_spec.js | 18 +++ spec/lib/gitlab/ci/config/entry/job_spec.rb | 12 -- spec/lib/gitlab/time_tracking_formatter_spec.rb | 46 ------- spec/models/ci/build_spec.rb | 36 ----- .../concerns/chronic_duration_attribute_spec.rb | 30 ----- .../cleanup_tags_service_shared_examples.rb | 11 -- 59 files changed, 783 insertions(+), 440 deletions(-) create mode 100644 app/assets/javascripts/search/sidebar/components/notes_filters.vue create mode 100644 app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue create mode 100644 app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql delete mode 100644 config/feature_flags/development/update_chronic_duration.yml delete mode 100644 qa/qa/specs/features/shared_contexts/merge_train_spec_with_user_prep.rb create mode 100644 spec/frontend/search/sidebar/components/notes_filters_spec.js create mode 100644 spec/frontend/search/sidebar/components/projects_filters_spec.js delete mode 100644 spec/frontend/search/sidebar/components/projects_filters_specs.js create mode 100644 spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 5d6d218ef81..9dab471bb7c 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1388,7 +1388,6 @@ lib/gitlab/checks/** /lib/tasks/gitlab/seed/runner_fleet.rake @gitlab-org/ci-cd/runner-fleet-team/backend-approvers # CI/CD templates require approval from specific owners. /lib/gitlab/ci/templates/ @gitlab-org/maintainers/cicd-templates -/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah /lib/gitlab/ci/templates/Security/ @gonzoyumo @twoodham @amarpatel /lib/gitlab/ci/templates/Security/API-Fuzzing.*.yml @gitlab-org/secure/dynamic-analysis /lib/gitlab/ci/templates/Security/Container-Scanning.*.yml @gitlab-org/secure/composition-analysis-be diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index c4ed752e07b..28370ccb017 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -16cd7e7fa2bb9afdd7061ae6187f739dc2fe120f +3958129f4b15a62ed6a11b8dbb7f8ba5134963db diff --git a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue index 46c27c33f56..1d926c0d0c5 100644 --- a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue +++ b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue @@ -193,6 +193,7 @@ export default { > 0 && this.sourceBranchNames.length % BRANCHES_PER_PAGE === 0 + ); + }, branchDropdownText() { return this.selectedBranchName || __('Select a branch'); }, @@ -59,45 +67,63 @@ export default { onSearch: debounce(function debouncedSearch(branchSearchQuery) { this.onSourceBranchSearchQuery(branchSearchQuery); }, 250), - onSourceBranchSearchQuery(branchSearchQuery) { + async onSourceBranchSearchQuery(branchSearchQuery) { this.branchSearchQuery = branchSearchQuery; - this.fetchSourceBranchNames({ + this.sourceBranchNamesLoading = true; + + await this.fetchSourceBranchNames({ + projectPath: this.selectedProject.fullPath, + searchPattern: this.branchSearchQuery, + }); + this.sourceBranchNamesLoading = false; + }, + async onBottomReached() { + this.sourceBranchNamesLoadingMore = true; + + await this.fetchSourceBranchNames({ projectPath: this.selectedProject.fullPath, searchPattern: this.branchSearchQuery, + append: true, }); + + this.sourceBranchNamesLoadingMore = false; }, onError({ message } = {}) { this.$emit('error', { message }); }, - async fetchSourceBranchNames({ projectPath, searchPattern } = {}) { - this.sourceBranchNamesLoading = true; + async fetchSourceBranchNames({ projectPath, searchPattern, append = false } = {}) { try { const { data } = await this.$apollo.query({ query: getProjectQuery, variables: { projectPath, branchNamesLimit: this.$options.BRANCHES_PER_PAGE, - branchNamesOffset: 0, + branchNamesOffset: append ? this.sourceBranchNames.length : 0, branchNamesSearchPattern: searchPattern ? `*${searchPattern}*` : '*', }, }); const { branchNames, rootRef } = data?.project.repository || {}; - this.sourceBranchNames = - branchNames.map((value) => { + const branchNameItems = + branchNames?.map((value) => { return { text: value, value }; }) || []; - // Use root ref as the default selection - if (rootRef && !this.hasSelectedSourceBranch) { - this.onSourceBranchSelect(rootRef); + if (append) { + this.sourceBranchNames.push(...branchNameItems); + } else { + this.sourceBranchNames = branchNameItems; + + // Use root ref as the default selection + if (rootRef && !this.hasSelectedSourceBranch) { + this.onSourceBranchSelect(rootRef); + } } } catch (err) { + logError(err); this.onError({ message: __('Something went wrong while fetching source branches.'), }); - } finally { - this.sourceBranchNamesLoading = false; } }, }, @@ -107,6 +133,7 @@ export default { diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js index 831e253a0b6..7a3dc8ad96b 100644 --- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js +++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js @@ -9,6 +9,7 @@ const scopes = { PROJECTS: 'projects', ISSUES: 'issues', MERGE_REQUESTS: 'merge_requests', + NOTES: 'notes', }; const filterParam = 'include_archived'; diff --git a/app/assets/javascripts/search/sidebar/components/notes_filters.vue b/app/assets/javascripts/search/sidebar/components/notes_filters.vue new file mode 100644 index 00000000000..3a9f582d554 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/notes_filters.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js index e5b803b92cb..e6808082185 100644 --- a/app/assets/javascripts/search/sidebar/constants/index.js +++ b/app/assets/javascripts/search/sidebar/constants/index.js @@ -2,6 +2,7 @@ export const SCOPE_ISSUES = 'issues'; export const SCOPE_MERGE_REQUESTS = 'merge_requests'; export const SCOPE_BLOB = 'blobs'; export const SCOPE_PROJECTS = 'projects'; +export const SCOPE_NOTES = 'notes'; export const LABEL_DEFAULT_CLASSES = [ 'gl-display-flex', 'gl-flex-direction-row', diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index fd713a7a504..da213b0ed43 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -1,5 +1,6 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import { __, s__ } from '~/locale'; +import ContinuousVulnerabilityScan from '~/security_configuration/components/continuous_vulnerability_scan.vue'; import { REPORT_TYPE_SAST, @@ -210,6 +211,7 @@ export const securityFeatures = [ configurationHelpPath: DEPENDENCY_SCANNING_CONFIG_HELP_PATH, type: REPORT_TYPE_DEPENDENCY_SCANNING, anchor: 'dependency-scanning', + slotComponent: ContinuousVulnerabilityScan, }, { name: CONTAINER_SCANNING_NAME, diff --git a/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue b/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue new file mode 100644 index 00000000000..61cbde2107c --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue @@ -0,0 +1,127 @@ + + + diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue index a757657339b..7f0a049a6ad 100644 --- a/app/assets/javascripts/security_configuration/components/feature_card.vue +++ b/app/assets/javascripts/security_configuration/components/feature_card.vue @@ -73,6 +73,9 @@ export default { hasSecondary() { return Boolean(this.feature.secondary); }, + hasSlotComponent() { + return Boolean(this.feature.slotComponent); + }, // This condition is a temporary hack to not display any wrong information // until this BE Bug is fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/350307. // More Information: https://gitlab.com/gitlab-org/gitlab/-/issues/350307#note_825447417 @@ -215,5 +218,9 @@ export default { {{ $options.i18n.configurationGuide }} + +
+ +
diff --git a/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql b/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql new file mode 100644 index 00000000000..79f4316d106 --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql @@ -0,0 +1,8 @@ +mutation ProjectSetContinuousVulnerabilityScanning( + $input: ProjectSetContinuousVulnerabilityScanningInput! +) { + projectSetContinuousVulnerabilityScanning(input: $input) { + continuousVulnerabilityScanningEnabled + errors + } +} diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js index aa3c9c87622..4b498091134 100644 --- a/app/assets/javascripts/security_configuration/index.js +++ b/app/assets/javascripts/security_configuration/index.js @@ -26,6 +26,7 @@ export const initSecurityConfiguration = (el) => { autoDevopsHelpPagePath, autoDevopsPath, vulnerabilityTrainingDocsPath, + continuousVulnerabilityScansEnabled, } = el.dataset; const { augmentedSecurityFeatures } = augmentFeatures( @@ -43,6 +44,7 @@ export const initSecurityConfiguration = (el) => { autoDevopsHelpPagePath, autoDevopsPath, vulnerabilityTrainingDocsPath, + continuousVulnerabilityScansEnabled, }, render(createElement) { return createElement(SecurityConfigurationApp, { diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index 8320da30ba8..7f5cce8341f 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -217,7 +217,7 @@ export default { - + dom_id(todo) + "_describer" } + %span.todo-target-title{ :id => dom_id(todo) + "_describer" } = todo_target_title(todo) - if !todo.for_design? && !todo.member_access_requested? diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 4a7aa9a86ab..066b2ef98dd 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -13,7 +13,7 @@ .merge-request-title.title %span.merge-request-title-text.js-onboarding-mr-item = hidden_merge_request_icon(merge_request) - = link_to merge_request.title, merge_request_path(merge_request), class: 'js-prefetch-document' + = link_to markdown_field(merge_request, :title), merge_request_path(merge_request), class: 'js-prefetch-document' - if merge_request.tasks? %span.task-status.d-none.d-sm-inline-block.gl-font-sm   diff --git a/config/feature_flags/development/member_expiring_email_notification.yml b/config/feature_flags/development/member_expiring_email_notification.yml index 1775cc67b52..36a15c27daf 100644 --- a/config/feature_flags/development/member_expiring_email_notification.yml +++ b/config/feature_flags/development/member_expiring_email_notification.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/416581 milestone: '16.3' type: development group: group::authentication and authorization -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/update_chronic_duration.yml b/config/feature_flags/development/update_chronic_duration.yml deleted file mode 100644 index 4af65299619..00000000000 --- a/config/feature_flags/development/update_chronic_duration.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: update_chronic_duration -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130531 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423696 -milestone: '16.4' -type: development -group: group::pipeline authoring -default_enabled: false diff --git a/doc/.vale/gitlab/Substitutions.yml b/doc/.vale/gitlab/Substitutions.yml index 10369669ca5..0d49ac583dd 100644 --- a/doc/.vale/gitlab/Substitutions.yml +++ b/doc/.vale/gitlab/Substitutions.yml @@ -14,6 +14,7 @@ swap: codequality: code quality Customer [Pp]ortal: Customers Portal disallow: prevent + '(? [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/333463) in GitLab 16.4. - Every call to this endpoint requires authentication. -## Create a pipeline-level dependency list export +## Create a pipeline-level dependency list export **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/333463) in GitLab 16.4 [with a flag](../administration/feature_flags.md) named `merge_sbom_api`. Enabled by default. This feature is an [Experiment](../policy/experiment-beta-support.md#experiment). + +FLAG: +On self-managed GitLab, by default this feature is available. +To hide the feature, an administrator can [disable the feature flag](../administration/feature_flags.md) named `merge_sbom_api`. +On GitLab.com, this feature is available. + +WARNING: +This feature is an [Experiment](../policy/experiment-beta-support.md#experiment) +and subject to change without notice. Create a new CycloneDX JSON export for all the project dependencies detected in a pipeline. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 163c6174c9d..d9d69f6d2f5 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -867,6 +867,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | `dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. Only dismissed Vulnerabilities will be included with the filter. | | `hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. | +| `hasMergeRequest` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked merge requests. | | `hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. | | `image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. | | `projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | @@ -18209,6 +18210,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | `dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. Only dismissed Vulnerabilities will be included with the filter. | | `hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. | +| `hasMergeRequest` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked merge requests. | | `hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. | | `image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. | | `projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | @@ -23161,6 +23163,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | `dismissalReason` | [`[VulnerabilityDismissalReason!]`](#vulnerabilitydismissalreason) | Filter by dismissal reason. Only dismissed Vulnerabilities will be included with the filter. | | `hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. | +| `hasMergeRequest` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked merge requests. | | `hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. | | `image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. | | `projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | diff --git a/doc/ci/environments/kubernetes_dashboard.md b/doc/ci/environments/kubernetes_dashboard.md index f8b127cd011..0f9e1d808ec 100644 --- a/doc/ci/environments/kubernetes_dashboard.md +++ b/doc/ci/environments/kubernetes_dashboard.md @@ -15,9 +15,6 @@ Use the Dashboard for Kubernetes to understand the status of your clusters with The dashboard works with every connected Kubernetes cluster, whether you deployed them with CI/CD or GitOps. -For Flux users, the synchronization status of a given environment is not displayed in the dashboard. -[Issue 391581](https://gitlab.com/gitlab-org/gitlab/-/issues/391581) proposes to add this functionality. - ## Configure a dashboard > - Filtering resources by namespace [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/403618) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `kubernetes_namespace_for_environment`. Disabled by default. diff --git a/doc/ci/migration/plan_a_migration.md b/doc/ci/migration/plan_a_migration.md index 84d11f53eed..488b2abf3a2 100644 --- a/doc/ci/migration/plan_a_migration.md +++ b/doc/ci/migration/plan_a_migration.md @@ -23,7 +23,7 @@ the behavior of your old tool. ## Manage organizational changes -An important part of transitioning from Jenkins to GitLab is the cultural and organizational +An important part of transitioning to GitLab CI/CD is the cultural and organizational changes that come with the move, and successfully managing them. A few things that organizations have reported as helping: @@ -37,8 +37,8 @@ A few things that organizations have reported as helping: - Finding ways to sequence or delay parts of the migration can help a lot. Importantly though, try not to leave things in a non-migrated (or partially-migrated) state for too long. -- To gain all the benefits of GitLab, moving your existing Jenkins setup over - as-is, including any current problems, isn't enough. Take advantage of the improvements +- To gain all the benefits of GitLab, moving your existing configuration over as-is, + including any current problems, isn't enough. Take advantage of the improvements that GitLab CI/CD offers, and update your implementation as part of the transition. ## Technical questions to ask before starting a migration @@ -57,7 +57,7 @@ the migration requirements: ### Jenkins -If you are migrating from Jenkins, these additional questions can help with planning +If you are [migrating from Jenkins](jenkins.md), these additional questions can help with planning the migration: - What plugins are used by jobs in Jenkins today? diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 956544ceb82..6275d4293ea 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -79,8 +79,9 @@ or import additional pipeline configuration. > Support for `id_tokens` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/419750) in GitLab 16.4. -You can set global defaults for some keywords. Jobs that do not define one or more -of the listed keywords use the value defined in the `default` section. +You can set global defaults for some keywords. Each default keyword is copied to every job +that doesn't already have it defined. If the job already has a keyword defined, that default +is not used. **Keyword type**: Global keyword. @@ -104,6 +105,7 @@ of the listed keywords use the value defined in the `default` section. ```yaml default: image: ruby:3.0 + retry: 2 rspec: script: bundle exec rspec @@ -113,16 +115,17 @@ rspec 2.7: script: bundle exec rspec ``` -In this example, `ruby:3.0` is the default `image` value for all jobs in the pipeline. -The `rspec 2.7` job does not use the default, because it overrides the default with -a job-specific `image` section: +In this example: + +- `image: ruby:3.0` and `retry: 2` are the default keywords for all jobs in the pipeline. +- The `rspec` job does not have `image` or `retry` defined, so it uses the defaults of + `image: ruby:3.0` and `retry: 2`. +- The `rspec 2.7` job does not have `retry` defined, but it does have `image` explictly defined. + It uses the default `retry: 2`, but ignores the default `image` and uses the `image: ruby:2.7` + defined in the job. **Additional details**: -- When the pipeline is created, each default is copied to all jobs that don't have - that keyword defined. -- If a job already has one of the keywords configured, the configuration in the job - takes precedence and is not replaced by the default. - Control inheritance of default keywords in jobs with [`inherit:default`](#inheritdefault). ### `include` diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 65dc4de30d1..9be322812e3 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -1290,6 +1290,48 @@ class BuildMetadata end ``` +Additionally, you can expose the keys in a `JSONB` column as +ActiveRecord attributes. Do this when you need complex validations, +or ActiveRecord change tracking. This feature is provided by the +[`jsonb_accessor`](https://github.com/madeintandem/jsonb_accessor) gem, +and does not replace `JsonSchemaValidator`. + +```ruby +module Organizations + class OrganizationSetting < ApplicationRecord + belongs_to :organization + + validates :settings, json_schema: { filename: "organization_settings" } + + jsonb_accessor :settings, + restricted_visibility_levels: [:integer, { array: true }] + + validates_each :restricted_visibility_levels do |record, attr, value| + value&.each do |level| + unless Gitlab::VisibilityLevel.options.value?(level) + record.errors.add(attr, format(_("'%{level}' is not a valid visibility level"), level: level)) + end + end + end + end +end +``` + +You can now use `restricted_visibility_levels` as an ActiveRecord attribute: + +```ruby +> s = Organizations::OrganizationSetting.find(1) +=> # +> s.settings +=> {"restricted_visibility_levels"=>[20]} +> s.restricted_visibility_levels +=> [20] +> s.restricted_visibility_levels = [0] +=> [0] +> s.changes +=> {"settings"=>[{"restricted_visibility_levels"=>[20]}, {"restricted_visibility_levels"=>[0]}], "restricted_visibility_levels"=>[[20], [0]]} +``` + ## Encrypted attributes > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227779) in GitLab 14.0. diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md index f0000ea19db..82982a03016 100644 --- a/doc/operations/error_tracking.md +++ b/doc/operations/error_tracking.md @@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w Error Tracking allows developers to discover and view errors generated by their application. Because error information is surfaced where the code is developed, this increases efficiency and awareness. Users can choose between [GitLab Integrated error tracking](#integrated-error-tracking) and [Sentry based](#sentry-error-tracking) backends. +To leave feedback about Error Tracking bugs or functionality, please comment in the [feedback issue](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2362) or open a [new issue](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/new). + ## How error tracking works For error tracking to work, you need: diff --git a/doc/operations/tracing.md b/doc/operations/tracing.md index aca10690a7f..608a9c998f8 100644 --- a/doc/operations/tracing.md +++ b/doc/operations/tracing.md @@ -6,9 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Distributed tracing **(ULTIMATE SAAS EXPERIMENT)** -This feature is an [Experiment](../policy/experiment-beta-support.md). If you find a bug, -[open an issue in our issue tracker](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/). - > Introduced in GitLab 16.3 [with flags](../administration/feature_flags.md) named `observability_group_tab` and `observability_tracing`. Disabled by default. FLAG: @@ -16,10 +13,9 @@ On GitLab.com, by default this feature is not available. To make it available, an administrator can [enable the feature flags](../administration/feature_flags.md) named `observability_group_tab` and `observability_tracing`. The feature is not ready for production use. -With distributed tracing you can inspect how a request moves through different services and systems, -the timing of each operation, and any errors or logs as they occur. +With distributed tracing, you can troubleshoot application performance issues by inspecting how a request moves through different services and systems, the timing of each operation, and any errors or logs as they occur. Tracing is particularly useful in the context of microservice applications, which group multiple independent services collaborating to fulfill user requests. -Tracing is particularly useful in the context of microservice applications, which group multiple independent services collaborating to fulfill user requests. +This feature is an [Experiment](../policy/experiment-beta-support.md). For more information, see the [group direction page](https://about.gitlab.com/direction/analytics/observability/). To leave feedback about tracing bugs or functionality, please comment in the [feedback issue](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2363) or open a [new issue](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/new). ## Configure distributed tracing for a project diff --git a/doc/user/packages/composer_repository/index.md b/doc/user/packages/composer_repository/index.md index 40fef13e590..3c80e739465 100644 --- a/doc/user/packages/composer_repository/index.md +++ b/doc/user/packages/composer_repository/index.md @@ -280,21 +280,53 @@ To install a package: composer req : ``` - If successful, you should see output indicating that the package installed successfully. - - You can also install from source (by pulling the Git repository directly) using the - `--prefer-source` option: - - ```shell - composer update --prefer-source - ``` - WARNING: Never commit the `auth.json` file to your repository. To install packages from a CI/CD job, consider using the [`composer config`](https://getcomposer.org/doc/articles/handling-private-packages.md#satis) tool with your access token stored in a [GitLab CI/CD variable](../../../ci/variables/index.md) or in [HashiCorp Vault](../../../ci/secrets/index.md). +### Install from source + +You can install from source by pulling the Git repository directly. To do so, either: + +- Use the `--prefer-source` option: + + ```shell + composer update --prefer-source + ``` + +- In the `composer.json`, use the [`preferred-install` field under the `config` key](https://getcomposer.org/doc/06-config.md#preferred-install): + + ```json + { + ... + "config": { + "preferred-install": { + "": "source" + } + } + ... + } + ``` + +#### SSH access + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119739) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `composer_use_ssh_source_urls`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, an administrator can +[enable the feature flag](../../../administration/feature_flags.md) named `composer_use_ssh_source_urls`. + +When you install from source, the `composer` configures an +access to the project's Git repository. +Depending on the project visibility, the access type is different: + +- On public projects, the `https` Git URL is used. Make sure you can [clone the repository with HTTPS](../../../gitlab-basics/start-using-git.md#clone-with-https). +- On internal or private projects, the `ssh` Git URL is used. Make sure you can [clone the repository with SSH](../../../gitlab-basics/start-using-git.md#clone-with-ssh). + +You can access the `ssh` Git URL from a CI/CD job using [SSH keys with GitLab CI/CD](../../../ci/ssh_keys/index.md). + ### Working with Deploy Tokens Although Composer packages are accessed at the group level, a group or project deploy token can be diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index c2b02f143db..193320f48d5 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -62,6 +62,8 @@ In GitLab 14.3 and later, your account email must be confirmed to enable 2FA. To enable 2FA with a one-time password: + + 1. **In GitLab:** 1. Access your [**User settings**](../index.md#access-your-user-settings). 1. Select **Account**. @@ -70,7 +72,7 @@ To enable 2FA with a one-time password: 1. Install a compatible application. For example: - Cloud-based (recommended because you can restore access if you lose the hardware device): - [Authy](https://authy.com/). - - [Duo](https://duo.com/). + - [Cisco Duo](https://duo.com/). - Other (proprietary): - [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en). - [Microsoft Authenticator](https://www.microsoft.com/en-us/security/mobile-authenticator-app). @@ -85,6 +87,8 @@ To enable 2FA with a one-time password: 1. Enter your current password. 1. Select **Submit**. + + If you entered the correct pin, GitLab displays a list of [recovery codes](#recovery-codes). Download them and keep them in a safe place. @@ -152,27 +156,31 @@ Configure FortiAuthenticator in GitLab. On your GitLab server: (Linux package installations) or [restart](../../../administration/restart_gitlab.md#self-compiled-installations) (self-compiled installations). -### Enable one-time password using Duo + + +### Enable one-time password using Cisco Duo > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15760) in GitLab 15.10. FLAG: On self-managed GitLab, by default this feature is available. On GitLab.com this feature is not available. -You can use Duo as an OTP provider in GitLab. +You can use Cisco Duo as an OTP provider in GitLab. #### Prerequisites -To use Duo as an OTP provider: +To use Cisco Duo as an OTP provider: + +- Your account must exist in both Cisco Duo and GitLab, with the same username in both applications. +- You must have [configured Cisco Duo](https://admin.duosecurity.com/) and have an integration key, secret key, and API hostname. -- Your account must exist in both Duo and GitLab, with the same username in both applications. -- You must have [configured Duo](https://admin.duosecurity.com/) and have an integration key, secret key, and API hostname. +For more information, see the [Cisco Duo API documentation](https://duo.com/docs/authapi). -For more information, see the [Duo API documentation](https://duo.com/docs/authapi). +GitLab 15.10 has been tested with Cisco Duo version D261.14 -GitLab 15.10 has been tested with Duo version D261.14 +#### Configure Cisco Duo in GitLab -#### Configure Duo in GitLab + On your GitLab server: diff --git a/doc/user/project/repository/mirror/index.md b/doc/user/project/repository/mirror/index.md index 36b184c1ad0..b16197e45b2 100644 --- a/doc/user/project/repository/mirror/index.md +++ b/doc/user/project/repository/mirror/index.md @@ -124,9 +124,8 @@ When you create a mirror, you must configure the authentication method for it. GitLab supports these authentication methods: - [SSH authentication](#ssh-authentication). -- Password. +- Username and password. -When using password authentication, ensure you specify the username. For a [project access token](../../settings/project_access_tokens.md) or [group access token](../../../group/settings/group_access_tokens.md), use the username (not token name) and the token as the password. diff --git a/doc/user/project/repository/mirror/pull.md b/doc/user/project/repository/mirror/pull.md index cced9a52caf..5b3c76d982a 100644 --- a/doc/user/project/repository/mirror/pull.md +++ b/doc/user/project/repository/mirror/pull.md @@ -67,11 +67,10 @@ Prerequisites: 1. On the left sidebar, select **Search or go to** and find your project. 1. Select **Settings > Repository**. 1. Expand **Mirroring repositories**. -1. Enter the **Git repository URL**. Include the username - in the URL, if required: `https://MYUSERNAME@gitlab.com/GROUPNAME/PROJECTNAME.git` +1. Enter the **Git repository URL**. NOTE: - To mirror the `gitlab` repository, use `git@gitlab.com:gitlab-org/gitlab.git` + To mirror the `gitlab` repository, use `gitlab.com:gitlab-org/gitlab.git` or `https://gitlab.com/gitlab-org/gitlab.git`. 1. In **Mirror direction**, select **Pull**. diff --git a/doc/user/project/repository/mirror/push.md b/doc/user/project/repository/mirror/push.md index a6312dec162..e18e3631d7f 100644 --- a/doc/user/project/repository/mirror/push.md +++ b/doc/user/project/repository/mirror/push.md @@ -89,12 +89,12 @@ To configure a mirror from GitLab to GitHub: 1. Enter a **Git repository URL** with this format, changing the variables as needed: ```plaintext - https://USERNAME@github.com/GROUP/PROJECT.git + https://github.com/GROUP/PROJECT.git ``` - - `USERNAME`: The username of the owner of the personal access token. - `GROUP`: The group on GitHub. - `PROJECT`: The project on GitHub. +1. For **Username**, enter the username of the owner of the personal access token. 1. For **Password**, enter your GitHub personal access token. 1. Select **Mirror repository**. @@ -164,19 +164,17 @@ To set up a mirror from GitLab to AWS CodeCommit: 1. Open your new repository, and then select **Clone URL > Clone HTTPS** (not **Clone HTTPS (GRC)**). 1. In GitLab, open the repository to be push-mirrored. 1. Select **Settings > Repository**, and then expand **Mirroring repositories**. -1. Fill in the **Git repository URL** field using this format: +1. Fill in the **Git repository URL** field using this format, replacing + `` with your AWS region, and + `` with the name of your repository in CodeCommit: ```plaintext - https://@git-codecommit..amazonaws.com/v1/repos/ + https://git-codecommit..amazonaws.com/v1/repos/ ``` - Replace `` with the AWS **special HTTPS Git user ID** - from the IAM Git credentials created earlier. Replace `` - with the name of your repository in CodeCommit. - -1. For **Mirror direction**, select **Push**. -1. For **Authentication method**, select **Password**. Fill in the **Password** box - with the special IAM Git clone user ID **password** created earlier in AWS. +1. For **Authentication method**, select **Username and Password**. +1. For **Username**, enter the AWS **special HTTPS Git user ID**. +1. For **Password**, enter the special IAM Git clone user ID password created earlier in AWS. 1. Leave the option **Only mirror protected branches** for CodeCommit. It pushes more frequently (from every five minutes to every minute). @@ -202,7 +200,8 @@ If it isn't working correctly, a red `error` tag appears, and shows the error me [personal access token](../../../profile/personal_access_tokens.md) with `write_repository` scope. 1. On the source GitLab instance: 1. Enter the **Git repository URL** using this format: - `https://oauth2@//.git`. + `https:////.git`. + 1. Enter the **Username** `oauth2`. 1. Enter the **Password**. Use the GitLab personal access token created on the destination GitLab instance. 1. Select **Mirror repository**. diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index 7019b09d664..c6801ab8efd 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -179,7 +179,11 @@ available decreases. All projects no longer have the read-only status because 40 ## Namespace storage limit Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/). -This limit is not visible on the **Usage quotas** page, but is prior to the limit being [applied](#namespace-storage-limit-application-schedule). Self-managed deployments are not affected. + +After namespace storage limits are enforced, view them in the **Usage quotas** page. +For more information about the namespace storage limit enforcement, see the FAQ pages for the [Free](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier) and [Paid](https://about.gitlab.com/pricing/faq-paid-storage-transfer/) tiers. + +Namespace storage limits do not apply to self-managed deployments, but administrators can [manage the repository size](../administration/settings/account_and_limit_settings.md#repository-size-limit). Storage types that add to the total namespace storage are: @@ -200,13 +204,16 @@ To notify you that you have nearly exceeded your namespace storage quota: - In the GitLab UI, a notification displays when you've reached 75%, 95%, and 100% of your namespace storage quota. - GitLab sends an email to members with the Owner role to notify them when namespace storage usage is at 70%, 85%, 95%, and 100%. -To prevent exceeding the namespace storage quota, you can: +To prevent exceeding the namespace storage limit, you can: -- Reduce storage consumption by following the suggestions in the [Manage Your Storage Usage](#manage-your-storage-usage) section of this page. -- Apply for [GitLab for Education](https://about.gitlab.com/solutions/education/join/), [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/), or [GitLab for Startups](https://about.gitlab.com/solutions/startups/) if you meet the eligibility requirements. -- Consider using a [self-managed instance](../subscriptions/self_managed/index.md) of GitLab which does not have these limits on the free tier. +- [Manage your storage usage](#manage-your-storage-usage). +- If you meet the eligibility requirements, you can apply for: + - [GitLab for Education](https://about.gitlab.com/solutions/education/join/) + - [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/) + - [GitLab for Startups](https://about.gitlab.com/solutions/startups/) +- Consider using a [self-managed instance](../subscriptions/self_managed/index.md) of GitLab, which does not have these limits on the Free tier. - [Purchase additional storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer) units at $60/year for 10 GB of storage. -- [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing/) which include higher limits and features that enable growing teams to ship faster without sacrificing on quality. +- [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing/), which include higher limits and features to enable growing teams to ship faster without sacrificing on quality. - [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) for more information about your options. ### View project fork storage usage @@ -222,7 +229,3 @@ To view the amount of namespace storage the fork has used: The cost factor applies to the project repository, LFS objects, job artifacts, packages, snippets, and the wiki. The cost factor does not apply to private forks in namespaces on the Free plan. - -### Namespace storage limit application schedule - -Information on when namespace-level storage limits are applied is available on these FAQ pages for the [Free](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier) and [Paid](https://about.gitlab.com/pricing/faq-paid-storage-transfer/) tier. diff --git a/lib/gitlab/ci/build/duration_parser.rb b/lib/gitlab/ci/build/duration_parser.rb index bc365fe4e9f..97049a4f876 100644 --- a/lib/gitlab/ci/build/duration_parser.rb +++ b/lib/gitlab/ci/build/duration_parser.rb @@ -41,7 +41,7 @@ module Gitlab def parse return if never? - ChronicDuration.parse(value, use_complete_matcher: Feature.enabled?(:update_chronic_duration)) + ChronicDuration.parse(value, use_complete_matcher: true) end def validation_cache diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 044d920f557..c40d665f320 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -177,7 +177,7 @@ module Gitlab def parsed_timeout return unless has_timeout? - ChronicDuration.parse(timeout.to_s, use_complete_matcher: Feature.enabled?(:update_chronic_duration)) + ChronicDuration.parse(timeout.to_s, use_complete_matcher: true) end def ignored? diff --git a/lib/gitlab/config/entry/legacy_validation_helpers.rb b/lib/gitlab/config/entry/legacy_validation_helpers.rb index 9363c60e6db..ec67d65c526 100644 --- a/lib/gitlab/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/config/entry/legacy_validation_helpers.rb @@ -12,7 +12,7 @@ module Gitlab if parser && parser.respond_to?(:validate_duration) parser.validate_duration(value) else - ChronicDuration.parse(value, use_complete_matcher: Feature.enabled?(:update_chronic_duration)) + ChronicDuration.parse(value, use_complete_matcher: true) end rescue ChronicDuration::DurationParseError false @@ -25,10 +25,10 @@ module Gitlab parser.validate_duration_limit(value, limit) else ChronicDuration.parse( - value, use_complete_matcher: Feature.enabled?(:update_chronic_duration) + value, use_complete_matcher: true ).second.from_now < ChronicDuration.parse( - limit, use_complete_matcher: Feature.enabled?(:update_chronic_duration) + limit, use_complete_matcher: true ).second.from_now end rescue ChronicDuration::DurationParseError diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb index 99baec347fe..26efb3b918d 100644 --- a/lib/gitlab/time_tracking_formatter.rb +++ b/lib/gitlab/time_tracking_formatter.rb @@ -19,7 +19,7 @@ module Gitlab string, CUSTOM_DAY_AND_MONTH_LENGTH.merge( default_unit: 'hours', keep_zero: keep_zero, - use_complete_matcher: Feature.enabled?(:update_chronic_duration) + use_complete_matcher: true )) rescue StandardError nil diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f65a35d2697..363614d3f42 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9172,6 +9172,18 @@ msgstr "" msgid "CVE|Why Request a CVE ID?" msgstr "" +msgid "CVS|By enabling this feature, you accept the %{linkStart}Testing Terms of Use%{linkEnd}" +msgstr "" + +msgid "CVS|Continuous Vulnerability Scan" +msgstr "" + +msgid "CVS|Detect vulnerabilities outside a pipeline as new data is added to the GitLab Advisory Database." +msgstr "" + +msgid "CVS|Toggle CVS" +msgstr "" + msgid "Cadence is not automated" msgstr "" diff --git a/qa/qa/page/dashboard/todos.rb b/qa/qa/page/dashboard/todos.rb index a65bba9ac39..873b687c919 100644 --- a/qa/qa/page/dashboard/todos.rb +++ b/qa/qa/page/dashboard/todos.rb @@ -14,7 +14,6 @@ module QA view 'app/views/dashboard/todos/_todo.html.haml' do element :todo_item_container element :todo_action_name_content - element :todo_target_title_content element :todo_author_name_content end @@ -43,20 +42,15 @@ module QA wait_for_requests end - def has_latest_todo_with_author?(author:, action:) - content = { selector: :todo_author_name_content, text: author } - has_latest_todo_with_content?(action, **content) + def click_todo_with_content(content) + click_element(:todo_item_container, text: content) end - def has_latest_todo_with_title?(title:, action:) - content = { selector: :todo_target_title_content, text: title } + def has_latest_todo_with_author?(author:, action:) + content = { selector: :todo_author_name_content, text: author } has_latest_todo_with_content?(action, **content) end - def click_todo_with_content(content) - click_element(:todo_item_container, text: content) - end - private def has_latest_todo_with_content?(action, **kwargs) diff --git a/qa/qa/specs/features/shared_contexts/merge_train_spec_with_user_prep.rb b/qa/qa/specs/features/shared_contexts/merge_train_spec_with_user_prep.rb deleted file mode 100644 index fe0d103b6c0..00000000000 --- a/qa/qa/specs/features/shared_contexts/merge_train_spec_with_user_prep.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.shared_context 'merge train spec with user prep' do - let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" } - let(:file_name) { Faker::Lorem.word } - let(:mr_title) { Faker::Lorem.sentence } - let(:admin_api_client) { Runtime::API::Client.as_admin } - - let(:user) { create(:user, api_client: admin_api_client) } - - let(:project) { create(:project, name: 'pipeline-for-merge-trains') } - - let!(:runner) do - Resource::ProjectRunner.fabricate! do |runner| - runner.project = project - runner.name = executor - runner.tags = [executor] - end - end - - let!(:project_files) do - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project - commit.commit_message = 'Add .gitlab-ci.yml' - commit.add_files( - [ - { - file_path: '.gitlab-ci.yml', - content: <<~YAML - test_merge_train: - tags: - - #{executor} - script: - - sleep 10 - - echo 'OK!' - only: - - merge_requests - YAML - }, - { - file_path: file_name, - content: Faker::Lorem.sentence - } - ] - ) - end - end - - before do - project.add_member(user, Resource::Members::AccessLevel::MAINTAINER) - - Flow::Login.sign_in - project.visit! - Flow::MergeRequest.enable_merge_trains - - Flow::Login.sign_in(as: user) - - Resource::MergeRequest.fabricate_via_api! do |merge_request| - merge_request.title = mr_title - merge_request.project = project - merge_request.description = Faker::Lorem.sentence - merge_request.target_new_branch = false - merge_request.update_existing_file = true - merge_request.file_name = file_name - merge_request.file_content = Faker::Lorem.sentence - end.visit! - - Page::MergeRequest::Show.perform do |show| - show.has_pipeline_status?('passed') - show.try_to_merge! - end - end - - after do - runner&.remove_via_api! - user&.remove_via_api! - end - end -end diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js index cf2dacb50d8..95658f66d09 100644 --- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js @@ -1,58 +1,45 @@ -import { GlCollapsibleListbox } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { GlCollapsibleListbox } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; + import SourceBranchDropdown from '~/jira_connect/branches/components/source_branch_dropdown.vue'; import { BRANCHES_PER_PAGE } from '~/jira_connect/branches/constants'; import getProjectQuery from '~/jira_connect/branches/graphql/queries/get_project.query.graphql'; -import { mockProjects } from '../mock_data'; - -const mockProject = { - id: 'test', - repository: { - branchNames: ['main', 'f-test', 'release'], - rootRef: 'main', - }, -}; -const mockSelectedProject = mockProjects[0]; - -const mockProjectQueryResponse = { - data: { - project: mockProject, - }, -}; -const mockGetProjectQuery = jest.fn().mockResolvedValue(mockProjectQueryResponse); -const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {})); +import { + mockBranchNames, + mockBranchNames2, + mockProjects, + mockProjectQueryResponse, +} from '../mock_data'; + +Vue.use(VueApollo); describe('SourceBranchDropdown', () => { let wrapper; + const mockSelectedProject = mockProjects[0]; + const querySuccessHandler = jest.fn().mockResolvedValue(mockProjectQueryResponse()); + const queryLoadingHandler = jest.fn().mockReturnValue(new Promise(() => {})); + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); - const assertListboxItems = () => { + const assertListboxItems = (branchNames = mockBranchNames) => { const listboxItems = findListbox().props('items'); - expect(listboxItems).toHaveLength(mockProject.repository.branchNames.length); - expect(listboxItems.map((item) => item.text)).toEqual(mockProject.repository.branchNames); + expect(listboxItems).toHaveLength(branchNames.length); + expect(listboxItems.map((item) => item.text)).toEqual(branchNames); }; - function createMockApolloProvider({ getProjectQueryLoading = false } = {}) { - Vue.use(VueApollo); - - const mockApollo = createMockApollo([ - [getProjectQuery, getProjectQueryLoading ? mockQueryLoading : mockGetProjectQuery], - ]); + const createComponent = ({ props, handler = querySuccessHandler } = {}) => { + const mockApollo = createMockApollo([[getProjectQuery, handler]]); - return mockApollo; - } - - function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) { - wrapper = mountFn(SourceBranchDropdown, { - apolloProvider: mockApollo || createMockApolloProvider(), + wrapper = shallowMount(SourceBranchDropdown, { + apolloProvider: mockApollo, propsData: props, }); - } + }; describe('when `selectedProject` prop is not specified', () => { beforeEach(() => { @@ -78,6 +65,7 @@ describe('SourceBranchDropdown', () => { loading: false, searchable: true, searching: false, + selected: null, toggleText: 'Select a branch', }); }); @@ -92,23 +80,26 @@ describe('SourceBranchDropdown', () => { describe('when branches are loading', () => { it('sets loading prop to true', () => { createComponent({ - mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }), props: { selectedProject: mockSelectedProject }, + handler: queryLoadingHandler, }); - expect(findListbox().props('loading')).toEqual(true); + expect(findListbox().props('loading')).toBe(true); }); }); describe('when branches have loaded', () => { describe('when searching branches', () => { it('triggers a refetch', async () => { - createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } }); + createComponent({ props: { selectedProject: mockSelectedProject } }); await waitForPromises(); const mockSearchTerm = 'mai'; + expect(querySuccessHandler).toHaveBeenCalledTimes(1); + await findListbox().vm.$emit('search', mockSearchTerm); - expect(mockGetProjectQuery).toHaveBeenCalledWith({ + expect(querySuccessHandler).toHaveBeenCalledTimes(2); + expect(querySuccessHandler).toHaveBeenLastCalledWith({ branchNamesLimit: BRANCHES_PER_PAGE, branchNamesOffset: 0, branchNamesSearchPattern: `*${mockSearchTerm}*`, @@ -129,10 +120,15 @@ describe('SourceBranchDropdown', () => { loading: false, searchable: true, searching: false, + selected: null, toggleText: 'Select a branch', }); }); + it('disables infinite scroll', () => { + expect(findListbox().props('infiniteScroll')).toBe(false); + }); + it('omits monospace styling from listbox', () => { expect(findListbox().classes()).not.toContain('gl-font-monospace'); }); @@ -142,19 +138,19 @@ describe('SourceBranchDropdown', () => { }); it("emits `change` event with the repository's `rootRef` by default", () => { - expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]); + expect(wrapper.emitted('change')[0]).toEqual([mockBranchNames[0]]); }); describe('when selecting a listbox item', () => { it('emits `change` event with the selected branch name', () => { - const mockBranchName = mockProject.repository.branchNames[1]; + const mockBranchName = mockBranchNames[1]; findListbox().vm.$emit('select', mockBranchName); expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]); }); }); describe('when `selectedBranchName` prop is specified', () => { - const mockBranchName = mockProject.repository.branchNames[2]; + const mockBranchName = mockBranchNames[2]; beforeEach(() => { wrapper.setProps({ @@ -162,6 +158,10 @@ describe('SourceBranchDropdown', () => { }); }); + it('sets listbox selected to `selectedBranchName`', () => { + expect(findListbox().props('selected')).toBe(mockBranchName); + }); + it('sets listbox text to `selectedBranchName` value', () => { expect(findListbox().props('toggleText')).toBe(mockBranchName); }); @@ -170,6 +170,66 @@ describe('SourceBranchDropdown', () => { expect(findListbox().classes()).toContain('gl-font-monospace'); }); }); + + describe('when full page of branches returns', () => { + const fullPageBranchNames = Array(BRANCHES_PER_PAGE) + .fill(1) + .map((_, i) => mockBranchNames[i % mockBranchNames.length]); + + beforeEach(async () => { + createComponent({ + props: { selectedProject: mockSelectedProject }, + handler: () => Promise.resolve(mockProjectQueryResponse(fullPageBranchNames)), + }); + await waitForPromises(); + }); + + it('enables infinite scroll', () => { + expect(findListbox().props('infiniteScroll')).toBe(true); + }); + }); + }); + + describe('when loading more branches from infinite scroll', () => { + const queryLoadMoreHandler = jest.fn(); + + beforeEach(async () => { + queryLoadMoreHandler.mockResolvedValueOnce(mockProjectQueryResponse()); + queryLoadMoreHandler.mockResolvedValueOnce(mockProjectQueryResponse(mockBranchNames2)); + createComponent({ + props: { selectedProject: mockSelectedProject }, + handler: queryLoadMoreHandler, + }); + + await waitForPromises(); + + await findListbox().vm.$emit('bottom-reached'); + }); + + it('sets loading more prop to true', () => { + expect(findListbox().props('infiniteScrollLoading')).toBe(true); + }); + + it('triggers load more query', () => { + expect(queryLoadMoreHandler).toHaveBeenLastCalledWith({ + branchNamesLimit: BRANCHES_PER_PAGE, + branchNamesOffset: 3, + branchNamesSearchPattern: '*', + projectPath: 'test-path', + }); + }); + + it('renders available source branches as listbox items', async () => { + await waitForPromises(); + + assertListboxItems([...mockBranchNames, ...mockBranchNames2]); + }); + + it('sets loading more prop to false once done', async () => { + await waitForPromises(); + + expect(findListbox().props('infiniteScrollLoading')).toBe(false); + }); }); }); }); diff --git a/spec/frontend/jira_connect/branches/mock_data.js b/spec/frontend/jira_connect/branches/mock_data.js index 742ab5392c8..1720e0118c8 100644 --- a/spec/frontend/jira_connect/branches/mock_data.js +++ b/spec/frontend/jira_connect/branches/mock_data.js @@ -1,3 +1,6 @@ +export const mockBranchNames = ['main', 'f-test', 'release']; +export const mockBranchNames2 = ['dev', 'dev-1', 'dev-2']; + export const mockProjects = [ { id: 'test', @@ -28,3 +31,15 @@ export const mockProjects = [ }, }, ]; + +export const mockProjectQueryResponse = (branchNames = mockBranchNames) => ({ + data: { + project: { + id: 'gid://gitlab/Project/27', + repository: { + branchNames, + rootRef: 'main', + }, + }, + }, +}); diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js index 3944ba86942..72c9537bd61 100644 --- a/spec/frontend/search/sidebar/components/app_spec.js +++ b/spec/frontend/search/sidebar/components/app_spec.js @@ -2,7 +2,11 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; -import { SEARCH_TYPE_ZOEKT, SEARCH_TYPE_ADVANCED } from '~/search/sidebar/constants'; +import { + SEARCH_TYPE_ZOEKT, + SEARCH_TYPE_ADVANCED, + SEARCH_TYPE_BASIC, +} from '~/search/sidebar/constants'; import { MOCK_QUERY } from 'jest/search/mock_data'; import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; import GlobalSearchSidebar from '~/search/sidebar/components/app.vue'; @@ -10,6 +14,7 @@ import IssuesFilters from '~/search/sidebar/components/issues_filters.vue'; import MergeRequestsFilters from '~/search/sidebar/components/merge_requests_filters.vue'; import BlobsFilters from '~/search/sidebar/components/blobs_filters.vue'; import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue'; +import NotesFilters from '~/search/sidebar/components/notes_filters.vue'; import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue'; import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue'; @@ -37,6 +42,11 @@ describe('GlobalSearchSidebar', () => { wrapper = shallowMount(GlobalSearchSidebar, { store, + provide: { + glFeatures: { + searchNotesHideArchivedProjects: true, + }, + }, }); }; @@ -45,6 +55,7 @@ describe('GlobalSearchSidebar', () => { const findMergeRequestsFilters = () => wrapper.findComponent(MergeRequestsFilters); const findBlobsFilters = () => wrapper.findComponent(BlobsFilters); const findProjectsFilters = () => wrapper.findComponent(ProjectsFilters); + const findNotesFilters = () => wrapper.findComponent(NotesFilters); const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation); const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation); const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation); @@ -62,18 +73,26 @@ describe('GlobalSearchSidebar', () => { }); describe.each` - scope | filter - ${'issues'} | ${findIssuesFilters} - ${'merge_requests'} | ${findMergeRequestsFilters} - ${'blobs'} | ${findBlobsFilters} - `('with sidebar $scope scope:', ({ scope, filter }) => { + scope | filter | searchType | isShown + ${'issues'} | ${findIssuesFilters} | ${SEARCH_TYPE_BASIC} | ${true} + ${'merge_requests'} | ${findMergeRequestsFilters} | ${SEARCH_TYPE_BASIC} | ${true} + ${'projects'} | ${findProjectsFilters} | ${SEARCH_TYPE_BASIC} | ${true} + ${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${false} + ${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true} + ${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_ZOEKT} | ${false} + ${'notes'} | ${findNotesFilters} | ${SEARCH_TYPE_BASIC} | ${false} + ${'notes'} | ${findNotesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true} + `('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => { beforeEach(() => { getterSpies.currentScope = jest.fn(() => scope); - createComponent({ urlQuery: { scope }, searchType: SEARCH_TYPE_ADVANCED }); + createComponent({ urlQuery: { scope }, searchType }); }); - it(`shows filter ${filter.name.replace('find', '')}`, () => { - expect(filter().exists()).toBe(true); + it(`renders correctly filter ${filter.name.replace( + 'find', + '', + )} when search_type ${searchType}`, () => { + expect(filter().exists()).toBe(isShown); }); }); @@ -101,24 +120,27 @@ describe('GlobalSearchSidebar', () => { describe.each` currentScope | sidebarNavShown | legacyNavShown ${'issues'} | ${false} | ${true} - ${''} | ${false} | ${false} + ${'test'} | ${false} | ${true} ${'issues'} | ${true} | ${false} - ${''} | ${true} | ${false} - `('renders navigation', ({ currentScope, sidebarNavShown, legacyNavShown }) => { - beforeEach(() => { - getterSpies.currentScope = jest.fn(() => currentScope); - createComponent({ useSidebarNavigation: sidebarNavShown }); - }); - - it(`${!legacyNavShown ? 'hides' : 'shows'} the legacy navigation`, () => { - expect(findScopeLegacyNavigation().exists()).toBe(legacyNavShown); - expect(findSmallScreenDrawerNavigation().exists()).toBe(legacyNavShown); - }); - - it(`${!sidebarNavShown ? 'hides' : 'shows'} the sidebar navigation`, () => { - expect(findScopeSidebarNavigation().exists()).toBe(sidebarNavShown); - }); - }); + ${'test'} | ${true} | ${false} + `( + 'renders navigation for scope $currentScope', + ({ currentScope, sidebarNavShown, legacyNavShown }) => { + beforeEach(() => { + getterSpies.currentScope = jest.fn(() => currentScope); + createComponent({ useSidebarNavigation: sidebarNavShown }); + }); + + it(`renders navigation correctly with legacyNavShown ${legacyNavShown}`, () => { + expect(findScopeLegacyNavigation().exists()).toBe(legacyNavShown); + expect(findSmallScreenDrawerNavigation().exists()).toBe(legacyNavShown); + }); + + it(`renders navigation correctly with sidebarNavShown ${sidebarNavShown}`, () => { + expect(findScopeSidebarNavigation().exists()).toBe(sidebarNavShown); + }); + }, + ); }); describe('when useSidebarNavigation=true', () => { diff --git a/spec/frontend/search/sidebar/components/notes_filters_spec.js b/spec/frontend/search/sidebar/components/notes_filters_spec.js new file mode 100644 index 00000000000..2fb8e731ef5 --- /dev/null +++ b/spec/frontend/search/sidebar/components/notes_filters_spec.js @@ -0,0 +1,28 @@ +import { shallowMount } from '@vue/test-utils'; +import NotesFilters from '~/search/sidebar/components/projects_filters.vue'; +import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue'; +import FiltersTemplate from '~/search/sidebar/components/filters_template.vue'; + +describe('GlobalSearch ProjectsFilters', () => { + let wrapper; + + const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter); + const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate); + + const createComponent = () => { + wrapper = shallowMount(NotesFilters); + }; + + describe('Renders correctly', () => { + beforeEach(() => { + createComponent(); + }); + it('renders ArchivedFilter', () => { + expect(findArchivedFilter().exists()).toBe(true); + }); + + it('renders FiltersTemplate', () => { + expect(findFiltersTemplate().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/projects_filters_spec.js b/spec/frontend/search/sidebar/components/projects_filters_spec.js new file mode 100644 index 00000000000..930b7263ea4 --- /dev/null +++ b/spec/frontend/search/sidebar/components/projects_filters_spec.js @@ -0,0 +1,28 @@ +import { shallowMount } from '@vue/test-utils'; +import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue'; +import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue'; +import FiltersTemplate from '~/search/sidebar/components/filters_template.vue'; + +describe('GlobalSearch ProjectsFilters', () => { + let wrapper; + + const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter); + const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate); + + const createComponent = () => { + wrapper = shallowMount(ProjectsFilters); + }; + + describe('Renders correctly', () => { + beforeEach(() => { + createComponent(); + }); + it('renders ArchivedFilter', () => { + expect(findArchivedFilter().exists()).toBe(true); + }); + + it('renders FiltersTemplate', () => { + expect(findFiltersTemplate().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/projects_filters_specs.js b/spec/frontend/search/sidebar/components/projects_filters_specs.js deleted file mode 100644 index 15e3254e289..00000000000 --- a/spec/frontend/search/sidebar/components/projects_filters_specs.js +++ /dev/null @@ -1,28 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue'; -import ArchivedFilter from '~/search/sidebar/components/language_filter/index.vue'; -import FiltersTemplate from '~/search/sidebar/components/filters_template.vue'; - -describe('GlobalSearch ProjectsFilters', () => { - let wrapper; - - const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter); - const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate); - - const createComponent = () => { - wrapper = shallowMount(ProjectsFilters); - }; - - describe('Renders correctly', () => { - beforeEach(() => { - createComponent(); - }); - it('renders ArchivedFilter', () => { - expect(findArchivedFilter().exists()).toBe(true); - }); - - it('renders FiltersTemplate', () => { - expect(findFiltersTemplate().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js b/spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js new file mode 100644 index 00000000000..84a468e4dd8 --- /dev/null +++ b/spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js @@ -0,0 +1,124 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlBadge, GlToggle } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import Vue from 'vue'; +import ProjectSetContinuousVulnerabilityScanning from '~/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql'; +import ContinuousVulnerabilityScan from '~/security_configuration/components/continuous_vulnerability_scan.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; + +Vue.use(VueApollo); + +const setCVSMockResponse = { + data: { + projectSetContinuousVulnerabilityScanning: { + continuousVulnerabilityScanningEnabled: true, + errors: [], + }, + }, +}; + +const defaultProvide = { + continuousVulnerabilityScansEnabled: true, + projectFullPath: 'project/full/path', +}; + +describe('ContinuousVulnerabilityScan', () => { + let wrapper; + let apolloProvider; + let requestHandlers; + + const createComponent = (options) => { + requestHandlers = { + setCVSMutationHandler: jest.fn().mockResolvedValue(setCVSMockResponse), + }; + + apolloProvider = createMockApollo([ + [ProjectSetContinuousVulnerabilityScanning, requestHandlers.setCVSMutationHandler], + ]); + + wrapper = shallowMount(ContinuousVulnerabilityScan, { + propsData: { + feature: { + available: true, + configured: true, + }, + }, + provide: { + glFeatures: { + dependencyScanningOnAdvisoryIngestion: true, + }, + ...defaultProvide, + }, + apolloProvider, + ...options, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + apolloProvider = null; + }); + + const findBadge = () => wrapper.findComponent(GlBadge); + const findToggle = () => wrapper.findComponent(GlToggle); + + it('renders the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('renders the correct title', () => { + expect(wrapper.text()).toContain('Continuous Vulnerability Scan'); + }); + + it('renders the badge and toggle component with correct values', () => { + expect(findBadge().exists()).toBe(true); + expect(findBadge().text()).toBe('Experiment'); + + expect(findToggle().exists()).toBe(true); + expect(findToggle().props('value')).toBe(defaultProvide.continuousVulnerabilityScansEnabled); + }); + + it('should disable toggle when feature is not configured', () => { + createComponent({ + propsData: { + feature: { + available: true, + configured: false, + }, + }, + }); + expect(findToggle().props('disabled')).toBe(true); + }); + + it('calls mutation on toggle change with correct payload', () => { + findToggle().vm.$emit('change', true); + + expect(requestHandlers.setCVSMutationHandler).toHaveBeenCalledWith({ + input: { + projectPath: 'project/full/path', + enable: true, + }, + }); + }); + + describe('when feature flag is disabled', () => { + beforeEach(() => { + createComponent({ + provide: { + glFeatures: { + dependencyScanningOnAdvisoryIngestion: false, + }, + ...defaultProvide, + }, + }); + }); + + it('should not render toggle and badge', () => { + expect(findToggle().exists()).toBe(false); + expect(findBadge().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js index 983a66a7fd3..c715d01dd58 100644 --- a/spec/frontend/security_configuration/components/feature_card_spec.js +++ b/spec/frontend/security_configuration/components/feature_card_spec.js @@ -1,5 +1,6 @@ import { GlIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import Vue from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { securityFeatures } from '~/security_configuration/components/constants'; import FeatureCard from '~/security_configuration/components/feature_card.vue'; @@ -13,6 +14,10 @@ import { import { manageViaMRErrorMessage } from '../constants'; import { makeFeature } from './utils'; +const MockComponent = Vue.component('MockComponent', { + render: (createElement) => createElement('span'), +}); + describe('FeatureCard component', () => { let feature; let wrapper; @@ -389,4 +394,17 @@ describe('FeatureCard component', () => { }); }); }); + + describe('when a slot component is passed', () => { + beforeEach(() => { + feature = makeFeature({ + slotComponent: MockComponent, + }); + createComponent({ feature }); + }); + + it('renders the component properly', () => { + expect(wrapper.findComponent(MockComponent).exists()).toBe(true); + }); + }); }); diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index df58877f120..1a78d929871 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -426,18 +426,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo expect(entry.errors).to be_empty expect(entry.timeout).to eq('1m 1s') end - - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'returns correct timeout' do - expect(entry).to be_valid - expect(entry.errors).to be_empty - expect(entry.timeout).to eq('1m 1s') - end - end end context 'when it is a release' do diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb index dc4ce9458e4..b3372f676d4 100644 --- a/spec/lib/gitlab/time_tracking_formatter_spec.rb +++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb @@ -12,28 +12,12 @@ RSpec.describe Gitlab::TimeTrackingFormatter, feature_category: :team_planning d let(:duration_string) { '3h 20m' } it { expect(subject).to eq(12_000) } - - context 'when update_chronic_duration is false' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it { expect(subject).to eq(12_000) } - end end context 'negative durations' do let(:duration_string) { '-3h 20m' } it { expect(subject).to eq(-12_000) } - - context 'when update_chronic_duration is false' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it { expect(subject).to eq(-12_000) } - end end context 'durations with months' do @@ -42,16 +26,6 @@ RSpec.describe Gitlab::TimeTrackingFormatter, feature_category: :team_planning d it 'uses our custom conversions' do expect(subject).to eq(576_000) end - - context 'when update_chronic_duration is false' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'uses our custom conversions' do - expect(subject).to eq(576_000) - end - end end context 'when the duration is nil' do @@ -69,16 +43,6 @@ RSpec.describe Gitlab::TimeTrackingFormatter, feature_category: :team_planning d it 'returns nil' do expect(subject).to be_nil end - - context 'when update_chronic_duration is false' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'returns nil' do - expect(subject).to be_nil - end - end end context 'when keep_zero is true' do @@ -87,16 +51,6 @@ RSpec.describe Gitlab::TimeTrackingFormatter, feature_category: :team_planning d it 'returns zero' do expect(subject).to eq(0) end - - context 'when update_chronic_duration is false' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'returns zero' do - expect(subject).to eq(0) - end - end end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 8adbd27cd1c..149c0609799 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -735,18 +735,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def is_expected.to eq(1.day.since) end end - - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'returns date after 1 day' do - freeze_time do - is_expected.to eq(1.day.since) - end - end - end end context 'when start_in is 1 week' do @@ -757,18 +745,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def is_expected.to eq(1.week.since) end end - - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'returns date after 1 week' do - freeze_time do - is_expected.to eq(1.week.since) - end - end - end end end @@ -1099,18 +1075,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def is_expected.to be_nil end - - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'assigns a valid duration' do - build.artifacts_expire_in = '7 days' - - is_expected.to be_within(10).of(7.days.to_i) - end - end end describe '#commit' do diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb index fe4a3523909..61b86455840 100644 --- a/spec/models/concerns/chronic_duration_attribute_spec.rb +++ b/spec/models/concerns/chronic_duration_attribute_spec.rb @@ -39,16 +39,6 @@ RSpec.shared_examples 'ChronicDurationAttribute writer' do expect(subject.valid?).to be_truthy end - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'parses chronic duration input' do - expect(subject.send(source_field)).to eq(600) - end - end - context 'when negative input is used' do before do subject.send("#{source_field}=", 3600) @@ -82,16 +72,6 @@ RSpec.shared_examples 'ChronicDurationAttribute writer' do it 'passes validation' do expect(subject.valid?).to be_truthy end - - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'writes default value' do - expect(subject.send(source_field)).to eq(default_value) - end - end end context 'when nil input is used' do @@ -110,16 +90,6 @@ RSpec.shared_examples 'ChronicDurationAttribute writer' do it "doesn't raise exception" do expect { subject.send("#{virtual_field}=", nil) }.not_to raise_error end - - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it 'writes default value' do - expect(subject.send(source_field)).to eq(default_value) - end - end end end diff --git a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb index d55c99b9eb1..f9f8435c211 100644 --- a/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb +++ b/spec/support/shared_examples/projects/container_repository/cleanup_tags_service_shared_examples.rb @@ -153,17 +153,6 @@ RSpec.shared_examples 'when removing older than 1 day' do service_response_extra: service_response_extra, supports_caching: supports_caching, delete_expectations: delete_expectations - - context 'when update_chronic_duration is disabled' do - before do - stub_feature_flags(update_chronic_duration: false) - end - - it_behaves_like 'removing the expected tags', - service_response_extra: service_response_extra, - supports_caching: supports_caching, - delete_expectations: delete_expectations - end end RSpec.shared_examples 'when combining all parameters' do -- cgit v1.2.3