diff --git a/app/assets/javascripts/tracking/tracking.js b/app/assets/javascripts/tracking/tracking.js index 173eef0646b..f299c57b33f 100644 --- a/app/assets/javascripts/tracking/tracking.js +++ b/app/assets/javascripts/tracking/tracking.js @@ -13,8 +13,12 @@ import { const ALLOWED_URL_HASHES = ['#diff', '#note']; export default class Tracking { - static queuedEvents = []; + static nonInitializedQueue = []; static initialized = false; + static definitionsLoaded = false; + static definitionsManifest = {}; + static definitionsEventsQueue = []; + static definitions = []; /** * (Legacy) Determines if tracking is enabled at the user level. @@ -54,13 +58,71 @@ export default class Tracking { } if (!this.initialized) { - this.queuedEvents.push(eventData); + this.nonInitializedQueue.push(eventData); return false; } return dispatchSnowplowEvent(...eventData); } + /** + * Preloads event definitions. + * + * @returns {undefined} + */ + static loadDefinitions() { + // TODO: fetch definitions from the server and flush the queue + // See https://gitlab.com/gitlab-org/gitlab/-/issues/358256 + this.definitionsLoaded = true; + + while (this.definitionsEventsQueue.length) { + this.dispatchFromDefinition(...this.definitionsEventsQueue.shift()); + } + } + + /** + * Dispatches a structured event with data from its event definition. + * + * @param {String} basename + * @param {Object} eventData + * @returns {undefined|Boolean} + */ + static definition(basename, eventData = {}) { + if (!this.enabled()) { + return false; + } + + if (!(basename in this.definitionsManifest)) { + throw new Error(`Missing Snowplow event definition "${basename}"`); + } + + return this.dispatchFromDefinition(basename, eventData); + } + + /** + * Builds an event with data from a valid definition and sends it to + * Snowplow. If the definitions are not loaded, it pushes the data to a queue. + * + * @param {String} basename + * @param {Object} eventData + * @returns {undefined|Boolean} + */ + static dispatchFromDefinition(basename, eventData) { + if (!this.definitionsLoaded) { + this.definitionsEventsQueue.push([basename, eventData]); + + return false; + } + + const eventDefinition = this.definitions.find((definition) => definition.key === basename); + + return this.event( + eventData.category ?? eventDefinition.category, + eventData.action ?? eventDefinition.action, + eventData, + ); + } + /** * Dispatches any event emitted before initialization. * @@ -69,8 +131,8 @@ export default class Tracking { static flushPendingEvents() { this.initialized = true; - while (this.queuedEvents.length) { - dispatchSnowplowEvent(...this.queuedEvents.shift()); + while (this.nonInitializedQueue.length) { + dispatchSnowplowEvent(...this.nonInitializedQueue.shift()); } } diff --git a/app/views/shared/wikis/_main_links.html.haml b/app/views/shared/wikis/_main_links.html.haml index 02794950895..c1fd8c48c60 100644 --- a/app/views/shared/wikis/_main_links.html.haml +++ b/app/views/shared/wikis/_main_links.html.haml @@ -1,5 +1,5 @@ - if @page&.persisted? - = link_to wiki_page_path(@wiki, @page, action: :history), class: "btn gl-button", role: "button", data: { qa_selector: 'page_history_button' } do + = link_to wiki_page_path(@wiki, @page, action: :history), class: "btn gl-button btn-default", role: "button", data: { qa_selector: 'page_history_button' } do = s_("Wiki|Page history") - if can?(current_user, :create_wiki, @wiki.container) = link_to wiki_path(@wiki, action: :new), class: "btn gl-button btn-confirm-secondary", role: "button", data: { qa_selector: 'new_page_button' } do diff --git a/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml b/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml old mode 100644 new mode 100755 index 217c20e574f..282dd32e9c5 --- a/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml +++ b/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml @@ -168,6 +168,7 @@ options: - p_ci_templates_kaniko - p_ci_templates_qualys_iac_security - p_ci_templates_liquibase + - p_ci_templates_matlab distribution: - ce - ee diff --git a/config/metrics/counts_28d/20220310184327_p_ci_templates_matlab_monthly.yml b/config/metrics/counts_28d/20220310184327_p_ci_templates_matlab_monthly.yml new file mode 100644 index 00000000000..bd61f9ddc57 --- /dev/null +++ b/config/metrics/counts_28d/20220310184327_p_ci_templates_matlab_monthly.yml @@ -0,0 +1,25 @@ +--- +key_path: redis_hll_counters.ci_templates.p_ci_templates_matlab_monthly +description: "" +product_section: "" +product_stage: "" +product_group: "" +product_category: "" +value_type: number +status: active +milestone: "14.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82914 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - p_ci_templates_matlab diff --git a/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml b/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml old mode 100644 new mode 100755 index 9368500ab13..f2706260463 --- a/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml +++ b/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml @@ -168,6 +168,7 @@ options: - p_ci_templates_kaniko - p_ci_templates_qualys_iac_security - p_ci_templates_liquibase + - p_ci_templates_matlab distribution: - ce - ee diff --git a/config/metrics/counts_7d/20220310184320_p_ci_templates_matlab_weekly.yml b/config/metrics/counts_7d/20220310184320_p_ci_templates_matlab_weekly.yml new file mode 100644 index 00000000000..0642003f672 --- /dev/null +++ b/config/metrics/counts_7d/20220310184320_p_ci_templates_matlab_weekly.yml @@ -0,0 +1,25 @@ +--- +key_path: redis_hll_counters.ci_templates.p_ci_templates_matlab_weekly +description: "" +product_section: "" +product_stage: "" +product_group: "" +product_category: "" +value_type: number +status: active +milestone: "14.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82914 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +options: + events: + - p_ci_templates_matlab diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md index 6db37ac1546..bb741c39c08 100644 --- a/doc/administration/reference_architectures/index.md +++ b/doc/administration/reference_architectures/index.md @@ -430,7 +430,7 @@ to any of the [available reference architectures](#available-reference-architect > - Required domain knowledge: PostgreSQL, HAProxy, shared storage, distributed systems GitLab supports [zero-downtime upgrades](../../update/zero_downtime.md). -Single GitLab nodes can be updated with only a [few minutes of downtime](../../update/zero_downtime.md#single-node-deployment). +Single GitLab nodes can be updated with only a [few minutes of downtime](../../update/index.md#upgrade-based-on-installation-method). To avoid this, we recommend to separate GitLab into several application nodes. As long as at least one of each component is online and capable of handling the instance's usage load, your team's productivity will not be interrupted during the update. diff --git a/doc/update/zero_downtime.md b/doc/update/zero_downtime.md index 2cfe062aca2..5aa80c12f11 100644 --- a/doc/update/zero_downtime.md +++ b/doc/update/zero_downtime.md @@ -15,14 +15,12 @@ there are the following requirements: sequence [and leave the database schema in a broken state](https://gitlab.com/gitlab-org/gitlab/-/issues/321542). - You have to use [post-deployment migrations](../development/database/post_deployment_migrations.md). - You are using PostgreSQL. Starting from GitLab 12.1, MySQL is not supported. -- Multi-node GitLab instance. Single-node instances may experience brief interruptions - [as services restart (Puma in particular)](#single-node-deployment). +- You have set up a multi-node GitLab instance. Single-node instances do not support zero-downtime upgrades. If you meet all the requirements above, follow these instructions in order. There are three sets of steps, depending on your deployment type: | Deployment type | Description | | --------------------------------------------------------------- | ------------------------------------------------ | -| [Single-node](#single-node-deployment) | GitLab CE/EE on a single node | | [Gitaly Cluster](#gitaly-cluster) | GitLab CE/EE using HA architecture for Gitaly Cluster | | [Multi-node / PostgreSQL HA](#use-postgresql-ha) | GitLab CE/EE using HA architecture for PostgreSQL | | [Multi-node / Redis HA](#use-redis-ha-using-sentinel) | GitLab CE/EE using HA architecture for Redis | @@ -87,80 +85,6 @@ migrations this could potentially lead to hours of downtime, depending on the size of your database. To work around this you must use PostgreSQL and meet the other online upgrade requirements mentioned above. -## Single-node deployment - -WARNING: -You can only upgrade one minor release at a time. - -Before following these instructions, note the following **important** information: - -- You can only upgrade one minor release at a time. So from 13.6 to 13.7, not to 13.8. - If you attempt more than one minor release, the upgrade may fail. -- On single-node Omnibus deployments, updates with no downtime are not possible when - using Puma because Puma always requires a complete restart. This is because the - [phased restart](https://github.com/puma/puma/blob/master/README.md#clustered-mode) - feature of Puma does not work with the way it is configured in GitLab all-in-one - packages (cluster-mode with app preloading). -- While it is possible to minimize downtime on a single-node instance by following - these instructions, **it is not possible to always achieve true zero downtime - updates**. Users may see some connections timeout or be refused for a few minutes, - depending on which services need to restart. -- On Omnibus deployments, the `/etc/gitlab/gitlab.rb` configuration file must **not** have - `gitlab_rails['auto_migrate'] = true`. - -1. Create an empty file at `/etc/gitlab/skip-auto-reconfigure`. This prevents upgrades from running `gitlab-ctl reconfigure`, which by default automatically stops GitLab, runs all database migrations, and restarts GitLab. - - ```shell - sudo touch /etc/gitlab/skip-auto-reconfigure - ``` - -1. Update the GitLab package: - - - For GitLab [Enterprise Edition](https://about.gitlab.com/pricing/): - - ```shell - # Debian/Ubuntu - sudo apt-get update - sudo apt-get install gitlab-ee - - # Centos/RHEL - sudo yum install gitlab-ee - ``` - - - For GitLab Community Edition: - - ```shell - # Debian/Ubuntu - sudo apt-get update - sudo apt-get install gitlab-ce - - # Centos/RHEL - sudo yum install gitlab-ce - ``` - -1. To get the regular migrations and latest code in place, run - - ```shell - sudo SKIP_POST_DEPLOYMENT_MIGRATIONS=true gitlab-ctl reconfigure - ``` - -1. Once the node is updated and `reconfigure` finished successfully, run post-deployment migrations with - - ```shell - sudo gitlab-rake db:migrate - ``` - -1. Hot reload `puma` and `sidekiq` services - - ```shell - sudo gitlab-ctl hup puma - sudo gitlab-ctl restart sidekiq - ``` - -If you do not want to run zero downtime upgrades in the future, make -sure you remove `/etc/gitlab/skip-auto-reconfigure` after -you've completed these steps. - ## Multi-node / HA deployment WARNING: diff --git a/doc/user/admin_area/merge_requests_approvals.md b/doc/user/admin_area/merge_requests_approvals.md index 0ecf76902e1..ed7fdfe2111 100644 --- a/doc/user/admin_area/merge_requests_approvals.md +++ b/doc/user/admin_area/merge_requests_approvals.md @@ -38,4 +38,4 @@ Merge request approval settings that can be set at an instance level are: See also the following, which are affected by instance-level rules: - [Project merge request approval rules](../project/merge_requests/approvals/index.md). -- [Group merge request approval rules](../group/index.md#group-approval-rules) available in GitLab 13.9 and later. +- [Group merge request approval settings](../group/index.md#group-approval-settings) available in GitLab 13.9 and later. diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index f03d8327744..af463ae2342 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -190,6 +190,26 @@ of your GitLab instance (`.gitlab-ci.yml` if not set): It is also possible to specify a [custom CI/CD configuration file for a specific project](../../../ci/pipelines/settings.md#specify-a-custom-cicd-configuration-file). +## Set CI/CD limits + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352175) in GitLab 14.10. + +You can configure some [CI/CD limits](../../../administration/instance_limits.md#cicd-limits) +from the Admin Area: + +1. On the top bar, select **Menu > Admin**. +1. On the left sidebar, select **Settings > CI/CD**. +1. Expand the **Continuous Integration and Deployment** section. +1. In the **CI/CD limits** section, you can set the following limits: + - **Maximum number of jobs in a single pipeline** + - **Total number of jobs in currently active pipelines** + - **Maximum number of active pipelines per project** + - **Maximum number of pipeline subscriptions to and from a project** + - **Maximum number of pipeline schedules** + - **Maximum number of DAG dependencies that a job can have** + - **Maximum number of runners registered per group** + - **Maximum number of runners registered per project** + ## Enable or disable the pipeline suggestion banner By default, a banner displays in merge requests with no pipeline suggesting a diff --git a/doc/user/group/index.md b/doc/user/group/index.md index b5f541bfd59..085cd054c14 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -796,23 +796,25 @@ The group's new subgroups have push rules set for them based on either: - The closest parent group with push rules defined. - Push rules set at the instance level, if no parent groups have push rules defined. -## Group approval rules **(PREMIUM)** +## Group approval settings **(PREMIUM)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285458) in GitLab 13.9. [Deployed behind the `group_merge_request_approval_settings_feature_flag` flag](../../administration/feature_flags.md), disabled by default. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/285410) in GitLab 14.5. > - [Feature flag `group_merge_request_approval_settings_feature_flag`](https://gitlab.com/gitlab-org/gitlab/-/issues/343872) removed in GitLab 14.9. -Group approval rules manage [project merge request approval rules](../project/merge_requests/approvals/index.md) -at the top-level group level. These rules [cascade to all projects](../project/merge_requests/approvals/settings.md#settings-cascading) +Group approval settings manage [project merge request approval settings](../project/merge_requests/approvals/settings.md) +at the top-level group level. These settings [cascade to all projects](../project/merge_requests/approvals/settings.md#settings-cascading) that belong to the group. -To view the merge request approval rules for a group: +To view the merge request approval settings for a group: 1. Go to the top-level group's **Settings > General** page. 1. Expand the **Merge request approvals** section. 1. Select the settings you want. 1. Select **Save changes**. +Support for group-level settings for merge request approval rules is tracked in this [epic](https://gitlab.com/groups/gitlab-org/-/epics/4367). + ## Related topics - [Group wikis](../project/wiki/index.md) diff --git a/doc/user/infrastructure/iac/index.md b/doc/user/infrastructure/iac/index.md index 6fa211742b8..bc7a3c0d069 100644 --- a/doc/user/infrastructure/iac/index.md +++ b/doc/user/infrastructure/iac/index.md @@ -91,49 +91,29 @@ in the template you fetched to customize your configuration. ## GitLab-managed Terraform state -[Terraform remote backends](https://www.terraform.io/docs/language/settings/backends/index.html) -enable you to store the state file in a remote, shared store. GitLab uses the -[Terraform HTTP backend](https://www.terraform.io/docs/language/settings/backends/http.html) -to securely store the state files in local storage (the default) or -[the remote store of your choice](../../../administration/terraform_state.md). - -The GitLab-managed Terraform state backend can safely store your Terraform state. It spares you from setting up additional remote resources like -Amazon S3 or Google Cloud Storage. Its features include: - -- Supporting encryption of the state file both in transit and at rest. -- Locking and unlocking state. -- Remote Terraform plan and apply execution. - -Read more about setting up and [using GitLab-managed Terraform states](terraform_state.md). +Use the [GitLab-managed Terraform state](terraform_state.md) to store state +files in local storage or in a remote store of your choice. ## Terraform module registry -GitLab can be used as a [Terraform module registry](../../packages/terraform_module_registry/index.md) -to create and publish Terraform modules to a private registry specific to your -top-level namespace. +Use GitLab as a [Terraform module registry](../../packages/terraform_module_registry/index.md) +to create and publish Terraform modules to a private registry. ## Terraform integration in merge requests -Collaborating around Infrastructure as Code (IaC) changes requires both code changes -and expected infrastructure changes to be checked and approved. GitLab provides a -solution to help collaboration around Terraform code changes and their expected -effects using the merge request pages. This way users don't have to build custom -tools or rely on 3rd party solutions to streamline their IaC workflows. - -Read more on setting up and [using the merge request integrations](mr_integration.md). +Use the [Terraform integration in merge requests](mr_integration.md) +to collaborate on Terraform code changes and Infrastructure-as-Code +workflows. ## The GitLab Terraform provider -WARNING: +NOTE: The GitLab Terraform provider is released separately from GitLab. -We are working on migrating the GitLab Terraform provider for GitLab.com. +We are working on migrating the GitLab Terraform provider to GitLab.com. -You can use the [GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab) -to manage various aspects of GitLab using Terraform. The provider is an open source project, -owned by GitLab, where everyone can contribute. - -The [documentation of the provider](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs) -is available as part of the official Terraform provider documentation. +The [GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab) is a plugin for Terraform to facilitate +managing of GitLab resources such as users, groups, and projects. +Its documentation is available on [Terraform](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs). ## Create a new cluster through IaC diff --git a/doc/user/project/merge_requests/approvals/settings.md b/doc/user/project/merge_requests/approvals/settings.md index 0a7fbc9ee95..0ede9310393 100644 --- a/doc/user/project/merge_requests/approvals/settings.md +++ b/doc/user/project/merge_requests/approvals/settings.md @@ -146,7 +146,7 @@ You can also enforce merge request approval settings: - At the [instance level](../../../admin_area/merge_requests_approvals.md), which apply to all groups on an instance and, therefore, all projects. -- On a [top-level group](../../../group/index.md#group-approval-rules), which apply to all subgroups +- On a [top-level group](../../../group/index.md#group-approval-settings), which apply to all subgroups and projects. If the settings are inherited by a group or project, they cannot be changed in the group or project diff --git a/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml b/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml new file mode 100644 index 00000000000..67c69115948 --- /dev/null +++ b/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml @@ -0,0 +1,96 @@ +# To contribute improvements to CI/CD templates, please follow the Development guide at: +# https://docs.gitlab.com/ee/development/cicd/templates.html +# This specific template is located at: +# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml + +# Use this template to run MATLAB and Simulink as part of your CI/CD pipeline. The template has three jobs: +# - `command`: Run MATLAB scripts, functions, and statements. +# - `test`: Run tests authored using the MATLAB unit testing framework or Simulink Test. +# - `test_artifacts_job`: Run MATLAB and Simulink tests, and generate test and coverage artifacts. +# +# You can copy and paste one or more jobs in this template into your `.gitlab-ci.yml` file. +# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. +# +# - To run MATLAB and Simulink, MATLAB must be installed on the runner that will run the jobs. +# The runner will use the topmost MATLAB version on the system path. +# The build fails if the operating system cannot find MATLAB on the path. +# - The jobs in this template use the `matlab -batch` syntax to start MATLAB. The `-batch` option is supported +# in MATLAB R2019a and later. + +# The `command` runs MATLAB scripts, functions, and statements. To use the job in your pipeline, +# substitute `command` with the code you want to run. +# +command: + script: matlab -batch command + +# If the value of `command` is the name of a MATLAB script or function, do not specify the file extension. +# For example, to run a script named `myscript.m` in the root of your repository, specify the `command` like this: +# +# "myscript" +# +# If you specify more than one script, function, or statement, use a comma or semicolon to separate them. +# For example, to run `myscript.m` in a folder named `myfolder` located in the root of the repository, +# you can specify the `command` like this: +# +# "addpath('myfolder'), myscript" +# +# MATLAB exits with exit code 0 if the specified script, function, or statement executes successfully without +# error. Otherwise, MATLAB terminates with a nonzero exit code, which causes the job to fail. To have the +# job fail in certain conditions, use the [`assert`][1] or [`error`][2] functions. +# +# [1] https://www.mathworks.com/help/matlab/ref/assert.html +# [2] https://www.mathworks.com/help/matlab/ref/error.html + +# The `test` runs the MATLAB and Simulink tests in your project. It calls the [`runtests`][3] function +# to run the tests and then the [`assertSuccess`][4] method to fail the job if any of the tests fail. +# +test: + script: matlab -batch "results = runtests('IncludeSubfolders',true), assertSuccess(results);" + +# By default, the job includes any files in your [MATLAB Project][5] that have a `Test` label. If your repository +# does not have a MATLAB project, then the job includes all tests in the root of your repository or in any of +# its subfolders. +# +# [3] https://www.mathworks.com/help/matlab/ref/runtests.html +# [4] https://www.mathworks.com/help/matlab/ref/matlab.unittest.testresult.assertsuccess.html +# [5] https://www.mathworks.com/help/matlab/projects.html + +# The `test_artifacts_job` runs your tests and additionally generates test and coverage artifacts. +# It uses the plugin classes in the [`matlab.unittest.plugins`][6] package to generate a JUnit test results +# report and a Cobertura code coverage report. Like the `run_tests` job, this job runs all the tests in your +# project and fails the build if any of the tests fail. +# +test_artifacts_job: + script: | + matlab -batch " + import matlab.unittest.TestRunner + import matlab.unittest.Verbosity + import matlab.unittest.plugins.CodeCoveragePlugin + import matlab.unittest.plugins.XMLPlugin + import matlab.unittest.plugins.codecoverage.CoberturaFormat + + suite = testsuite(pwd,'IncludeSubfolders',true); + + [~,~] = mkdir('artifacts'); + + runner = TestRunner.withTextOutput('OutputDetail',Verbosity.Detailed); + runner.addPlugin(XMLPlugin.producingJUnitFormat('artifacts/results.xml')) + runner.addPlugin(CodeCoveragePlugin.forFolder(pwd,'IncludingSubfolders',true, ... + 'Producing',CoberturaFormat('artifacts/cobertura.xml'))) + + results = runner.run(suite) + assertSuccess(results);" + + artifacts: + reports: + junit: "./artifacts/results.xml" + cobertura: "./artifacts/cobertura.xml" + paths: + - "./artifacts" + +# You can modify the contents of the `test_artifacts_job` depending on your goals. For more +# information on how to customize the test runner and generate various test and coverage artifacts, +# see [Generate Artifacts Using MATLAB Unit Test Plugins][7]. +# +# [6] https://www.mathworks.com/help/matlab/ref/matlab.unittest.plugins-package.html +# [7] https://www.mathworks.com/help/matlab/matlab_prog/generate-artifacts-using-matlab-unit-test-plugins.html diff --git a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml index 917a7b5caec..412716aa649 100644 --- a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml +++ b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml @@ -619,3 +619,7 @@ category: ci_templates redis_slot: ci_templates aggregation: weekly +- name: p_ci_templates_matlab + category: ci_templates + redis_slot: ci_templates + aggregation: weekly diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 96e1f558255..7fe49c2571c 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' RSpec.describe "Admin Runners" do - include StubENV + include Spec::Support::Helpers::Features::RunnersHelpers + + let_it_be(:admin) { create(:admin) } before do - stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - admin = create(:admin) sign_in(admin) gitlab_enable_admin_mode_sign_in(admin) @@ -20,47 +20,54 @@ RSpec.describe "Admin Runners" do let_it_be(:namespace) { create(:namespace) } let_it_be(:project) { create(:project, namespace: namespace, creator: user) } + context "runners registration" do + before do + visit admin_runners_path + end + + it_behaves_like "shows and resets runner registration token" do + let(:dropdown_text) { 'Register an instance runner' } + let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token } + end + end + context "when there are runners" do - it 'has all necessary texts' do - create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: Time.zone.now) - create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.week.ago) - create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.year.ago) + context "with an instance runner" do + let!(:instance_runner) { create(:ci_runner, :instance) } - visit admin_runners_path + before do + visit admin_runners_path + end - expect(page).to have_text "Register an instance runner" - expect(page).to have_text "Online runners 1" - expect(page).to have_text "Offline runners 2" - expect(page).to have_text "Stale runners 1" - end + it_behaves_like 'shows runner in list' do + let(:runner) { instance_runner } + end - it 'with an instance runner shows an instance badge' do - runner = create(:ci_runner, :instance) + it_behaves_like 'pauses, resumes and deletes a runner' do + let(:runner) { instance_runner } + end - visit admin_runners_path - - within "[data-testid='runner-row-#{runner.id}']" do - expect(page).to have_selector '.badge', text: 'shared' + it 'shows an instance badge' do + within_runner_row(instance_runner.id) do + expect(page).to have_selector '.badge', text: 'shared' + end end end - it 'with a group runner shows a group badge' do - runner = create(:ci_runner, :group, groups: [group]) + context "with multiple runners" do + before do + create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: Time.zone.now) + create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.week.ago) + create(:ci_runner, :instance, created_at: 1.year.ago, contacted_at: 1.year.ago) - visit admin_runners_path - - within "[data-testid='runner-row-#{runner.id}']" do - expect(page).to have_selector '.badge', text: 'group' + visit admin_runners_path end - end - it 'with a project runner shows a project badge' do - runner = create(:ci_runner, :project, projects: [project]) - - visit admin_runners_path - - within "[data-testid='runner-row-#{runner.id}']" do - expect(page).to have_selector '.badge', text: 'specific' + it 'has all necessary texts' do + expect(page).to have_text "Register an instance runner" + expect(page).to have_text "Online runners 1" + expect(page).to have_text "Offline runners 2" + expect(page).to have_text "Stale runners 1" end end @@ -72,44 +79,8 @@ RSpec.describe "Admin Runners" do visit admin_runners_path - within "[data-testid='runner-row-#{runner.id}'] [data-label='Jobs']" do - expect(page).to have_content '2' - end - end - - describe 'delete runner' do - let!(:runner) { create(:ci_runner, description: 'runner-foo') } - - before do - visit admin_runners_path - - within "[data-testid='runner-row-#{runner.id}']" do - click_on 'Delete runner' - end - end - - it 'shows a confirmation modal' do - expect(page).to have_text "Delete runner ##{runner.id} (#{runner.short_sha})?" - expect(page).to have_text "Are you sure you want to continue?" - end - - it 'deletes a runner' do - within '.modal' do - click_on 'Delete runner' - end - - expect(page.find('.gl-toast')).to have_text(/Runner .+ deleted/) - expect(page).not_to have_content 'runner-foo' - end - - it 'cancels runner deletion' do - within '.modal' do - click_on 'Cancel' - end - - wait_for_requests - - expect(page).to have_content 'runner-foo' + within_runner_row(runner.id) do + expect(find("[data-label='Jobs']")).to have_content '2' end end @@ -249,7 +220,7 @@ RSpec.describe "Admin Runners" do expect(page).not_to have_content 'runner-paused' expect(page).to have_content 'runner-never-contacted' - within "[data-testid='runner-row-#{never_contacted.id}']" do + within_runner_row(never_contacted.id) do expect(page).to have_selector '.badge', text: 'never contacted' end end @@ -447,15 +418,7 @@ RSpec.describe "Admin Runners" do visit admin_runners_path end - it 'has all necessary texts including no runner message' do - expect(page).to have_text "Register an instance runner" - - expect(page).to have_text "Online runners 0" - expect(page).to have_text "Offline runners 0" - expect(page).to have_text "Stale runners 0" - - expect(page).to have_text 'No runners found' - end + it_behaves_like "shows no runners" it 'shows tabs with total counts equal to 0' do expect(page).to have_link('All 0') @@ -484,17 +447,6 @@ RSpec.describe "Admin Runners" do expect(page).to have_current_path(admin_runners_path('paused[]': 'true') ) end end - - describe "runners registration" do - before do - visit admin_runners_path - end - - it_behaves_like "shows and resets runner registration token" do - let(:dropdown_text) { 'Register an instance runner' } - let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token } - end - end end describe "Runner show page", :js do @@ -644,57 +596,4 @@ RSpec.describe "Admin Runners" do end end end - - private - - def search_bar_selector - '[data-testid="runners-filtered-search"]' - end - - # The filters must be clicked first to be able to receive events - # See: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1493 - def focus_filtered_search - page.within(search_bar_selector) do - page.find('.gl-filtered-search-term-token').click - end - end - - def input_filtered_search_keys(search_term) - focus_filtered_search - - page.within(search_bar_selector) do - page.find('input').send_keys(search_term) - click_on 'Search' - end - - wait_for_requests - end - - def open_filtered_search_suggestions(filter) - focus_filtered_search - - page.within(search_bar_selector) do - click_on filter - end - - wait_for_requests - end - - def input_filtered_search_filter_is_only(filter, value) - focus_filtered_search - - page.within(search_bar_selector) do - click_on filter - - # For OPERATOR_IS_ONLY, clicking the filter - # immediately preselects "=" operator - - page.find('input').send_keys(value) - page.find('input').send_keys(:enter) - - click_on 'Search' - end - - wait_for_requests - end end diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb index beb465cd3a5..1d821edefa3 100644 --- a/spec/features/groups/group_runners_spec.rb +++ b/spec/features/groups/group_runners_spec.rb @@ -3,10 +3,11 @@ require 'spec_helper' RSpec.describe "Group Runners" do + include Spec::Support::Helpers::Features::RunnersHelpers + let_it_be(:group_owner) { create(:user) } let_it_be(:group) { create(:group) } - - let!(:group_registration_token) { group.runners_token } + let_it_be(:project) { create(:project, group: group) } before do group.add_owner(group_owner) @@ -14,7 +15,9 @@ RSpec.describe "Group Runners" do end describe "Group runners page", :js do - describe "runners registration" do + let!(:group_registration_token) { group.runners_token } + + context "runners registration" do before do visit group_runners_path(group) end @@ -24,5 +27,142 @@ RSpec.describe "Group Runners" do let(:registration_token) { group_registration_token } end end + + context "with no runners" do + before do + visit group_runners_path(group) + end + + it_behaves_like "shows no runners" + + it 'shows tabs with total counts equal to 0' do + expect(page).to have_link('All 0') + expect(page).to have_link('Group 0') + expect(page).to have_link('Project 0') + end + end + + context "with an online group runner" do + let!(:group_runner) do + create(:ci_runner, :group, groups: [group], description: 'runner-foo', contacted_at: Time.zone.now) + end + + before do + visit group_runners_path(group) + end + + it_behaves_like 'shows runner in list' do + let(:runner) { group_runner } + end + + it_behaves_like 'pauses, resumes and deletes a runner' do + let(:runner) { group_runner } + end + + it 'shows a group badge' do + within_runner_row(group_runner.id) do + expect(page).to have_selector '.badge', text: 'group' + end + end + + it 'can edit runner information' do + within_runner_row(group_runner.id) do + expect(find_link('Edit')[:href]).to end_with(edit_group_runner_path(group, group_runner)) + end + end + end + + context "with an online project runner" do + let!(:project_runner) do + create(:ci_runner, :project, projects: [project], description: 'runner-bar', contacted_at: Time.zone.now) + end + + before do + visit group_runners_path(group) + end + + it_behaves_like 'shows runner in list' do + let(:runner) { project_runner } + end + + it_behaves_like 'pauses, resumes and deletes a runner' do + let(:runner) { project_runner } + end + + it 'shows a project (specific) badge' do + within_runner_row(project_runner.id) do + expect(page).to have_selector '.badge', text: 'specific' + end + end + + it 'can edit runner information' do + within_runner_row(project_runner.id) do + expect(find_link('Edit')[:href]).to end_with(edit_group_runner_path(group, project_runner)) + end + end + end + + context 'with a multi-project runner' do + let(:project) { create(:project, group: group) } + let(:project_2) { create(:project, group: group) } + let!(:runner) { create(:ci_runner, :project, projects: [project, project_2], description: 'group-runner') } + + it 'user cannot remove the project runner' do + visit group_runners_path(group) + + within_runner_row(runner.id) do + expect(page).to have_button 'Delete runner', disabled: true + end + end + end + + context 'filtered search' do + before do + visit group_runners_path(group) + end + + it 'allows user to search by paused and status', :js do + focus_filtered_search + + page.within(search_bar_selector) do + expect(page).to have_link('Paused') + expect(page).to have_content('Status') + end + end + end + end + + describe "Group runner edit page", :js do + let!(:runner) do + create(:ci_runner, :group, groups: [group], description: 'runner-foo', contacted_at: Time.zone.now) + end + + it 'user edits the runner to be protected' do + visit edit_group_runner_path(group, runner) + + expect(page.find_field('runner[access_level]')).not_to be_checked + + check 'runner_access_level' + click_button 'Save changes' + + expect(page).to have_content 'Protected Yes' + end + + context 'when a runner has a tag' do + before do + runner.update!(tag_list: ['tag']) + end + + it 'user edits runner not to run untagged jobs' do + visit edit_group_runner_path(group, runner) + + expect(page.find_field('runner[run_untagged]')).to be_checked + + uncheck 'runner_run_untagged' + click_button 'Save changes' + + expect(page).to have_content 'Can run untagged jobs No' + end + end end end diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js index d85299cdfc3..665bf44fc77 100644 --- a/spec/frontend/tracking/tracking_spec.js +++ b/spec/frontend/tracking/tracking_spec.js @@ -129,6 +129,72 @@ describe('Tracking', () => { }); }); + describe('.definition', () => { + const TEST_VALID_BASENAME = '202108302307_default_click_button'; + const TEST_EVENT_DATA = { category: undefined, action: 'click_button' }; + let eventSpy; + let dispatcherSpy; + + beforeAll(() => { + Tracking.definitionsManifest = { + '202108302307_default_click_button': 'config/events/202108302307_default_click_button.yml', + }; + }); + + beforeEach(() => { + eventSpy = jest.spyOn(Tracking, 'event'); + dispatcherSpy = jest.spyOn(Tracking, 'dispatchFromDefinition'); + }); + + it('throws an error if the definition does not exists', () => { + const basename = '20220230_default_missing_definition'; + const expectedError = new Error(`Missing Snowplow event definition "${basename}"`); + + expect(() => Tracking.definition(basename)).toThrow(expectedError); + }); + + it('dispatches an event from a definition present in the manifest', () => { + Tracking.definition(TEST_VALID_BASENAME); + + expect(dispatcherSpy).toHaveBeenCalledWith(TEST_VALID_BASENAME, {}); + }); + + it('push events to the queue if not loaded', () => { + Tracking.definitionsLoaded = false; + Tracking.definitionsEventsQueue = []; + + const dispatched = Tracking.definition(TEST_VALID_BASENAME); + + expect(dispatched).toBe(false); + expect(Tracking.definitionsEventsQueue[0]).toStrictEqual([TEST_VALID_BASENAME, {}]); + expect(eventSpy).not.toHaveBeenCalled(); + }); + + it('dispatch events when the definition is loaded', () => { + const definition = { key: TEST_VALID_BASENAME, ...TEST_EVENT_DATA }; + Tracking.definitions = [{ ...definition }]; + Tracking.definitionsEventsQueue = []; + Tracking.definitionsLoaded = true; + + const dispatched = Tracking.definition(TEST_VALID_BASENAME); + + expect(dispatched).not.toBe(false); + expect(Tracking.definitionsEventsQueue).toEqual([]); + expect(eventSpy).toHaveBeenCalledWith(definition.category, definition.action, {}); + }); + + it('lets defined event data takes precedence', () => { + const definition = { key: TEST_VALID_BASENAME, category: undefined, action: 'click_button' }; + const eventData = { category: TEST_CATEGORY }; + Tracking.definitions = [{ ...definition }]; + Tracking.definitionsLoaded = true; + + Tracking.definition(TEST_VALID_BASENAME, eventData); + + expect(eventSpy).toHaveBeenCalledWith(TEST_CATEGORY, definition.action, eventData); + }); + }); + describe('.enableFormTracking', () => { it('tells snowplow to enable form tracking, with only explicit contexts', () => { const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } }; diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index 1a9e2f02de6..6cb9085c3ad 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -6,11 +6,15 @@ RSpec.describe Gitlab::Auth::OAuth::User do include LdapHelpers let(:oauth_user) { described_class.new(auth_hash) } + let(:oauth_user_2) { described_class.new(auth_hash_2) } let(:gl_user) { oauth_user.gl_user } + let(:gl_user_2) { oauth_user_2.gl_user } let(:uid) { 'my-uid' } + let(:uid_2) { 'my-uid-2' } let(:dn) { 'uid=user1,ou=people,dc=example' } let(:provider) { 'my-provider' } let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } + let(:auth_hash_2) { OmniAuth::AuthHash.new(uid: uid_2, provider: provider, info: info_hash) } let(:info_hash) do { nickname: '-john+gitlab-ETC%.git@gmail.com', @@ -24,6 +28,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do end let(:ldap_user) { Gitlab::Auth::Ldap::Person.new(Net::LDAP::Entry.new, 'ldapmain') } + let(:ldap_user_2) { Gitlab::Auth::Ldap::Person.new(Net::LDAP::Entry.new, 'ldapmain') } describe '.find_by_uid_and_provider' do let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' } @@ -46,12 +51,12 @@ RSpec.describe Gitlab::Auth::OAuth::User do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } it "finds an existing user based on uid and provider (facebook)" do - expect( oauth_user.persisted? ).to be_truthy + expect(oauth_user.persisted?).to be_truthy end it 'returns false if user is not found in database' do allow(auth_hash).to receive(:uid).and_return('non-existing') - expect( oauth_user.persisted? ).to be_falsey + expect(oauth_user.persisted?).to be_falsey end end @@ -78,15 +83,27 @@ RSpec.describe Gitlab::Auth::OAuth::User do context 'when signup is disabled' do before do stub_application_setting signup_enabled: false + stub_omniauth_config(allow_single_sign_on: [provider]) end it 'creates the user' do - stub_omniauth_config(allow_single_sign_on: [provider]) - oauth_user.save # rubocop:disable Rails/SaveBang expect(gl_user).to be_persisted end + + it 'does not repeat the default user password' do + oauth_user.save # rubocop:disable Rails/SaveBang + oauth_user_2.save # rubocop:disable Rails/SaveBang + + expect(gl_user.password).not_to eq(gl_user_2.password) + end + + it 'has the password length within specified range' do + oauth_user.save # rubocop:disable Rails/SaveBang + + expect(gl_user.password.length).to be_between(Devise.password_length.min, Devise.password_length.max) + end end context 'when user confirmation email is enabled' do @@ -330,6 +347,12 @@ RSpec.describe Gitlab::Auth::OAuth::User do allow(ldap_user).to receive(:name) { 'John Doe' } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } allow(ldap_user).to receive(:dn) { dn } + + allow(ldap_user_2).to receive(:uid) { uid_2 } + allow(ldap_user_2).to receive(:username) { uid_2 } + allow(ldap_user_2).to receive(:name) { 'Beck Potter' } + allow(ldap_user_2).to receive(:email) { ['beckpotter@example.com', 'beck2@example.com'] } + allow(ldap_user_2).to receive(:dn) { dn } end context "and no account for the LDAP user" do @@ -340,6 +363,14 @@ RSpec.describe Gitlab::Auth::OAuth::User do oauth_user.save # rubocop:disable Rails/SaveBang end + it 'does not repeat the default user password' do + allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user_2) + + oauth_user_2.save # rubocop:disable Rails/SaveBang + + expect(gl_user.password).not_to eq(gl_user_2.password) + end + it "creates a user with dual LDAP and omniauth identities" do expect(gl_user).to be_valid expect(gl_user.username).to eql uid @@ -609,6 +640,7 @@ RSpec.describe Gitlab::Auth::OAuth::User do context 'signup with SAML' do let(:provider) { 'saml' } + let(:block_auto_created_users) { false } before do stub_omniauth_config({ @@ -625,6 +657,13 @@ RSpec.describe Gitlab::Auth::OAuth::User do it_behaves_like 'not being blocked on creation' do let(:block_auto_created_users) { false } end + + it 'does not repeat the default user password' do + oauth_user.save # rubocop:disable Rails/SaveBang + oauth_user_2.save # rubocop:disable Rails/SaveBang + + expect(gl_user.password).not_to eq(gl_user_2.password) + end end context 'signup with omniauth only' do diff --git a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb new file mode 100644 index 00000000000..a12d69b67a6 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'MATLAB.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('MATLAB') } + + describe 'the created pipeline' do + let_it_be(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } + + let(:user) { project.first_owner } + let(:default_branch) { 'master' } + let(:pipeline_branch) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + end + + it 'creates all jobs' do + expect(build_names).to include('command', 'test', 'test_artifacts_job') + end + end +end diff --git a/spec/support/helpers/features/runner_helpers.rb b/spec/support/helpers/features/runner_helpers.rb new file mode 100644 index 00000000000..63fc628358c --- /dev/null +++ b/spec/support/helpers/features/runner_helpers.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Spec + module Support + module Helpers + module Features + module RunnersHelpers + def within_runner_row(runner_id) + within "[data-testid='runner-row-#{runner_id}']" do + yield + end + end + + def search_bar_selector + '[data-testid="runners-filtered-search"]' + end + + # The filters must be clicked first to be able to receive events + # See: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1493 + def focus_filtered_search + page.within(search_bar_selector) do + page.find('.gl-filtered-search-term-token').click + end + end + + def input_filtered_search_keys(search_term) + focus_filtered_search + + page.within(search_bar_selector) do + page.find('input').send_keys(search_term) + click_on 'Search' + end + + wait_for_requests + end + + def open_filtered_search_suggestions(filter) + focus_filtered_search + + page.within(search_bar_selector) do + click_on filter + end + + wait_for_requests + end + + def input_filtered_search_filter_is_only(filter, value) + focus_filtered_search + + page.within(search_bar_selector) do + click_on filter + + # For OPERATOR_IS_ONLY, clicking the filter + # immediately preselects "=" operator + + page.find('input').send_keys(value) + page.find('input').send_keys(:enter) + + click_on 'Search' + end + + wait_for_requests + end + end + end + end + end +end diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index 133f3ff5fb6..3660934ce8e 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -2,6 +2,7 @@ RSpec.shared_examples 'shows and resets runner registration token' do include Spec::Support::Helpers::ModalHelpers + include Spec::Support::Helpers::Features::RunnersHelpers before do click_on dropdown_text @@ -60,3 +61,81 @@ RSpec.shared_examples 'shows and resets runner registration token' do end end end + +RSpec.shared_examples 'shows no runners' do + it 'shows counts with 0' do + expect(page).to have_text "Online runners 0" + expect(page).to have_text "Offline runners 0" + expect(page).to have_text "Stale runners 0" + end + + it 'shows "no runners" message' do + expect(page).to have_text 'No runners found' + end +end + +RSpec.shared_examples 'shows runner in list' do + it 'does not show empty state' do + expect(page).not_to have_content 'No runners found' + end + + it 'shows runner row' do + within_runner_row(runner.id) do + expect(page).to have_text "##{runner.id}" + expect(page).to have_text runner.short_sha + expect(page).to have_text runner.description + end + end +end + +RSpec.shared_examples 'pauses, resumes and deletes a runner' do + include Spec::Support::Helpers::ModalHelpers + + it 'pauses and resumes runner' do + within_runner_row(runner.id) do + click_button "Pause" + + expect(page).to have_text 'paused' + expect(page).to have_button 'Resume' + expect(page).not_to have_button 'Pause' + + click_button "Resume" + + expect(page).not_to have_text 'paused' + expect(page).not_to have_button 'Resume' + expect(page).to have_button 'Pause' + end + end + + describe 'deletes runner' do + before do + within_runner_row(runner.id) do + click_on 'Delete runner' + end + end + + it 'shows a confirmation modal' do + expect(page).to have_text "Delete runner ##{runner.id} (#{runner.short_sha})?" + expect(page).to have_text "Are you sure you want to continue?" + end + + it 'deletes a runner' do + within_modal do + click_on 'Delete runner' + end + + expect(page.find('.gl-toast')).to have_text(/Runner .+ deleted/) + expect(page).not_to have_content runner.description + end + + it 'cancels runner deletion' do + within_modal do + click_on 'Cancel' + end + + wait_for_requests + + expect(page).to have_content runner.description + end + end +end