Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									96fb7f03bd
								
							
						
					
					
						commit
						3a5eccd3d0
					
				|  | @ -1067,11 +1067,7 @@ | |||
|           "type": "string", | ||||
|           "markdownDescription": "Determines the strategy for downloading and updating the cache. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachepolicy)", | ||||
|           "default": "pull-push", | ||||
|           "enum": [ | ||||
|             "pull", | ||||
|             "push", | ||||
|             "pull-push" | ||||
|           ] | ||||
|           "pattern": "pull-push|pull|push|\\$\\w{1,255}" | ||||
|         }, | ||||
|         "unprotect": { | ||||
|           "type": "boolean", | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic | |||
| 
 | ||||
|   around_action :allow_gitaly_ref_name_caching | ||||
| 
 | ||||
|   after_action :track_viewed_diffs_events, only: [:diffs_batch] | ||||
|   after_action :track_viewed_diffs_events, only: [:diffs_batch, :diff_for_path] | ||||
| 
 | ||||
|   urgency :low, [ | ||||
|     :show, | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ class PopulateReleasesAccessLevelFromRepository < Gitlab::Database::Migration[2. | |||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     update_column_in_batches( | ||||
|     update_column_in_batches( # rubocop: disable Migration/UpdateColumnInBatches | ||||
|       :project_features, | ||||
|       :releases_access_level, | ||||
|       Arel.sql('repository_access_level') | ||||
|  |  | |||
|  | @ -261,6 +261,39 @@ cache: | |||
|   key: $CI_JOB_NAME | ||||
| ``` | ||||
| 
 | ||||
| ### Use a variable to control a job's cache policy | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371480) in GitLab 16.1. | ||||
| 
 | ||||
| To reduce duplication of jobs where the only difference is the pull policy, you can use a [CI/CD variable](../variables/index.md). | ||||
| 
 | ||||
| For example: | ||||
| 
 | ||||
| ```yaml | ||||
| conditional-policy: | ||||
|   rules: | ||||
|     - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | ||||
|       variables: | ||||
|         POLICY: pull-push | ||||
|     - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH | ||||
|       variables: | ||||
|         POLICY: pull | ||||
|   stage: build | ||||
|   cache: | ||||
|     key: gems | ||||
|     policy: $POLICY | ||||
|     paths: | ||||
|       - vendor/bundle | ||||
|   script: | ||||
|     - echo "This job pulls and pushes the cache depending on the branch" | ||||
|     - echo "Downloading dependencies..." | ||||
| ``` | ||||
| 
 | ||||
| In this example, the job's cache policy is: | ||||
| 
 | ||||
| - `pull-push` for changes to the default branch. | ||||
| - `pull` for changes to other branches. | ||||
| 
 | ||||
| ### Cache Node.js dependencies | ||||
| 
 | ||||
| If your project uses [npm](https://www.npmjs.com/) to install Node.js | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ There are two places defined variables can be used. On the: | |||
| | [`artifacts:name`](../yaml/index.md#artifactsname)                    | yes              | Runner                 | The variable expansion is made by GitLab Runner's shell environment. | | ||||
| | [`before_script`](../yaml/index.md#before_script)                     | yes              | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment) | | ||||
| | [`cache:key`](../yaml/index.md#cachekey)                              | yes              | Runner                 | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). | | ||||
| | [`cache:policy`](../yaml/index.md#cachepolicy)                        | yes              | Runner                 | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). | | ||||
| | [`environment:name`](../yaml/index.md#environmentname)                | yes              | GitLab                 | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- `CI_ENVIRONMENT_*` variables.<br/>- [Persisted variables](#persisted-variables). | | ||||
| | [`environment:url`](../yaml/index.md#environmenturl)                  | yes              | GitLab                 | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab.<br/><br/>Supported are all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules).<br/><br/>Not supported are variables defined in the GitLab Runner `config.toml` and variables created in the job's `script`. | | ||||
| | [`environment:auto_stop_in`](../yaml/index.md#environmentauto_stop_in)| yes              | GitLab                 | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab.<br/><br/> The value of the variable being substituted should be a period of time in a human readable natural language form. See [possible inputs](../yaml/index.md#environmentauto_stop_in) for more information.| | ||||
|  |  | |||
|  | @ -1453,6 +1453,7 @@ Must be used with `cache: paths`, or nothing is cached. | |||
| - `pull` | ||||
| - `push` | ||||
| - `pull-push` (default) | ||||
| - [CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file). | ||||
| 
 | ||||
| **Example of `cache:policy`**: | ||||
| 
 | ||||
|  | @ -1480,6 +1481,10 @@ faster-test-job: | |||
|     - echo "Running tests..." | ||||
| ``` | ||||
| 
 | ||||
| **Related topics**: | ||||
| 
 | ||||
| - You can [use a variable to control a job's cache policy](../caching/index.md#use-a-variable-to-control-a-jobs-cache-policy). | ||||
| 
 | ||||
| #### `cache:fallback_keys` | ||||
| 
 | ||||
| Use `cache:fallback_keys` to specify a list of keys to try to restore cache from | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ Because of the many registration paths and multiple verification stages, identit | |||
| 
 | ||||
| Before you enable these features, ensure [hard email confirmation](../security/user_email_confirmation.md) is enabled and [Arkose](../integration/arkose.md#configuration) is configured properly. | ||||
| 
 | ||||
| 
 | ||||
| | Feature flag name | Description | | ||||
| |---------|-------------| | ||||
| | `identity_verification` | Turns on email verification for all registration paths | | ||||
|  |  | |||
|  | @ -300,6 +300,40 @@ All UI strings should be prepared for translation by following our [internationa | |||
| 
 | ||||
| The strings should use the integration name as [namespace](../i18n/externalization.md#namespaces), for example, `s_('FooBarIntegration|My string')`. | ||||
| 
 | ||||
| ## Deprecate and remove an integration | ||||
| 
 | ||||
| To remove an integration, you must first deprecate the integration. For more information, | ||||
| see the [feature deprecation guidelines](../../development/deprecation_guidelines/index.md). | ||||
| 
 | ||||
| ### Deprecate an integration | ||||
| 
 | ||||
| You must announce any deprecation [no later than the third milestone preceding intended removal](../../development/deprecation_guidelines/index.md#when-can-a-feature-be-deprecated). | ||||
| To deprecate an integration: | ||||
| 
 | ||||
| - [Add a deprecation entry](../../development/deprecation_guidelines/index.md#update-the-deprecations-and-removals-documentation-pages). | ||||
| - [Mark the integration documentation as deprecated](../../development/documentation/versions.md#deprecate-a-page-or-topic). | ||||
| - Optional. To prevent any new project-level records from | ||||
|   being created, add the integration to `Project#disabled_integrations` (see [example merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114835)). | ||||
| 
 | ||||
| ### Remove an integration | ||||
| 
 | ||||
| To safely remove an integration, you must stage the removal across two milestones. | ||||
| 
 | ||||
| In the major milestone of intended removal (M.0), disable the integration and delete the records from the database: | ||||
| 
 | ||||
| - Remove the integration from `Integration::INTEGRATION_NAMES`. | ||||
| - Delete the integration model's `#execute` and `#test` methods (if defined), but keep the model. | ||||
| - Add a post-migration to delete the integration records from PostgreSQL (see [example merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114721)). | ||||
| - [Add a removal entry](../../development/deprecation_guidelines/index.md#update-the-deprecations-and-removals-documentation-pages). | ||||
| - [Mark the integration documentation as removed](../../development/documentation/versions.md#remove-a-page). | ||||
| - [Update the integration API documentation](../../api/integrations.md). | ||||
| 
 | ||||
| In the next minor release (M.1): | ||||
| 
 | ||||
| - Remove the integration's model and any remaining code. | ||||
| - Close any issues, merge requests, and epics that have the integration's label (`~Integration::<name>`). | ||||
| - Delete the integration's label (`~Integration::<name>`) from `gitlab-org`. | ||||
| 
 | ||||
| ## Ongoing migrations and refactorings | ||||
| 
 | ||||
| Developers should be aware that the Integrations team is in the process of | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ module Gitlab | |||
|           include ::Gitlab::Config::Entry::Attributable | ||||
| 
 | ||||
|           ALLOWED_KEYS = %i[key untracked paths when policy unprotect fallback_keys].freeze | ||||
|           ALLOWED_POLICY = %w[pull-push push pull].freeze | ||||
|           ALLOWED_POLICY = /pull-push|push|pull|\$\w{1,255}*/ | ||||
|           DEFAULT_POLICY = 'pull-push' | ||||
|           ALLOWED_WHEN = %w[on_success on_failure always].freeze | ||||
|           DEFAULT_WHEN = 'on_success' | ||||
|  | @ -18,9 +18,9 @@ module Gitlab | |||
| 
 | ||||
|           validations do | ||||
|             validates :config, type: Hash, allowed_keys: ALLOWED_KEYS | ||||
|             validates :policy, type: String, allow_blank: true, inclusion: { | ||||
|               in: ALLOWED_POLICY, | ||||
|               message: "should be one of: #{ALLOWED_POLICY.join(', ')}" | ||||
|             validates :policy, type: String, allow_blank: true, format: { | ||||
|               with: ALLOWED_POLICY, | ||||
|               message: "should be a variable or one of: pull-push, push, pull" | ||||
|             } | ||||
| 
 | ||||
|             with_options allow_nil: true do | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ code_quality: | |||
|   variables: | ||||
|     DOCKER_DRIVER: overlay2 | ||||
|     DOCKER_TLS_CERTDIR: "" | ||||
|     CODE_QUALITY_IMAGE_TAG: "0.94.0" | ||||
|     CODE_QUALITY_IMAGE_TAG: "0.96.0" | ||||
|     CODE_QUALITY_IMAGE: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/ci-cd/codequality:$CODE_QUALITY_IMAGE_TAG" | ||||
|   needs: [] | ||||
|   script: | ||||
|  |  | |||
|  | @ -29,6 +29,59 @@ RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   shared_examples 'diff tracking' do | ||||
|     it 'tracks mr_diffs event' do | ||||
|       expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|         .to receive(:track_mr_diffs_action) | ||||
|         .with(merge_request: merge_request) | ||||
| 
 | ||||
|       method_call | ||||
|     end | ||||
| 
 | ||||
|     context 'when DNT is enabled' do | ||||
|       before do | ||||
|         stub_do_not_track('1') | ||||
|       end | ||||
| 
 | ||||
|       it 'does not track any mr_diffs event' do | ||||
|         expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|           .not_to receive(:track_mr_diffs_action) | ||||
| 
 | ||||
|         expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|           .not_to receive(:track_mr_diffs_single_file_action) | ||||
| 
 | ||||
|         method_call | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when user has view_diffs_file_by_file set to false' do | ||||
|       before do | ||||
|         user.update!(view_diffs_file_by_file: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not track single_file_diffs events' do | ||||
|         expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|           .not_to receive(:track_mr_diffs_single_file_action) | ||||
| 
 | ||||
|         method_call | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when user has view_diffs_file_by_file set to true' do | ||||
|       before do | ||||
|         user.update!(view_diffs_file_by_file: true) | ||||
|       end | ||||
| 
 | ||||
|       it 'tracks single_file_diffs events' do | ||||
|         expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|           .to receive(:track_mr_diffs_single_file_action) | ||||
|           .with(merge_request: merge_request, user: user) | ||||
| 
 | ||||
|         method_call | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   shared_examples 'forked project with submodules' do | ||||
|     render_views | ||||
| 
 | ||||
|  | @ -327,6 +380,10 @@ RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code | |||
|     context 'when the merge request exists' do | ||||
|       context 'when the user can view the merge request' do | ||||
|         context 'when the path exists in the diff' do | ||||
|           include_examples 'diff tracking' do | ||||
|             let(:method_call) { diff_for_path(old_path: existing_path, new_path: existing_path) } | ||||
|           end | ||||
| 
 | ||||
|           it 'enables diff notes' do | ||||
|             diff_for_path(old_path: existing_path, new_path: existing_path) | ||||
| 
 | ||||
|  | @ -399,6 +456,10 @@ RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code | |||
|     end | ||||
| 
 | ||||
|     shared_examples_for 'successful request' do | ||||
|       include_examples 'diff tracking' do | ||||
|         let(:method_call) { subject } | ||||
|       end | ||||
| 
 | ||||
|       it 'returns success' do | ||||
|         subject | ||||
| 
 | ||||
|  | @ -414,57 +475,6 @@ RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code | |||
| 
 | ||||
|         subject | ||||
|       end | ||||
| 
 | ||||
|       it 'tracks mr_diffs event' do | ||||
|         expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|           .to receive(:track_mr_diffs_action) | ||||
|           .with(merge_request: merge_request) | ||||
| 
 | ||||
|         subject | ||||
|       end | ||||
| 
 | ||||
|       context 'when DNT is enabled' do | ||||
|         before do | ||||
|           stub_do_not_track('1') | ||||
|         end | ||||
| 
 | ||||
|         it 'does not track any mr_diffs event' do | ||||
|           expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|             .not_to receive(:track_mr_diffs_action) | ||||
| 
 | ||||
|           expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|             .not_to receive(:track_mr_diffs_single_file_action) | ||||
| 
 | ||||
|           subject | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when user has view_diffs_file_by_file set to false' do | ||||
|         before do | ||||
|           user.update!(view_diffs_file_by_file: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not track single_file_diffs events' do | ||||
|           expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|             .not_to receive(:track_mr_diffs_single_file_action) | ||||
| 
 | ||||
|           subject | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when user has view_diffs_file_by_file set to true' do | ||||
|         before do | ||||
|           user.update!(view_diffs_file_by_file: true) | ||||
|         end | ||||
| 
 | ||||
|         it 'tracks single_file_diffs events' do | ||||
|           expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) | ||||
|             .to receive(:track_mr_diffs_single_file_action) | ||||
|             .with(merge_request: merge_request, user: user) | ||||
| 
 | ||||
|           subject | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def collection_arguments(pagination_data = {}) | ||||
|  |  | |||
|  | @ -82,6 +82,7 @@ jest.mock('~/lib/utils/url_utility', () => ({ | |||
| 
 | ||||
| describe('GroupRunnersApp', () => { | ||||
|   let wrapper; | ||||
|   const showToast = jest.fn(); | ||||
| 
 | ||||
|   const findRunnerStats = () => wrapper.findComponent(RunnerStats); | ||||
|   const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell); | ||||
|  | @ -123,6 +124,11 @@ describe('GroupRunnersApp', () => { | |||
|         staleTimeoutSecs, | ||||
|         ...provide, | ||||
|       }, | ||||
|       mocks: { | ||||
|         $toast: { | ||||
|           show: showToast, | ||||
|         }, | ||||
|       }, | ||||
|       ...options, | ||||
|     }); | ||||
| 
 | ||||
|  | @ -250,8 +256,6 @@ describe('GroupRunnersApp', () => { | |||
|   }); | ||||
| 
 | ||||
|   describe('Single runner row', () => { | ||||
|     let showToast; | ||||
| 
 | ||||
|     const { webUrl, editUrl, node } = mockGroupRunnersEdges[0]; | ||||
|     const { id: graphqlId, shortSha, jobExecutionStatus } = node; | ||||
|     const id = getIdFromGraphQLId(graphqlId); | ||||
|  | @ -260,7 +264,6 @@ describe('GroupRunnersApp', () => { | |||
| 
 | ||||
|     beforeEach(async () => { | ||||
|       await createComponent({ mountFn: mountExtended }); | ||||
|       showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show'); | ||||
|     }); | ||||
| 
 | ||||
|     it('Shows job status and links to jobs', () => { | ||||
|  |  | |||
|  | @ -82,6 +82,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do | |||
|             'pull-push' | 'pull-push' | ||||
|             'push'      | 'push' | ||||
|             'pull'      | 'pull' | ||||
|             '$VARIABLE' | '$VARIABLE' | ||||
|             'unknown'   | 'unknown' # invalid | ||||
|           end | ||||
| 
 | ||||
|  | @ -145,6 +146,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do | |||
|           'pull-push' | true | ||||
|           'push'      | true | ||||
|           'pull'      | true | ||||
|           '$VARIABLE' | true | ||||
|           'unknown'   | false | ||||
|         end | ||||
| 
 | ||||
|  | @ -280,7 +282,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do | |||
|             let(:config) { { policy: 'unknown' } } | ||||
| 
 | ||||
|             it 'returns error' do | ||||
|               is_expected.to include('cache policy should be one of: pull-push, push, pull') | ||||
|               is_expected.to include('cache policy should be a variable or one of: pull-push, push, pull') | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|  |  | |||
|  | @ -239,391 +239,3 @@ RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examp | |||
| 
 | ||||
|   it_behaves_like 'validation on Time arguments' | ||||
| end | ||||
| 
 | ||||
| RSpec.shared_examples 'value stream analytics flow metrics leadTime examples' do | ||||
|   let_it_be(:milestone) { create(:milestone, group: group) } | ||||
|   let_it_be(:label) { create(:group_label, group: group) } | ||||
| 
 | ||||
|   let_it_be(:author) { create(:user) } | ||||
|   let_it_be(:assignee) { create(:user) } | ||||
| 
 | ||||
|   let_it_be(:issue1) do | ||||
|     create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue2) do | ||||
|     create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue3) do | ||||
|     create(:labeled_issue, | ||||
|       project: project1, | ||||
|       labels: [label], | ||||
|       author: author, | ||||
|       milestone: milestone, | ||||
|       assignees: [assignee], | ||||
|       created_at: 14.days.ago, | ||||
|       closed_at: 11.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue4) do | ||||
|     create(:labeled_issue, | ||||
|       project: project2, | ||||
|       labels: [label], | ||||
|       assignees: [assignee], | ||||
|       created_at: 20.days.ago, | ||||
|       closed_at: 15.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute | ||||
|   end | ||||
| 
 | ||||
|   let(:query) do | ||||
|     <<~QUERY | ||||
|       query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { | ||||
|         #{context}(fullPath: $path) { | ||||
|           flowMetrics { | ||||
|             leadTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { | ||||
|               value | ||||
|               unit | ||||
|               identifier | ||||
|               title | ||||
|               links { | ||||
|                 label | ||||
|                 url | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     QUERY | ||||
|   end | ||||
| 
 | ||||
|   let(:variables) do | ||||
|     { | ||||
|       path: full_path, | ||||
|       from: 21.days.ago.iso8601, | ||||
|       to: 10.days.ago.iso8601 | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   subject(:result) do | ||||
|     post_graphql(query, current_user: current_user, variables: variables) | ||||
| 
 | ||||
|     graphql_data.dig(context.to_s, 'flowMetrics', 'leadTime') | ||||
|   end | ||||
| 
 | ||||
|   it 'returns the correct value' do | ||||
|     expect(result).to match(a_hash_including({ | ||||
|       'identifier' => 'lead_time', | ||||
|       'unit' => n_('day', 'days', 4), | ||||
|       'value' => 4, | ||||
|       'title' => _('Lead Time'), | ||||
|       'links' => [ | ||||
|         { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) }, | ||||
|         { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) } | ||||
|       ] | ||||
|     })) | ||||
|   end | ||||
| 
 | ||||
|   context 'when the user is not authorized' do | ||||
|     let(:current_user) { create(:user) } | ||||
| 
 | ||||
|     it 'returns nil' do | ||||
|       expect(result).to eq(nil) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when outside of the date range' do | ||||
|     let(:variables) do | ||||
|       { | ||||
|         path: full_path, | ||||
|         from: 30.days.ago.iso8601, | ||||
|         to: 25.days.ago.iso8601 | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns 0 count' do | ||||
|       expect(result).to match(a_hash_including({ 'value' => nil })) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'with all filters' do | ||||
|     let(:variables) do | ||||
|       { | ||||
|         path: full_path, | ||||
|         assigneeUsernames: [assignee.username], | ||||
|         labelNames: [label.title], | ||||
|         authorUsername: author.username, | ||||
|         milestoneTitle: milestone.title, | ||||
|         from: 20.days.ago.iso8601, | ||||
|         to: 10.days.ago.iso8601 | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns filtered count' do | ||||
|       expect(result).to match(a_hash_including({ 'value' => 3 })) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' do | ||||
|   let_it_be(:milestone) { create(:milestone, group: group) } | ||||
|   let_it_be(:label) { create(:group_label, group: group) } | ||||
| 
 | ||||
|   let_it_be(:author) { create(:user) } | ||||
|   let_it_be(:assignee) { create(:user) } | ||||
| 
 | ||||
|   let_it_be(:issue1) do | ||||
|     create(:issue, project: project1, author: author, closed_at: 12.days.ago).tap do |issue| | ||||
|       issue.metrics.update!(first_mentioned_in_commit_at: 17.days.ago) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue2) do | ||||
|     create(:issue, project: project2, author: author, closed_at: 13.days.ago).tap do |issue| | ||||
|       issue.metrics.update!(first_mentioned_in_commit_at: 16.days.ago) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue3) do | ||||
|     create(:labeled_issue, | ||||
|       project: project1, | ||||
|       labels: [label], | ||||
|       author: author, | ||||
|       milestone: milestone, | ||||
|       assignees: [assignee], | ||||
|       closed_at: 11.days.ago).tap do |issue| | ||||
|         issue.metrics.update!(first_mentioned_in_commit_at: 14.days.ago) | ||||
|       end | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue4) do | ||||
|     create(:labeled_issue, | ||||
|       project: project2, | ||||
|       labels: [label], | ||||
|       assignees: [assignee], | ||||
|       closed_at: 15.days.ago).tap do |issue| | ||||
|         issue.metrics.update!(first_mentioned_in_commit_at: 20.days.ago) | ||||
|       end | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute | ||||
|   end | ||||
| 
 | ||||
|   let(:query) do | ||||
|     <<~QUERY | ||||
|       query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { | ||||
|         #{context}(fullPath: $path) { | ||||
|           flowMetrics { | ||||
|             cycleTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { | ||||
|               value | ||||
|               unit | ||||
|               identifier | ||||
|               title | ||||
|               links { | ||||
|                 label | ||||
|                 url | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     QUERY | ||||
|   end | ||||
| 
 | ||||
|   let(:variables) do | ||||
|     { | ||||
|       path: full_path, | ||||
|       from: 21.days.ago.iso8601, | ||||
|       to: 10.days.ago.iso8601 | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   subject(:result) do | ||||
|     post_graphql(query, current_user: current_user, variables: variables) | ||||
| 
 | ||||
|     graphql_data.dig(context.to_s, 'flowMetrics', 'cycleTime') | ||||
|   end | ||||
| 
 | ||||
|   it 'returns the correct value' do | ||||
|     expect(result).to eq({ | ||||
|       'identifier' => 'cycle_time', | ||||
|       'unit' => n_('day', 'days', 4), | ||||
|       'value' => 4, | ||||
|       'title' => _('Cycle Time'), | ||||
|       'links' => [] | ||||
|     }) | ||||
|   end | ||||
| 
 | ||||
|   context 'when the user is not authorized' do | ||||
|     let(:current_user) { create(:user) } | ||||
| 
 | ||||
|     it 'returns nil' do | ||||
|       expect(result).to eq(nil) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when outside of the date range' do | ||||
|     let(:variables) do | ||||
|       { | ||||
|         path: full_path, | ||||
|         from: 30.days.ago.iso8601, | ||||
|         to: 25.days.ago.iso8601 | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns 0 count' do | ||||
|       expect(result).to match(a_hash_including({ 'value' => nil })) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'with all filters' do | ||||
|     let(:variables) do | ||||
|       { | ||||
|         path: full_path, | ||||
|         assigneeUsernames: [assignee.username], | ||||
|         labelNames: [label.title], | ||||
|         authorUsername: author.username, | ||||
|         milestoneTitle: milestone.title, | ||||
|         from: 20.days.ago.iso8601, | ||||
|         to: 10.days.ago.iso8601 | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns filtered count' do | ||||
|       expect(result).to match(a_hash_including({ 'value' => 3 })) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| RSpec.shared_examples 'value stream analytics flow metrics issuesCompleted examples' do | ||||
|   let_it_be(:milestone) { create(:milestone, group: group) } | ||||
|   let_it_be(:label) { create(:group_label, group: group) } | ||||
| 
 | ||||
|   let_it_be(:author) { create(:user) } | ||||
|   let_it_be(:assignee) { create(:user) } | ||||
| 
 | ||||
|   # we don't care about opened date, only closed date. | ||||
|   let_it_be(:issue1) do | ||||
|     create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue2) do | ||||
|     create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue3) do | ||||
|     create(:labeled_issue, | ||||
|       project: project1, | ||||
|       labels: [label], | ||||
|       author: author, | ||||
|       milestone: milestone, | ||||
|       assignees: [assignee], | ||||
|       created_at: 14.days.ago, | ||||
|       closed_at: 11.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   let_it_be(:issue4) do | ||||
|     create(:labeled_issue, | ||||
|       project: project2, | ||||
|       labels: [label], | ||||
|       assignees: [assignee], | ||||
|       created_at: 20.days.ago, | ||||
|       closed_at: 15.days.ago) | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute | ||||
|   end | ||||
| 
 | ||||
|   let(:query) do | ||||
|     <<~QUERY | ||||
|       query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { | ||||
|         #{context}(fullPath: $path) { | ||||
|           flowMetrics { | ||||
|             issuesCompletedCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { | ||||
|               value | ||||
|               unit | ||||
|               identifier | ||||
|               title | ||||
|               links { | ||||
|                 label | ||||
|                 url | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     QUERY | ||||
|   end | ||||
| 
 | ||||
|   let(:variables) do | ||||
|     { | ||||
|       path: full_path, | ||||
|       from: 21.days.ago.iso8601, | ||||
|       to: 10.days.ago.iso8601 | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   subject(:result) do | ||||
|     post_graphql(query, current_user: current_user, variables: variables) | ||||
| 
 | ||||
|     graphql_data.dig(context.to_s, 'flowMetrics', 'issuesCompletedCount') | ||||
|   end | ||||
| 
 | ||||
|   it 'returns the correct value' do | ||||
|     expect(result).to match(a_hash_including({ | ||||
|       'identifier' => 'issues_completed', | ||||
|       'unit' => n_('issue', 'issues', 4), | ||||
|       'value' => 4, | ||||
|       'title' => _('Issues Completed'), | ||||
|       'links' => [ | ||||
|         { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) }, | ||||
|         { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) } | ||||
|       ] | ||||
|     })) | ||||
|   end | ||||
| 
 | ||||
|   context 'when the user is not authorized' do | ||||
|     let(:current_user) { create(:user) } | ||||
| 
 | ||||
|     it 'returns nil' do | ||||
|       expect(result).to eq(nil) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when outside of the date range' do | ||||
|     let(:variables) do | ||||
|       { | ||||
|         path: full_path, | ||||
|         from: 30.days.ago.iso8601, | ||||
|         to: 25.days.ago.iso8601 | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns 0 count' do | ||||
|       expect(result).to match(a_hash_including({ 'value' => 0.0 })) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'with all filters' do | ||||
|     let(:variables) do | ||||
|       { | ||||
|         path: full_path, | ||||
|         assigneeUsernames: [assignee.username], | ||||
|         labelNames: [label.title], | ||||
|         authorUsername: author.username, | ||||
|         milestoneTitle: milestone.title, | ||||
|         from: 20.days.ago.iso8601, | ||||
|         to: 10.days.ago.iso8601 | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns filtered count' do | ||||
|       expect(result).to match(a_hash_including({ 'value' => 1.0 })) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue