diff options
41 files changed, 767 insertions, 557 deletions
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 7e9a0b7133a..72bfd2cdec4 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -16,12 +16,13 @@ ## Author's checklist (required) - [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). -- [ ] Apply the ~documentation label, plus: - - The corresponding DevOps stage and group label, if applicable. - - ~"development guidelines" when changing docs under `doc/development/*`, `CONTRIBUTING.md`, or `README.md`. - - ~"development guidelines" and ~"Documentation guidelines" when changing docs under `development/documentation/*`. - - ~"development guidelines" and ~"Description templates (.gitlab/\*)" when creating/updating issue and MR description templates. -- [ ] Assign the [designated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments). +- If you have `developer` access or higher (for example, GitLab team members or [Core Team](https://about.gitlab.com/community/core-team/) members) + - [ ] Apply the ~documentation label, plus: + - The corresponding DevOps stage and group label, if applicable. + - ~"development guidelines" when changing docs under `doc/development/*`, `CONTRIBUTING.md`, or `README.md`. + - ~"development guidelines" and ~"Documentation guidelines" when changing docs under `development/documentation/*`. + - ~"development guidelines" and ~"Description templates (.gitlab/\*)" when creating/updating issue and MR description templates. + - [ ] Assign the [designated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments). When applicable: diff --git a/.rubocop.yml b/.rubocop.yml index fbcad46a0c7..92861717cab 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -212,11 +212,13 @@ Gitlab/HTTParty: - 'ee/spec/**/*' Gitlab/Json: - Enabled: false + Enabled: true Exclude: - 'db/**/*' - 'qa/**/*' - 'scripts/**/*' + - 'lib/rspec_flaky/**/*' + - 'lib/quality/**/*' GitlabSecurity/PublicSend: Enabled: true diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index 98f682c2e8a..5305894873f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -51,7 +51,7 @@ export default { rel="noopener noreferrer nofollow" data-container="body" > - <icon name="question-o" /> + <icon name="question" /> </a> </div> </template> diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 85fdcb753b4..b241d0a2bdc 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -167,10 +167,6 @@ } } - a.gl-label-icon { - color: $gray-500; - } - .gl-label .gl-label-link:hover { text-decoration: none; color: inherit; @@ -180,11 +176,6 @@ } } - .gl-label .gl-label-icon:hover { - text-decoration: none; - color: $gray-500; - } - .btn-link { color: inherit; } @@ -826,10 +817,6 @@ } } } - - .gl-label-icon { - color: $gray-500; - } } @media(max-width: map-get($grid-breakpoints, lg)-1) { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index b42f0195f69..22c1cb127cd 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -460,8 +460,7 @@ // Label inside title of Delete Label Modal .modal-header .page-title { .scoped-label-wrapper { - .scoped-label, - .gl-label-icon { + .scoped-label { line-height: 20px; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index fa10ab022dc..c473cc44637 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -68,10 +68,6 @@ $status-box-line-height: 26px; .gl-label-link { color: inherit; } - - .gl-label-icon { - color: $gray-500; - } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 6445695bc6b..bed147aa3a7 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -688,8 +688,7 @@ $note-form-margin-left: 72px; text-decoration: underline; } - .gl-label-link:hover, - .gl-label-icon:hover { + .gl-label-link:hover { text-decoration: none; color: inherit; diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml index f523b993aa7..732ba95a63f 100644 --- a/app/views/import/google_code/new_user_map.html.haml +++ b/app/views/import/google_code/new_user_map.html.haml @@ -30,7 +30,7 @@ .form-group.row .col-sm-12 - = text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15 + = text_area_tag :user_map, Gitlab::Json.pretty_generate(@user_map), class: 'form-control', rows: 15 .form-actions = submit_tag _('Continue to the next step'), class: "btn btn-success" diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml index f3b56df0c96..6b056e93460 100644 --- a/app/views/shared/hook_logs/_content.html.haml +++ b/app/views/shared/hook_logs/_content.html.haml @@ -31,7 +31,7 @@ %h5 Request body: %pre :escaped - #{JSON.pretty_generate(hook_log.request_data)} + #{Gitlab::Json.pretty_generate(hook_log.request_data)} %h5 Response headers: %pre - hook_log.response_headers.each do |k,v| diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index ccac944605d..6809d818717 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -849,7 +849,7 @@ :urgency: :high :resource_boundary: :cpu :weight: 5 - :idempotent: + :idempotent: true - :name: repository_check:repository_check_batch :feature_category: :source_code_management :has_external_dependencies: diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb index 69698ba81bd..63d11d33283 100644 --- a/app/workers/update_head_pipeline_for_merge_request_worker.rb +++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class UpdateHeadPipelineForMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker +class UpdateHeadPipelineForMergeRequestWorker include ApplicationWorker include PipelineQueue @@ -9,6 +9,8 @@ class UpdateHeadPipelineForMergeRequestWorker # rubocop:disable Scalability/Idem urgency :high worker_resource_boundary :cpu + idempotent! + def perform(merge_request_id) MergeRequest.find_by_id(merge_request_id).try do |merge_request| merge_request.update_head_pipeline diff --git a/changelogs/unreleased/199053-inconsistent-help-icon-styling.yml b/changelogs/unreleased/199053-inconsistent-help-icon-styling.yml new file mode 100644 index 00000000000..3d11351ca8b --- /dev/null +++ b/changelogs/unreleased/199053-inconsistent-help-icon-styling.yml @@ -0,0 +1,5 @@ +--- +title: Update merge request widget question mark icons +merge_request: 30759 +author: +type: other diff --git a/changelogs/unreleased/fj-update-snippet-backfilling-with-migrate-bot.yml b/changelogs/unreleased/fj-update-snippet-backfilling-with-migrate-bot.yml new file mode 100644 index 00000000000..07030a44875 --- /dev/null +++ b/changelogs/unreleased/fj-update-snippet-backfilling-with-migrate-bot.yml @@ -0,0 +1,5 @@ +--- +title: Use migration bot user in snippet migration +merge_request: 30762 +author: +type: fixed diff --git a/danger/telemetry/Dangerfile b/danger/telemetry/Dangerfile index dd3c8a6a322..c18a15fcb03 100644 --- a/danger/telemetry/Dangerfile +++ b/danger/telemetry/Dangerfile @@ -9,12 +9,24 @@ USAGE_DATA_FILES_MESSAGE = <<~MSG For the following files, a review from the [Data team and Telemetry team](https://gitlab.com/groups/gitlab-org/growth/telemetry/engineers/-/group_members?with_inherited_permissions=exclude) is recommended: MSG +tracking_files = [ + 'lib/gitlab/tracking.rb', + 'spec/lib/gitlab/tracking_spec.rb', + 'app/helpers/tracking_helper.rb', + 'spec/helpers/tracking_helper_spec.rb', + 'app/assets/javascripts/tracking.js', + 'spec/frontend/tracking_spec.js' + ] + usage_data_changed_files = git.modified_files.grep(%r{usage_data}) +snowplow_events_changed_files = git.modified_files & tracking_files + +changed_files = (usage_data_changed_files + snowplow_events_changed_files) -if usage_data_changed_files.any? +if changed_files.any? warn format(TELEMETRY_CHANGED_FILES_MESSAGE) - markdown(USAGE_DATA_FILES_MESSAGE + helper.markdown_list(usage_data_changed_files)) + markdown(USAGE_DATA_FILES_MESSAGE + helper.markdown_list(changed_files)) telemetry_labels = ['telemetry'] telemetry_labels << 'telemetry::review pending' unless helper.mr_has_labels?('telemetry::reviewed') diff --git a/db/migrate/20200429181335_add_default_value_for_file_store_to_lfs_objects.rb b/db/migrate/20200429181335_add_default_value_for_file_store_to_lfs_objects.rb new file mode 100644 index 00000000000..f316a092bfc --- /dev/null +++ b/db/migrate/20200429181335_add_default_value_for_file_store_to_lfs_objects.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddDefaultValueForFileStoreToLfsObjects < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + change_column_default :lfs_objects, :file_store, 1 + end + end + + def down + with_lock_retries do + change_column_default :lfs_objects, :file_store, nil + end + end +end diff --git a/db/migrate/20200429181955_add_default_value_for_file_store_to_ci_job_artifacts.rb b/db/migrate/20200429181955_add_default_value_for_file_store_to_ci_job_artifacts.rb new file mode 100644 index 00000000000..ac3d5e41e3e --- /dev/null +++ b/db/migrate/20200429181955_add_default_value_for_file_store_to_ci_job_artifacts.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddDefaultValueForFileStoreToCiJobArtifacts < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + change_column_default :ci_job_artifacts, :file_store, 1 + end + end + + def down + with_lock_retries do + change_column_default :ci_job_artifacts, :file_store, nil + end + end +end diff --git a/db/migrate/20200429182245_add_default_value_for_store_to_uploads.rb b/db/migrate/20200429182245_add_default_value_for_store_to_uploads.rb new file mode 100644 index 00000000000..f28fcce8f2f --- /dev/null +++ b/db/migrate/20200429182245_add_default_value_for_store_to_uploads.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddDefaultValueForStoreToUploads < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + change_column_default :uploads, :store, 1 + end + end + + def down + with_lock_retries do + change_column_default :uploads, :store, nil + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 73edcc371ce..e450a99eace 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1120,7 +1120,7 @@ CREATE TABLE public.ci_job_artifacts ( updated_at timestamp with time zone NOT NULL, expire_at timestamp with time zone, file character varying, - file_store integer, + file_store integer DEFAULT 1, file_sha256 bytea, file_format smallint, file_location smallint, @@ -3644,7 +3644,7 @@ CREATE TABLE public.lfs_objects ( created_at timestamp without time zone, updated_at timestamp without time zone, file character varying, - file_store integer + file_store integer DEFAULT 1 ); CREATE SEQUENCE public.lfs_objects_id_seq @@ -6487,7 +6487,7 @@ CREATE TABLE public.uploads ( model_type character varying, uploader character varying NOT NULL, created_at timestamp without time zone NOT NULL, - store integer, + store integer DEFAULT 1, mount_point character varying, secret character varying ); @@ -13706,5 +13706,8 @@ COPY "schema_migrations" (version) FROM STDIN; 20200424101920 20200427064130 20200429015603 +20200429181335 +20200429181955 +20200429182245 \. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 3eb7467b410..76dfcc901fd 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -229,25 +229,29 @@ To use Container Scanning in an offline environment, you need: NOTE: **Note:** GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy), -meaning the runner may try to pull remote images even if a local copy is available. Set GitLab -Runner's [`pull_policy` to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) -in an offline environment if you prefer using only locally available Docker images. +meaning the Runner tries to pull Docker images from the GitLab container registry even if a local +copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) +in an offline environment if you prefer using only locally available Docker images. However, we +recommend keeping the pull policy setting to `always` if not in an offline environment, as this +enables the use of updated scanners in your CI/CD pipelines. #### Make GitLab Container Scanning analyzer images available inside your Docker registry -For Container Scanning, import and host the following images from `registry.gitlab.com` to your -offline [local Docker container registry](../../packages/container_registry/index.md): +For Container Scanning, import the following default images from `registry.gitlab.com` into your +[local Docker container registry](../../packages/container_registry/index.md): -- [arminc/clair-db vulnerabilities database](https://hub.docker.com/r/arminc/clair-db) -- GitLab klar analyzer: `registry.gitlab.com/gitlab-org/security-products/analyzers/klar` +```plaintext +registry.gitlab.com/gitlab-org/security-products/analyzers/klar +https://hub.docker.com/r/arminc/clair-db +``` The process for importing Docker images into a local offline Docker registry depends on **your network security policy**. Please consult your IT staff to find an accepted and approved -process by which external resources can be imported or temporarily accessed. - -Note that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database) +process by which you can import or temporarily access external resources. Note that these scanners +are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database) with new definitions, so consider if you are able to make periodic updates yourself. -You can read more specific steps on how to do this [below](#automating-container-scanning-vulnerability-database-updates-with-a-pipeline). + +For more information, see [the specific steps on how to update an image with a pipeline](#automating-container-scanning-vulnerability-database-updates-with-a-pipeline). For details on saving and transporting Docker images as a file, see Docker's documentation on [`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/), @@ -255,8 +259,6 @@ For details on saving and transporting Docker images as a file, see Docker's doc #### Set Container Scanning CI job variables to use local Container Scanner analyzers -Container Scanning can be executed on an offline GitLab Ultimate installation using the following process: - 1. [Override the container scanning template](#overriding-the-container-scanning-template) in your `.gitlab-ci.yml` file to refer to the Docker images hosted on your local Docker container registry: ```yaml diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 42480e60e48..15ce6695b4f 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -523,14 +523,15 @@ To use DAST in an offline environment, you need: NOTE: **Note:** GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy), -meaning the runner may try to pull remote images even if a local copy is available. Set GitLab -Runner's [`pull_policy` to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) -in an offline environment if you prefer using only locally available Docker images. +meaning the Runner tries to pull Docker images from the GitLab container registry even if a local +copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) +in an offline environment if you prefer using only locally available Docker images. However, we +recommend keeping the pull policy setting to `always` if not in an offline environment, as this +enables the use of updated scanners in your CI/CD pipelines. ### Make GitLab DAST analyzer images available inside your Docker registry -For DAST, import the following default DAST analyzer image from `registry.gitlab.com` to your local "offline" -registry: +For DAST, import the following default DAST analyzer image from `registry.gitlab.com` to your [local Docker container registry](../../packages/container_registry/index.md): - `registry.gitlab.com/gitlab-org/security-products/dast:latest` @@ -548,16 +549,18 @@ For details on saving and transporting Docker images as a file, see Docker's doc ### Set DAST CI job variables to use local DAST analyzers -1. Add the following configuration to your `.gitlab-ci.yml` file. You must replace `image` to refer - to the DAST Docker image hosted on your local Docker container registry: +Add the following configuration to your `.gitlab-ci.yml` file. You must replace `image` to refer to +the DAST Docker image hosted on your local Docker container registry: - ```yaml - include: - - template: DAST.gitlab-ci.yml +```yaml +include: + - template: DAST.gitlab-ci.yml +dast: + image: registry.example.com/namespace/dast:latest +``` - dast: - image: registry.example.com/namespace/dast:latest - ``` +The DAST job should now use local copies of the DAST analyzers to scan your code and generate +security reports without requiring internet access. ## Reports diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index be9c8c9d129..99f4d524b7d 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -420,32 +420,33 @@ You can also [submit new vulnerabilities](https://gitlab.com/gitlab-org/security ## Running Dependency Scanning in an offline environment For self-managed GitLab instances in an environment with limited, restricted, or intermittent access -to external resources through the internet, some adjustments are required for dependency scanning jobs to run successfully. For more information, see [Offline environments](../offline_deployments/index.md). +to external resources through the internet, some adjustments are required for Dependency Scanning +jobs to run successfully. For more information, see [Offline environments](../offline_deployments/index.md). ### Requirements for offline Dependency Scanning -The requirements for using Dependency Scanning in an offline environment are: +Here are the requirements for using Dependency Scanning in an offline environment: - [Disable Docker-In-Docker](#disabling-docker-in-docker-for-dependency-scanning). - GitLab Runner with the [`docker` or `kubernetes` executor](#requirements). -- Docker Container Registry with locally available copies of dependency scanning [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images. +- Docker Container Registry with locally available copies of Dependency Scanning [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images. - Host an offline Git copy of the [gemnasium-db advisory database](https://gitlab.com/gitlab-org/security-products/gemnasium-db/) - _Only if scanning Ruby projects_: Host an offline Git copy of the [advisory database](https://github.com/rubysec/ruby-advisory-db). - _Only if scanning npm/yarn projects_: Host an offline copy of the [retire.js](https://github.com/RetireJS/retire.js/) [node](https://github.com/RetireJS/retire.js/blob/master/repository/npmrepository.json) and [js](https://github.com/RetireJS/retire.js/blob/master/repository/jsrepository.json) advisory databases. NOTE: **Note:** GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy), -meaning the runner will try to pull Docker images from the GitLab container registry even if a local +meaning the Runner tries to pull Docker images from the GitLab container registry even if a local copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) -in an offline environment, if you prefer using only locally available Docker images. However, we -recommend keeping the pull policy setting to `always` as it will better enable updated scanners to -be utilized within your CI/CD pipelines. +in an offline environment if you prefer using only locally available Docker images. However, we +recommend keeping the pull policy setting to `always` if not in an offline environment, as this +enables the use of updated scanners in your CI/CD pipelines. ### Make GitLab Dependency Scanning analyzer images available inside your Docker registry -For Dependency Scanning, import Docker images ([supported languages and frameworks](#supported-languages-and-package-managers)) -from `registry.gitlab.com` to your offline Docker registry. The Dependency Scanning analyzer -Docker images are: +For Dependency Scanning with all [supported languages and frameworks](#supported-languages-and-package-managers), +import the following default Dependency Scanning analyzer images from `registry.gitlab.com` into +your [local Docker container registry](../../packages/container_registry/index.md): ```plaintext registry.gitlab.com/gitlab-org/security-products/analyzers/gemnasium:2 @@ -465,10 +466,10 @@ For details on saving and transporting Docker images as a file, see Docker's doc [`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/), [`docker export`](https://docs.docker.com/engine/reference/commandline/export/), and [`docker import`](https://docs.docker.com/engine/reference/commandline/import/). -### Set Dependency Scanning CI config for "offline" use +### Set Dependency Scanning CI job variables to use local Dependency Scanning analyzers -Below is a general `.gitlab-ci.yml` template to configure your environment for running -Dependency Scanning offline: +Add the following configuration to your `.gitlab-ci.yml` file. You must replace +`DS_ANALYZER_IMAGE_PREFIX` to refer to your local Docker container registry: ```yaml include: diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 51a2fdab1b9..0f42e062901 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -527,17 +527,17 @@ To use SAST in an offline environment, you need: NOTE: **Note:** GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy), -meaning the runner will try to pull Docker images from the GitLab container registry even if a local +meaning the Runner tries to pull Docker images from the GitLab container registry even if a local copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) in an offline environment if you prefer using only locally available Docker images. However, we -recommend keeping the pull policy setting to `always` as it will better enable updated scanners to -be utilized within your CI/CD pipelines. +recommend keeping the pull policy setting to `always` if not in an offline environment, as this +enables the use of updated scanners in your CI/CD pipelines. ### Make GitLab SAST analyzer images available inside your Docker registry For SAST with all [supported languages and frameworks](#supported-languages-and-frameworks), -import the following default SAST analyzer images from `registry.gitlab.com` to your local "offline" -registry: +import the following default SAST analyzer images from `registry.gitlab.com` into your +[local Docker container registry](../../packages/container_registry/index.md): ```plaintext registry.gitlab.com/gitlab-org/security-products/analyzers/bandit:2 @@ -568,10 +568,8 @@ For details on saving and transporting Docker images as a file, see Docker's doc ### Set SAST CI job variables to use local SAST analyzers -[Override SAST environment variables](#customizing-the-sast-settings) to use to your [local container registry](./analyzers.md#using-a-custom-docker-mirror) -as the source for SAST analyzer images. - -For example, assuming a local Docker registry repository of `localhost:5000/analyzers`: +Add the following configuration to your `.gitlab-ci.yml` file. You must replace +`SAST_ANALYZER_IMAGE_PREFIX` to refer to your local Docker container registry: ```yaml include: diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 3e468cbb607..9f77b3baad0 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -333,7 +333,7 @@ license_scanning: For self-managed GitLab instances in an environment with limited, restricted, or intermittent access to external resources through the internet, some adjustments are required for the License Compliance job to -successfully run. +successfully run. For more information, see [Offline environments](../../application_security/offline_deployments/index.md). ### Requirements for offline License Compliance @@ -344,11 +344,11 @@ To use License Compliance in an offline environment, you need: NOTE: **Note:** GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy), -meaning the runner will try to pull Docker images from the GitLab container registry even if a local +meaning the Runner tries to pull Docker images from the GitLab container registry even if a local copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) in an offline environment if you prefer using only locally available Docker images. However, we -recommend leaving the pull policy set to `always`, as it better enables updated scanners to be used -within your CI/CD pipelines. +recommend keeping the pull policy setting to `always` if not in an offline environment, as this +enables the use of updated scanners in your CI/CD pipelines. ### Make GitLab License Compliance analyzer images available inside your Docker registry @@ -371,10 +371,8 @@ For details on saving and transporting Docker images as a file, see Docker's doc ### Set License Compliance CI job variables to use local License Compliance analyzers -Override License Compliance environment variables to use to your local container registry -as the source for License Compliance analyzer images. - -For example, this assumes a local Docker registry repository of `localhost:5000/analyzers`: +Add the following configuration to your `.gitlab-ci.yml` file. You must replace `image` to refer to +the License Compliance Docker image hosted on your local Docker container registry: ```yaml include: diff --git a/lib/gitlab/background_migration/backfill_snippet_repositories.rb b/lib/gitlab/background_migration/backfill_snippet_repositories.rb index 7aa00e03dda..192a82c84e6 100644 --- a/lib/gitlab/background_migration/backfill_snippet_repositories.rb +++ b/lib/gitlab/background_migration/backfill_snippet_repositories.rb @@ -98,17 +98,17 @@ module Gitlab # because it is blocked, internal, ghost, ... we cannot commit # files because these users are not allowed to, but we need to # migrate their snippets as well. - # In this scenario an admin user will be the one that will commit the files. + # In this scenario the migration bot user will be the one that will commit the files. def commit_author(snippet) if Gitlab::UserAccessSnippet.new(snippet.author, snippet: snippet).can_do_action?(:update_snippet) snippet.author else - admin_user + migration_bot_user end end - def admin_user - @admin_user ||= User.admins.active.first + def migration_bot_user + @migration_bot_user ||= User.migration_bot end # We sometimes receive invalid path errors from Gitaly if the Snippet filename diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb index d05ffe9b508..a06eddab78f 100644 --- a/lib/gitlab/git_access_snippet.rb +++ b/lib/gitlab/git_access_snippet.rb @@ -61,6 +61,13 @@ module Gitlab end end + override :can_read_project? + def can_read_project? + return true if user&.migration_bot? + + super + end + override :check_download_access! def check_download_access! passed = guest_can_download_code? || user_can_download_code? diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb index 5cd2a86a106..c38769f39a9 100644 --- a/lib/gitlab/metrics/samplers/ruby_sampler.rb +++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb @@ -34,16 +34,14 @@ module Gitlab def init_metrics metrics = { - file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels), - memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used (RSS)', labels), - process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'), - process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'), - process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used (RSS)', labels), - process_unique_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :unique_memory_bytes), 'Memory used (USS)', labels), - process_proportional_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :proportional_memory_bytes), 'Memory used (PSS)', labels), - process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'), - sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels), - gc_duration_seconds: ::Gitlab::Metrics.histogram(with_prefix(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS) + file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels), + memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels), + process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'), + process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'), + process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used', labels), + process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'), + sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels), + gc_duration_seconds: ::Gitlab::Metrics.histogram(with_prefix(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS) } GC.stat.keys.each do |key| @@ -87,15 +85,10 @@ module Gitlab end def set_memory_usage_metrics - memory_rss = System.memory_usage - metrics[:memory_bytes].set(labels, memory_rss) - metrics[:process_resident_memory_bytes].set(labels, memory_rss) - - if Feature.enabled?(:collect_memory_uss_pss) - memory_uss_pss = System.memory_usage_uss_pss - metrics[:process_unique_memory_bytes].set(labels, memory_uss_pss[:uss]) - metrics[:process_proportional_memory_bytes].set(labels, memory_uss_pss[:pss]) - end + memory_usage = System.memory_usage + + metrics[:memory_bytes].set(labels, memory_usage) + metrics[:process_resident_memory_bytes].set(labels, memory_usage) end end end diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb index d01b6bc5b50..2a61b3de405 100644 --- a/lib/gitlab/metrics/system.rb +++ b/lib/gitlab/metrics/system.rb @@ -7,37 +7,47 @@ module Gitlab # This module relies on the /proc filesystem being available. If /proc is # not available the methods of this module will be stubbed. module System - PROC_STATUS_PATH = '/proc/self/status' - PROC_SMAPS_ROLLUP_PATH = '/proc/self/smaps_rollup' - PROC_LIMITS_PATH = '/proc/self/limits' - PROC_FD_GLOB = '/proc/self/fd/*' - - PRIVATE_PAGES_PATTERN = /^(Private_Clean|Private_Dirty|Private_Hugetlb):\s+(?<value>\d+)/.freeze - PSS_PATTERN = /^Pss:\s+(?<value>\d+)/.freeze - RSS_PATTERN = /VmRSS:\s+(?<value>\d+)/.freeze - MAX_OPEN_FILES_PATTERN = /Max open files\s*(?<value>\d+)/.freeze - - # Returns the current process' RSS (resident set size) in bytes. - def self.memory_usage - sum_matches(PROC_STATUS_PATH, rss: RSS_PATTERN)[:rss].kilobytes - end + if File.exist?('/proc') + # Returns the current process' memory usage in bytes. + def self.memory_usage + mem = 0 + match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/) + + if match && match[1] + mem = match[1].to_f * 1024 + end - # Returns the current process' USS/PSS (unique/proportional set size) in bytes. - def self.memory_usage_uss_pss - sum_matches(PROC_SMAPS_ROLLUP_PATH, uss: PRIVATE_PAGES_PATTERN, pss: PSS_PATTERN) - .transform_values(&:kilobytes) - end + mem + end - def self.file_descriptor_count - Dir.glob(PROC_FD_GLOB).length - end + def self.file_descriptor_count + Dir.glob('/proc/self/fd/*').length + end + + def self.max_open_file_descriptors + match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/) + + return unless match && match[1] - def self.max_open_file_descriptors - sum_matches(PROC_LIMITS_PATH, max_fds: MAX_OPEN_FILES_PATTERN)[:max_fds] + match[1].to_i + end + else + def self.memory_usage + 0.0 + end + + def self.file_descriptor_count + 0 + end + + def self.max_open_file_descriptors + 0 + end end def self.cpu_time - Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second) + Process + .clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second) end # Returns the current real time in a given precision. @@ -68,27 +78,6 @@ module Gitlab end_time - start_time end - - # Given a path to a file in /proc and a hash of (metric, pattern) pairs, - # sums up all values found for those patterns under the respective metric. - def self.sum_matches(proc_file, **patterns) - results = patterns.transform_values { 0 } - - begin - File.foreach(proc_file) do |line| - patterns.each do |metric, pattern| - match = line.match(pattern) - value = match&.named_captures&.fetch('value', 0) - results[metric] += value.to_i - end - end - rescue Errno::ENOENT - # This means the procfile we're reading from did not exist; - # this is safe to ignore, since we initialize each metric to 0 - end - - results - end end end end diff --git a/lib/gitlab/user_access_snippet.rb b/lib/gitlab/user_access_snippet.rb index bfed86c4df4..dcd45f9350d 100644 --- a/lib/gitlab/user_access_snippet.rb +++ b/lib/gitlab/user_access_snippet.rb @@ -17,7 +17,14 @@ module Gitlab @project = snippet&.project end + def allowed? + return true if snippet_migration? + + super + end + def can_do_action?(action) + return true if snippet_migration? return false unless can_access_git? permission_cache[action] = @@ -35,7 +42,10 @@ module Gitlab end def can_push_to_branch?(ref) + return true if snippet_migration? + super + return false unless snippet return false unless can_do_action?(:update_snippet) @@ -45,5 +55,9 @@ module Gitlab def can_merge_to_branch?(ref) false end + + def snippet_migration? + user&.migration_bot? && snippet + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1f29126109d..b77364cc74b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1356,7 +1356,7 @@ msgstr "" msgid "Adds an issue to an epic." msgstr "" -msgid "Adjust your filters/search criteria above." +msgid "Adjust your filters/search criteria above. If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information." msgstr "" msgid "Admin Area" @@ -9790,6 +9790,9 @@ msgstr "" msgid "Geo|%{name} is scheduled for re-verify" msgstr "" +msgid "Geo|Adjust your filters/search criteria above. If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information." +msgstr "" + msgid "Geo|All" msgstr "" @@ -9913,6 +9916,9 @@ msgstr "" msgid "Geo|The node is currently %{minutes_behind} behind the primary node." msgstr "" +msgid "Geo|There are no %{replicable_type} to show" +msgstr "" + msgid "Geo|Tracking database entry will be removed. Are you sure?" msgstr "" @@ -11132,9 +11138,6 @@ msgstr "" msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}" msgstr "" -msgid "If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information." -msgstr "" - msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes." msgstr "" @@ -13798,9 +13801,6 @@ msgstr "" msgid "No %{providerTitle} repositories found" msgstr "" -msgid "No %{replicableType} match this filter" -msgstr "" - msgid "No Epic" msgstr "" @@ -20962,6 +20962,9 @@ msgstr "" msgid "The vulnerability is no longer detected. Verify the vulnerability has been remediated before changing its status." msgstr "" +msgid "There are no %{replicableType} to show" +msgstr "" + msgid "There are no GPG keys associated with this account." msgstr "" @@ -25821,6 +25824,9 @@ msgstr "" msgid "updated %{time_ago}" msgstr "" +msgid "uploads" +msgstr "" + msgid "user avatar" msgstr "" diff --git a/package.json b/package.json index 5b145853ac7..acac9f5f36b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.2-2", "@sentry/browser": "^5.10.2", - "@sourcegraph/code-host-integration": "0.0.37", + "@sourcegraph/code-host-integration": "0.0.46", "@toast-ui/editor": "^2.0.1", "@toast-ui/vue-editor": "^2.0.1", "apollo-cache-inmemory": "^1.6.3", diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js new file mode 100644 index 00000000000..1c3a6c545a0 --- /dev/null +++ b/spec/frontend/pipelines/header_component_spec.js @@ -0,0 +1,116 @@ +import { shallowMount } from '@vue/test-utils'; +import HeaderComponent from '~/pipelines/components/header_component.vue'; +import CiHeader from '~/vue_shared/components/header_ci_component.vue'; +import eventHub from '~/pipelines/event_hub'; +import { GlModal } from '@gitlab/ui'; + +describe('Pipeline details header', () => { + let wrapper; + let glModalDirective; + + const threeWeeksAgo = new Date(); + threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + + const findDeleteModal = () => wrapper.find(GlModal); + + const defaultProps = { + pipeline: { + details: { + status: { + group: 'failed', + icon: 'status_failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + }, + id: 123, + created_at: threeWeeksAgo.toISOString(), + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + retry_path: 'retry', + cancel_path: 'cancel', + delete_path: 'delete', + }, + isLoading: false, + }; + + const createComponent = (props = {}) => { + glModalDirective = jest.fn(); + + wrapper = shallowMount(HeaderComponent, { + propsData: { + ...props, + }, + directives: { + glModal: { + bind(el, { value }) { + glModalDirective(value); + }, + }, + }, + }); + }; + + beforeEach(() => { + jest.spyOn(eventHub, '$emit'); + + createComponent(defaultProps); + }); + + afterEach(() => { + eventHub.$off(); + + wrapper.destroy(); + wrapper = null; + }); + + it('should render provided pipeline info', () => { + expect(wrapper.find(CiHeader).props()).toMatchObject({ + status: defaultProps.pipeline.details.status, + itemId: defaultProps.pipeline.id, + time: defaultProps.pipeline.created_at, + user: defaultProps.pipeline.user, + }); + }); + + describe('action buttons', () => { + it('should not trigger eventHub when nothing happens', () => { + expect(eventHub.$emit).not.toHaveBeenCalled(); + }); + + it('should call postAction when retry button action is clicked', () => { + wrapper.find('.js-retry-button').vm.$emit('click'); + + expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry'); + }); + + it('should call postAction when cancel button action is clicked', () => { + wrapper.find('.js-btn-cancel-pipeline').vm.$emit('click'); + + expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel'); + }); + + it('does not show delete modal', () => { + expect(findDeleteModal()).not.toBeVisible(); + }); + + describe('when delete button action is clicked', () => { + it('displays delete modal', () => { + expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID); + expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID); + }); + + it('should call delete when modal is submitted', () => { + findDeleteModal().vm.$emit('ok'); + + expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete'); + }); + }); + }); +}); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js new file mode 100644 index 00000000000..5e8d21660de --- /dev/null +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -0,0 +1,142 @@ +import { shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'spec/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import PipelinesActions from '~/pipelines/components/pipelines_actions.vue'; +import { GlDeprecatedButton } from '@gitlab/ui'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('Pipelines Actions dropdown', () => { + let wrapper; + let mock; + + const createComponent = (actions = []) => { + wrapper = shallowMount(PipelinesActions, { + propsData: { + actions, + }, + }); + }; + + const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedButton); + const findAllCountdowns = () => wrapper.findAll(GlCountdown); + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + mock.restore(); + }); + + describe('manual actions', () => { + const mockActions = [ + { + name: 'stop_review', + path: `${TEST_HOST}/root/review-app/builds/1893/play`, + }, + { + name: 'foo', + path: `${TEST_HOST}/disabled/pipeline/action`, + playable: false, + }, + ]; + + beforeEach(() => { + createComponent(mockActions); + }); + + it('renders a dropdown with the provided actions', () => { + expect(findAllDropdownItems()).toHaveLength(mockActions.length); + }); + + it("renders a disabled action when it's not playable", () => { + expect( + findAllDropdownItems() + .at(1) + .attributes('disabled'), + ).toBe('true'); + }); + + describe('on click', () => { + it('makes a request and toggles the loading state', () => { + mock.onPost(mockActions.path).reply(200); + + wrapper.find(GlDeprecatedButton).vm.$emit('click'); + + expect(wrapper.vm.isLoading).toBe(true); + + return waitForPromises().then(() => { + expect(wrapper.vm.isLoading).toBe(false); + }); + }); + }); + }); + + describe('scheduled jobs', () => { + const scheduledJobAction = { + name: 'scheduled action', + path: `${TEST_HOST}/scheduled/job/action`, + playable: true, + scheduled_at: '2063-04-05T00:42:00Z', + }; + const expiredJobAction = { + name: 'expired action', + path: `${TEST_HOST}/expired/job/action`, + playable: true, + scheduled_at: '2018-10-05T08:23:00Z', + }; + + beforeEach(() => { + jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime()); + createComponent([scheduledJobAction, expiredJobAction]); + }); + + it('makes post request after confirming', () => { + mock.onPost(scheduledJobAction.path).reply(200); + jest.spyOn(window, 'confirm').mockReturnValue(true); + + findAllDropdownItems() + .at(0) + .vm.$emit('click'); + + expect(window.confirm).toHaveBeenCalled(); + + return waitForPromises().then(() => { + expect(mock.history.post.length).toBe(1); + }); + }); + + it('does not make post request if confirmation is cancelled', () => { + mock.onPost(scheduledJobAction.path).reply(200); + jest.spyOn(window, 'confirm').mockReturnValue(false); + + findAllDropdownItems() + .at(0) + .vm.$emit('click'); + + expect(window.confirm).toHaveBeenCalled(); + expect(mock.history.post.length).toBe(0); + }); + + it('displays the remaining time in the dropdown', () => { + expect( + findAllCountdowns() + .at(0) + .props('endDateString'), + ).toBe(scheduledJobAction.scheduled_at); + }); + + it('displays 00:00:00 for expired jobs in the dropdown', () => { + expect( + findAllCountdowns() + .at(1) + .props('endDateString'), + ).toBe(expiredJobAction.scheduled_at); + }); + }); +}); diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js deleted file mode 100644 index 9043f30397d..00000000000 --- a/spec/javascripts/pipelines/header_component_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import Vue from 'vue'; -import headerComponent from '~/pipelines/components/header_component.vue'; -import eventHub from '~/pipelines/event_hub'; - -describe('Pipeline details header', () => { - let HeaderComponent; - let vm; - let props; - - beforeEach(() => { - spyOn(eventHub, '$emit'); - HeaderComponent = Vue.extend(headerComponent); - - const threeWeeksAgo = new Date(); - threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); - - props = { - pipeline: { - details: { - status: { - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - details_path: 'path', - }, - }, - id: 123, - created_at: threeWeeksAgo.toISOString(), - user: { - web_url: 'path', - name: 'Foo', - username: 'foobar', - email: 'foo@bar.com', - avatar_url: 'link', - }, - retry_path: 'retry', - cancel_path: 'cancel', - delete_path: 'delete', - }, - isLoading: false, - }; - - vm = new HeaderComponent({ propsData: props }).$mount(); - }); - - afterEach(() => { - eventHub.$off(); - vm.$destroy(); - }); - - const findDeleteModal = () => document.getElementById(headerComponent.DELETE_MODAL_ID); - const findDeleteModalSubmit = () => - [...findDeleteModal().querySelectorAll('.btn')].find(x => x.textContent === 'Delete pipeline'); - - it('should render provided pipeline info', () => { - expect( - vm.$el - .querySelector('.header-main-content') - .textContent.replace(/\s+/g, ' ') - .trim(), - ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo'); - }); - - describe('action buttons', () => { - it('should not trigger eventHub when nothing happens', () => { - expect(eventHub.$emit).not.toHaveBeenCalled(); - }); - - it('should call postAction when retry button action is clicked', () => { - vm.$el.querySelector('.js-retry-button').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry'); - }); - - it('should call postAction when cancel button action is clicked', () => { - vm.$el.querySelector('.js-btn-cancel-pipeline').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel'); - }); - - it('does not show delete modal', () => { - expect(findDeleteModal()).not.toBeVisible(); - }); - - describe('when delete button action is clicked', () => { - beforeEach(done => { - vm.$el.querySelector('.js-btn-delete-pipeline').click(); - - // Modal needs two ticks to show - vm.$nextTick() - .then(() => vm.$nextTick()) - .then(done) - .catch(done.fail); - }); - - it('should show delete modal', () => { - expect(findDeleteModal()).toBeVisible(); - }); - - it('should call delete when modal is submitted', () => { - findDeleteModalSubmit().click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete'); - }); - }); - }); -}); diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js deleted file mode 100644 index 91f7d2167cc..00000000000 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ /dev/null @@ -1,128 +0,0 @@ -import Vue from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { TEST_HOST } from 'spec/test_constants'; -import axios from '~/lib/utils/axios_utils'; -import PipelinesActions from '~/pipelines/components/pipelines_actions.vue'; - -describe('Pipelines Actions dropdown', () => { - const Component = Vue.extend(PipelinesActions); - let vm; - let mock; - - afterEach(() => { - vm.$destroy(); - mock.restore(); - }); - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - describe('manual actions', () => { - const actions = [ - { - name: 'stop_review', - path: `${TEST_HOST}/root/review-app/builds/1893/play`, - }, - { - name: 'foo', - path: `${TEST_HOST}/disabled/pipeline/action`, - playable: false, - }, - ]; - - beforeEach(() => { - vm = mountComponent(Component, { actions }); - }); - - it('renders a dropdown with the provided actions', () => { - const dropdownItems = vm.$el.querySelectorAll('.dropdown-menu li'); - - expect(dropdownItems.length).toEqual(actions.length); - }); - - it("renders a disabled action when it's not playable", () => { - const dropdownItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); - - expect(dropdownItem).toBeDisabled(); - }); - - describe('on click', () => { - it('makes a request and toggles the loading state', done => { - mock.onPost(actions.path).reply(200); - - vm.$el.querySelector('.dropdown-menu li button').click(); - - expect(vm.isLoading).toEqual(true); - - setTimeout(() => { - expect(vm.isLoading).toEqual(false); - - done(); - }); - }); - }); - }); - - describe('scheduled jobs', () => { - const scheduledJobAction = { - name: 'scheduled action', - path: `${TEST_HOST}/scheduled/job/action`, - playable: true, - scheduled_at: '2063-04-05T00:42:00Z', - }; - const expiredJobAction = { - name: 'expired action', - path: `${TEST_HOST}/expired/job/action`, - playable: true, - scheduled_at: '2018-10-05T08:23:00Z', - }; - const findDropdownItem = action => { - const buttons = vm.$el.querySelectorAll('.dropdown-menu li button'); - return Array.prototype.find.call(buttons, element => - element.innerText.trim().startsWith(action.name), - ); - }; - - beforeEach(done => { - spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); - vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] }); - - Vue.nextTick() - .then(done) - .catch(done.fail); - }); - - it('makes post request after confirming', done => { - mock.onPost(scheduledJobAction.path).reply(200); - spyOn(window, 'confirm').and.callFake(() => true); - - findDropdownItem(scheduledJobAction).click(); - - expect(window.confirm).toHaveBeenCalled(); - setTimeout(() => { - expect(mock.history.post.length).toBe(1); - done(); - }); - }); - - it('does not make post request if confirmation is cancelled', () => { - mock.onPost(scheduledJobAction.path).reply(200); - spyOn(window, 'confirm').and.callFake(() => false); - - findDropdownItem(scheduledJobAction).click(); - - expect(window.confirm).toHaveBeenCalled(); - expect(mock.history.post.length).toBe(0); - }); - - it('displays the remaining time in the dropdown', () => { - expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00'); - }); - - it('displays 00:00:00 for expired jobs in the dropdown', () => { - expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00'); - }); - }); -}); diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index 94ef094e72c..dcbf8d12f35 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s confirmed_at: 1.day.ago) end - let!(:admin) { users.create(id: 2, email: 'admin@example.com', projects_limit: 10, username: 'admin', name: 'Admin', admin: true, state: 'active') } + let(:migration_bot) { User.migration_bot } let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } @@ -88,34 +88,34 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s end context 'when author cannot update snippet or use git' do - shared_examples 'admin user commits files' do + shared_examples 'migration_bot user commits files' do it do subject last_commit = raw_repository(snippet).commit - expect(last_commit.author_name).to eq admin.name - expect(last_commit.author_email).to eq admin.email + expect(last_commit.author_name).to eq migration_bot.name + expect(last_commit.author_email).to eq migration_bot.email end end context 'when user is blocked' do let(:user_state) { 'blocked' } - it_behaves_like 'admin user commits files' + it_behaves_like 'migration_bot user commits files' end context 'when user is deactivated' do let(:user_state) { 'deactivated' } - it_behaves_like 'admin user commits files' + it_behaves_like 'migration_bot user commits files' end context 'when user is a ghost' do let(:ghost) { true } let(:user_type) { 'ghost' } - it_behaves_like 'admin user commits files' + it_behaves_like 'migration_bot user commits files' end end end diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb index 1c369d57e06..fb2a7d16665 100644 --- a/spec/lib/gitlab/git_access_snippet_spec.rb +++ b/spec/lib/gitlab/git_access_snippet_spec.rb @@ -11,8 +11,9 @@ describe Gitlab::GitAccessSnippet do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public) } let_it_be(:snippet) { create(:project_snippet, :public, :repository, project: project) } - let(:repository) { snippet.repository } + let_it_be(:migration_bot) { User.migration_bot } + let(:repository) { snippet.repository } let(:actor) { user } let(:protocol) { 'ssh' } let(:changes) { Gitlab::GitAccess::ANY } @@ -27,10 +28,22 @@ describe Gitlab::GitAccessSnippet do let(:actor) { build(:deploy_key) } it 'does not allow push and pull access' do + expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism]) expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism]) end end + shared_examples 'actor is migration bot' do + context 'when user is the migration bot' do + let(:user) { migration_bot } + + it 'can perform git operations' do + expect { push_access_check }.not_to raise_error + expect { pull_access_check }.not_to raise_error + end + end + end + describe '#check_snippet_accessibility!' do context 'when the snippet exists' do it 'allows access' do @@ -77,6 +90,12 @@ describe Gitlab::GitAccessSnippet do expect { push_access_check }.not_to raise_error expect { pull_access_check }.not_to raise_error end + + it_behaves_like 'actor is migration bot' do + before do + expect(migration_bot.required_terms_not_accepted?).to be_truthy + end + end end context 'project snippet accessibility', :aggregate_failures do @@ -107,6 +126,7 @@ describe Gitlab::GitAccessSnippet do context 'when project is public' do it_behaves_like 'checks accessibility' + it_behaves_like 'actor is migration bot' end context 'when project is public but snippet feature is private' do @@ -117,6 +137,7 @@ describe Gitlab::GitAccessSnippet do end it_behaves_like 'checks accessibility' + it_behaves_like 'actor is migration bot' end context 'when project is not accessible' do @@ -127,11 +148,58 @@ describe Gitlab::GitAccessSnippet do let(:membership) { membership } it 'respects accessibility' do - expect { push_access_check }.to raise_error(described_class::NotFoundError) - expect { pull_access_check }.to raise_error(described_class::NotFoundError) + expect { push_access_check }.to raise_snippet_not_found + expect { pull_access_check }.to raise_snippet_not_found + end + end + end + + it_behaves_like 'actor is migration bot' + end + + context 'when project is archived' do + let(:project) { create(:project, :public, :archived) } + + [:anonymous, :non_member].each do |membership| + context membership.to_s do + let(:membership) { membership } + + it 'cannot perform git operations' do + expect { push_access_check }.to raise_error(described_class::ForbiddenError) + expect { pull_access_check }.to raise_error(described_class::ForbiddenError) + end + end + end + + [:guest, :reporter, :maintainer, :author, :admin].each do |membership| + context membership.to_s do + let(:membership) { membership } + + it 'cannot perform git pushes' do + expect { push_access_check }.to raise_error(described_class::ForbiddenError) + expect { pull_access_check }.not_to raise_error + end + end + end + + it_behaves_like 'actor is migration bot' + end + + context 'when snippet feature is disabled' do + let(:project) { create(:project, :public, :snippets_disabled) } + + [:anonymous, :non_member, :author, :admin].each do |membership| + context membership.to_s do + let(:membership) { membership } + + it 'cannot perform git operations' do + expect { push_access_check }.to raise_error(described_class::ForbiddenError) + expect { pull_access_check }.to raise_error(described_class::ForbiddenError) end end end + + it_behaves_like 'actor is migration bot' end end @@ -159,6 +227,8 @@ describe Gitlab::GitAccessSnippet do expect { pull_access_check }.to raise_error(error_class) end end + + it_behaves_like 'actor is migration bot' end end @@ -166,36 +236,56 @@ describe Gitlab::GitAccessSnippet do let(:user) { snippet.author } let!(:primary_node) { FactoryBot.create(:geo_node, :primary) } - # Without override, push access would return Gitlab::GitAccessResult::CustomAction - it 'skips geo for snippet' do + before do allow(::Gitlab::Database).to receive(:read_only?).and_return(true) allow(::Gitlab::Geo).to receive(:secondary_with_primary?).and_return(true) + end + # Without override, push access would return Gitlab::GitAccessResult::CustomAction + it 'skips geo for snippet' do expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/) end + + context 'when user is migration bot' do + let(:user) { migration_bot } + + it 'skips geo for snippet' do + expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/) + end + end end context 'when changes are specific' do let(:changes) { "2d1db523e11e777e49377cfb22d368deec3f0793 ddd0f15ae83993f5cb66a927a28673882e99100b master" } let(:user) { snippet.author } - it 'does not raise error if SnippetCheck does not raise error' do - expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check| - expect(check).to receive(:validate!).and_call_original - end - expect_next_instance_of(Gitlab::Checks::PushFileCountCheck) do |check| - expect(check).to receive(:validate!) + shared_examples 'snippet checks' do + it 'does not raise error if SnippetCheck does not raise error' do + expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check| + expect(check).to receive(:validate!).and_call_original + end + expect_next_instance_of(Gitlab::Checks::PushFileCountCheck) do |check| + expect(check).to receive(:validate!) + end + + expect { push_access_check }.not_to raise_error end - expect { push_access_check }.not_to raise_error - end + it 'raises error if SnippetCheck raises error' do + expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check| + allow(check).to receive(:validate!).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo') + end - it 'raises error if SnippetCheck raises error' do - expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check| - allow(check).to receive(:validate!).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo') + expect { push_access_check }.to raise_forbidden('foo') end + end - expect { push_access_check }.to raise_forbidden('foo') + it_behaves_like 'snippet checks' + + context 'when user is migration bot' do + let(:user) { migration_bot } + + it_behaves_like 'snippet checks' end end @@ -260,6 +350,14 @@ describe Gitlab::GitAccessSnippet do it_behaves_like 'a push to repository already over the limit' it_behaves_like 'a push to repository below the limit' it_behaves_like 'a push to repository to make it over the limit' + + context 'when user is migration bot' do + let(:actor) { migration_bot } + + it_behaves_like 'a push to repository already over the limit' + it_behaves_like 'a push to repository below the limit' + it_behaves_like 'a push to repository to make it over the limit' + end end context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is not set' do @@ -274,6 +372,14 @@ describe Gitlab::GitAccessSnippet do it_behaves_like 'a push to repository already over the limit' it_behaves_like 'a push to repository below the limit' it_behaves_like 'a push to repository to make it over the limit' + + context 'when user is migration bot' do + let(:actor) { migration_bot } + + it_behaves_like 'a push to repository already over the limit' + it_behaves_like 'a push to repository below the limit' + it_behaves_like 'a push to repository to make it over the limit' + end end end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index 9d8ec2d9b21..8c4071a7ed1 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -19,19 +19,20 @@ describe Gitlab::Metrics::Samplers::RubySampler do end describe '#sample' do - it 'adds a metric containing the process resident memory bytes' do - expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000) - - expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000) + it 'samples various statistics' do + expect(Gitlab::Metrics::System).to receive(:cpu_time) + expect(Gitlab::Metrics::System).to receive(:file_descriptor_count) + expect(Gitlab::Metrics::System).to receive(:memory_usage) + expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors) + expect(sampler).to receive(:sample_gc) sampler.sample end - it 'adds a metric containing the process unique and proportional memory bytes' do - expect(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return(uss: 9000, pss: 10_000) + it 'adds a metric containing the process resident memory bytes' do + expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000) - expect(sampler.metrics[:process_unique_memory_bytes]).to receive(:set).with({}, 9000) - expect(sampler.metrics[:process_proportional_memory_bytes]).to receive(:set).with({}, 10_000) + expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000) sampler.sample end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index 37d26bd9d63..a5aa80686fd 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -3,122 +3,33 @@ require 'spec_helper' describe Gitlab::Metrics::System do - context 'when /proc files exist' do - # Fixtures pulled from: - # Linux carbon 5.3.0-7648-generic #41~1586789791~19.10~9593806-Ubuntu SMP Mon Apr 13 17:50:40 UTC x86_64 x86_64 x86_64 GNU/Linux - let(:proc_status) do - # most rows omitted for brevity - <<~SNIP - Name: less - VmHWM: 2468 kB - VmRSS: 2468 kB - RssAnon: 260 kB - SNIP - end - - let(:proc_smaps_rollup) do - # full snapshot - <<~SNIP - Rss: 2564 kB - Pss: 503 kB - Pss_Anon: 312 kB - Pss_File: 191 kB - Pss_Shmem: 0 kB - Shared_Clean: 2100 kB - Shared_Dirty: 0 kB - Private_Clean: 152 kB - Private_Dirty: 312 kB - Referenced: 2564 kB - Anonymous: 312 kB - LazyFree: 0 kB - AnonHugePages: 0 kB - ShmemPmdMapped: 0 kB - Shared_Hugetlb: 0 kB - Private_Hugetlb: 0 kB - Swap: 0 kB - SwapPss: 0 kB - Locked: 0 kB - SNIP - end - - let(:proc_limits) do - # full snapshot - <<~SNIP - Limit Soft Limit Hard Limit Units - Max cpu time unlimited unlimited seconds - Max file size unlimited unlimited bytes - Max data size unlimited unlimited bytes - Max stack size 8388608 unlimited bytes - Max core file size 0 unlimited bytes - Max resident set unlimited unlimited bytes - Max processes 126519 126519 processes - Max open files 1024 1048576 files - Max locked memory 67108864 67108864 bytes - Max address space unlimited unlimited bytes - Max file locks unlimited unlimited locks - Max pending signals 126519 126519 signals - Max msgqueue size 819200 819200 bytes - Max nice priority 0 0 - Max realtime priority 0 0 - Max realtime timeout unlimited unlimited us - SNIP - end - + if File.exist?('/proc') describe '.memory_usage' do - it "returns the process' resident set size (RSS) in bytes" do - mock_existing_proc_file('/proc/self/status', proc_status) - - expect(described_class.memory_usage).to eq(2527232) + it "returns the process' memory usage in bytes" do + expect(described_class.memory_usage).to be > 0 end end describe '.file_descriptor_count' do it 'returns the amount of open file descriptors' do - expect(Dir).to receive(:glob).and_return(['/some/path', '/some/other/path']) - - expect(described_class.file_descriptor_count).to eq(2) + expect(described_class.file_descriptor_count).to be > 0 end end describe '.max_open_file_descriptors' do it 'returns the max allowed open file descriptors' do - mock_existing_proc_file('/proc/self/limits', proc_limits) - - expect(described_class.max_open_file_descriptors).to eq(1024) - end - end - - describe '.memory_usage_uss_pss' do - it "returns the process' unique and porportional set size (USS/PSS) in bytes" do - mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup) - - # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024 - expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072) + expect(described_class.max_open_file_descriptors).to be > 0 end end - end - - context 'when /proc files do not exist' do - before do - mock_missing_proc_file - end - + else describe '.memory_usage' do - it 'returns 0' do - expect(described_class.memory_usage).to eq(0) - end - end - - describe '.memory_usage_uss_pss' do - it "returns 0 for all components" do - expect(described_class.memory_usage_uss_pss).to eq(uss: 0, pss: 0) + it 'returns 0.0' do + expect(described_class.memory_usage).to eq(0.0) end end describe '.file_descriptor_count' do it 'returns 0' do - expect(Dir).to receive(:glob).and_return([]) - expect(described_class.file_descriptor_count).to eq(0) end end @@ -187,12 +98,4 @@ describe Gitlab::Metrics::System do expect(described_class.thread_cpu_duration(start_time)).to be_nil end end - - def mock_existing_proc_file(path, content) - allow(File).to receive(:foreach).with(path) { |_path, &block| content.each_line(&block) } - end - - def mock_missing_proc_file - allow(File).to receive(:foreach).and_raise(Errno::ENOENT) - end end diff --git a/spec/lib/gitlab/user_access_snippet_spec.rb b/spec/lib/gitlab/user_access_snippet_spec.rb index 57e52e2e93d..2e8a0a49a76 100644 --- a/spec/lib/gitlab/user_access_snippet_spec.rb +++ b/spec/lib/gitlab/user_access_snippet_spec.rb @@ -7,6 +7,8 @@ describe Gitlab::UserAccessSnippet do let_it_be(:project) { create(:project, :private) } let_it_be(:snippet) { create(:project_snippet, :private, project: project) } + let_it_be(:migration_bot) { User.migration_bot } + let(:user) { create(:user) } describe '#can_do_action?' do @@ -36,6 +38,14 @@ describe Gitlab::UserAccessSnippet do expect(access.can_do_action?(:ability)).to eq(false) end end + + context 'when user is migration bot' do + let(:user) { migration_bot } + + it 'allows access' do + expect(access.can_do_action?(:ability)).to eq(true) + end + end end describe '#can_push_to_branch?' do @@ -65,6 +75,16 @@ describe Gitlab::UserAccessSnippet do end end + context 'when user is migration bot' do + let(:user) { migration_bot } + + it 'allows access' do + allow(Ability).to receive(:allowed?).and_return(false) + + expect(access.can_push_to_branch?('random_branch')).to eq(true) + end + end + context 'when snippet is nil' do let(:user) { create_user_from_membership(project, :admin) } let(:snippet) { nil } @@ -72,6 +92,14 @@ describe Gitlab::UserAccessSnippet do it 'disallows access' do expect(access.can_push_to_branch?('random_branch')).to eq(false) end + + context 'when user is migration bot' do + let(:user) { migration_bot } + + it 'disallows access' do + expect(access.can_push_to_branch?('random_branch')).to eq(false) + end + end end end @@ -79,17 +107,41 @@ describe Gitlab::UserAccessSnippet do it 'returns false' do expect(access.can_create_tag?('random_tag')).to be_falsey end + + context 'when user is migration bot' do + let(:user) { migration_bot } + + it 'returns false' do + expect(access.can_create_tag?('random_tag')).to be_falsey + end + end end describe '#can_delete_branch?' do it 'returns false' do expect(access.can_delete_branch?('random_branch')).to be_falsey end + + context 'when user is migration bot' do + let(:user) { migration_bot } + + it 'returns false' do + expect(access.can_delete_branch?('random_branch')).to be_falsey + end + end end describe '#can_merge_to_branch?' do it 'returns false' do expect(access.can_merge_to_branch?('random_branch')).to be_falsey end + + context 'when user is migration bot' do + let(:user) { migration_bot } + + it 'returns false' do + expect(access.can_merge_to_branch?('random_branch')).to be_falsey + end + end end end diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb index c4af829a5e2..8fe3f27c8b1 100644 --- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb +++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb @@ -4,18 +4,27 @@ require 'spec_helper' describe UpdateHeadPipelineForMergeRequestWorker do describe '#perform' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:merge_request) { create(:merge_request, source_project: project) } - let(:latest_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:latest_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } context 'when pipeline exists for the source project and branch' do - before do - create(:ci_empty_pipeline, project: project, ref: merge_request.source_branch, sha: latest_sha) - end + let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project, ref: merge_request.source_branch, sha: latest_sha) } it 'updates the head_pipeline_id of the merge_request' do - expect { subject.perform(merge_request.id) }.to change { merge_request.reload.head_pipeline_id } + expect { subject.perform(merge_request.id) } + .to change { merge_request.reload.head_pipeline_id }.from(nil).to(pipeline.id) + end + + it_behaves_like 'an idempotent worker' do + let(:job_args) { merge_request.id } + + it 'sets the pipeline as the head pipeline when run multiple times' do + subject + + expect(merge_request.reload.head_pipeline_id).to eq(pipeline.id) + end end context 'when merge request sha does not equal pipeline sha' do @@ -27,6 +36,15 @@ describe UpdateHeadPipelineForMergeRequestWorker do expect { subject.perform(merge_request.id) } .not_to change { merge_request.reload.head_pipeline_id } end + + it_behaves_like 'an idempotent worker' do + let(:job_args) { merge_request.id } + + it 'does not update the head_pipeline_id when run multiple times' do + expect { subject } + .not_to change { merge_request.reload.head_pipeline_id } + end + end end end @@ -35,10 +53,19 @@ describe UpdateHeadPipelineForMergeRequestWorker do expect { subject.perform(merge_request.id) } .not_to change { merge_request.reload.head_pipeline_id } end + + it_behaves_like 'an idempotent worker' do + let(:job_args) { merge_request.id } + + it 'does not update the head_pipeline_id when run multiple times' do + expect { subject } + .not_to change { merge_request.reload.head_pipeline_id } + end + end end context 'when a merge request pipeline exists' do - let!(:merge_request_pipeline) do + let_it_be(:merge_request_pipeline) do create(:ci_pipeline, project: project, source: :merge_request_event, @@ -52,6 +79,16 @@ describe UpdateHeadPipelineForMergeRequestWorker do .from(nil).to(merge_request_pipeline.id) end + it_behaves_like 'an idempotent worker' do + let(:job_args) { merge_request.id } + + it 'sets the merge request pipeline as the head pipeline when run multiple times' do + subject + + expect(merge_request.reload.head_pipeline_id).to eq(merge_request_pipeline.id) + end + end + context 'when branch pipeline exists' do let!(:branch_pipeline) do create(:ci_pipeline, project: project, source: :push, sha: latest_sha) @@ -62,6 +99,16 @@ describe UpdateHeadPipelineForMergeRequestWorker do .to change { merge_request.reload.head_pipeline_id } .from(nil).to(merge_request_pipeline.id) end + + it_behaves_like 'an idempotent worker' do + let(:job_args) { merge_request.id } + + it 'sets the merge request pipeline as the head pipeline when run multiple times' do + subject + + expect(merge_request.reload.head_pipeline_id).to eq(merge_request_pipeline.id) + end + end end end end diff --git a/yarn.lock b/yarn.lock index bbbe8a2eb71..859fc145342 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1040,10 +1040,10 @@ "@sentry/types" "5.10.0" tslib "^1.9.3" -"@sourcegraph/code-host-integration@0.0.37": - version "0.0.37" - resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.37.tgz#87f9a602e2a60520b6038311a67face2ece86827" - integrity sha512-GQvNuPORLjsMhto57Ue1umeSV3cir+hMEaGxwCKmmq+cc9ZSZpuXa8RVBXuT5azN99K9/8zFps4woyPJ8wrjYA== +"@sourcegraph/code-host-integration@0.0.46": + version "0.0.46" + resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.46.tgz#05e4cda671ed00450be12461e6a3caff473675aa" + integrity sha512-ghzfaV5ydSWTamLPIDLl5tRvTtM2MBDRmQbWTPg9ZoCP/eHk61uCTAFEq02lsefDQHISZHldeClqRYhZvGDZfw== "@toast-ui/editor@^2.0.1": version "2.0.1" |