Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-10-04 00:07:43 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-10-04 00:07:43 +0300
commit425c89a0081b218f50227c4e3d8ffbfb420b2850 (patch)
tree01b709177ba2794d1a35fe6c68c8658c6644bf47
parentc470e916040edf235ea4ae9322e6969159f44606 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile29
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/comment_type_dropdown.vue5
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue3
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--config/locales/doorkeeper.en.yml2
-rw-r--r--doc/ci/environments/index.md5
-rw-r--r--doc/user/storage_management_automation.md276
-rw-r--r--lib/gitlab/auth.rb7
-rw-r--r--qa/qa/page/component/note.rb97
-rw-r--r--qa/qa/page/merge_request/show.rb8
-rw-r--r--qa/qa/page/project/pipeline/index.rb8
-rw-r--r--qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb174
-rw-r--r--spec/lib/gitlab/auth_spec.rb68
-rw-r--r--spec/models/merge_request_spec.rb2
24 files changed, 401 insertions, 316 deletions
diff --git a/Gemfile b/Gemfile
index 4e40000270d..22c356e4c39 100644
--- a/Gemfile
+++ b/Gemfile
@@ -388,8 +388,8 @@ gem 'prometheus-client-mmap', '~> 0.28', require: 'prometheus/client' # rubocop:
gem 'warning', '~> 1.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
group :development do
- gem 'lefthook', '~> 1.4.7', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'rubocop' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'lefthook', '~> 1.4.7', require: false, feature_category: :tooling
+ gem 'rubocop', feature_category: :tooling
gem 'solargraph', '~> 0.47.2', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'letter_opener_web', '~> 2.0.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -435,8 +435,9 @@ group :development, :test do
# Profiling data from CI/CD pipelines
gem 'influxdb-client', '~> 2.9', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'knapsack', '~> 1.21.1' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'crystalball', '~> 0.7.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'knapsack', '~> 1.21.1', feature_category: :tooling
+ gem 'crystalball', '~> 0.7.0', require: false, feature_category: :tooling
+ gem 'test_file_finder', '~> 0.1.3', feature_category: :tooling
gem 'simple_po_parser', '~> 1.1.6', require: false # rubocop:todo Gemfile/MissingFeatureCategory
@@ -444,22 +445,20 @@ group :development, :test do
gem 'parallel', '~> 1.19', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'test_file_finder', '~> 0.1.3' # rubocop:todo Gemfile/MissingFeatureCategory
-
gem 'sigdump', '~> 0.2.4', require: 'sigdump/setup' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'pact', '~> 1.63' # rubocop:todo Gemfile/MissingFeatureCategory
end
group :development, :test, :danger do
- gem 'gitlab-dangerfiles', '~> 4.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'gitlab-dangerfiles', '~> 4.1.0', require: false, feature_category: :tooling
end
group :development, :test, :coverage do
- gem 'simplecov', '~> 0.21', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'simplecov-lcov', '~> 0.8.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'simplecov-cobertura', '~> 2.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'undercover', '~> 0.4.4', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'simplecov', '~> 0.21', require: false, feature_category: :tooling
+ gem 'simplecov-lcov', '~> 0.8.0', require: false, feature_category: :tooling
+ gem 'simplecov-cobertura', '~> 2.1.0', require: false, feature_category: :tooling
+ gem 'undercover', '~> 0.4.4', require: false, feature_category: :tooling
end
# Gems required in omnibus-gitlab pipeline
@@ -475,10 +474,10 @@ end
group :test do
gem 'fuubar', '~> 2.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'rspec-retry', '~> 0.6.2' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'rspec_profiling', '~> 0.0.6' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'rspec-benchmark', '~> 0.6.0' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'rspec-parameterized', '~> 1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'rspec-retry', '~> 0.6.2', feature_category: :tooling
+ gem 'rspec_profiling', '~> 0.0.6', feature_category: :tooling
+ gem 'rspec-benchmark', '~> 0.6.0', feature_category: :tooling
+ gem 'rspec-parameterized', '~> 1.0', require: false, feature_category: :tooling
gem 'capybara', '~> 3.39', '>= 3.39.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'capybara-screenshot', '~> 1.0.26' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index d72b0c76425..95223213191 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -491,7 +491,7 @@ export default {
<gl-dropdown-item
v-if="diffHasDiscussions(diffFile)"
ref="toggleDiscussionsButton"
- data-qa-selector="toggle_comments_button"
+ data-testid="toggle-comments-button"
@click="toggleFileDiscussionWrappers(diffFile)"
>
<template v-if="diffHasExpandedDiscussions(diffFile)">
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 638955d97ed..142f07973f2 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -69,7 +69,7 @@ export default {
id: 'note-body',
name: 'note[note]',
class: 'js-note-text note-textarea js-gfm-input markdown-area',
- 'data-qa-selector': 'comment_field',
+ 'data-testid': 'comment-field',
},
};
},
diff --git a/app/assets/javascripts/notes/components/comment_type_dropdown.vue b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
index 2e4f925194f..f85b0de0c4e 100644
--- a/app/assets/javascripts/notes/components/comment_type_dropdown.vue
+++ b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
@@ -108,7 +108,7 @@ export default {
text: this.dropdownStartThreadButtonTitle,
description: this.startDiscussionDescription,
value: constants.DISCUSSION,
- qaSelector: 'discussion_menu_item',
+ testid: 'discussion-menu-item',
},
];
},
@@ -132,7 +132,6 @@ export default {
:data-track-label="trackingLabel"
data-track-action="click_button"
data-testid="comment-button"
- data-qa-selector="comment_button"
>
<gl-button variant="confirm" :disabled="disabled" @click="handleClick">
{{ commentButtonTitle }}
@@ -149,7 +148,7 @@ export default {
@select="setNoteType"
>
<template #list-item="{ item }">
- <div :data-qa-selector="item.qaSelector">
+ <div :data-testid="item.testid">
<strong>{{ item.text }}</strong>
<p class="gl-m-0">{{ item.description }}</p>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index dcbf4a0e5d3..c68ffd73ecc 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -49,7 +49,7 @@ export default {
<template>
<div class="discussion-with-resolve-btn clearfix">
<reply-placeholder
- data-qa-selector="discussion_reply_tab"
+ data-testid="discussion-reply-tab"
:placeholder-text="__('Reply…')"
@focus="$emit('showReplyForm')"
/>
@@ -58,7 +58,6 @@ export default {
<div class="btn-group">
<resolve-discussion-button
v-if="discussion.resolvable"
- data-qa-selector="resolve_discussion_button"
data-testid="resolve-discussion-button"
:is-resolving="isResolving"
:button-title="resolveButtonTitle"
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 90f7a6862f0..bf3a750cf40 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -175,7 +175,7 @@ export default {
<gl-disclosure-dropdown
id="discussion-preferences-dropdown"
class="full-width-mobile"
- data-qa-selector="discussion_preferences_dropdown"
+ data-testid="discussion-preferences-dropdown"
:toggle-text="__('Sort or filter')"
:disabled="isLoading"
placement="right"
@@ -213,7 +213,7 @@ export default {
:is-selected="filter.value === currentValue"
:class="{ 'is-active': filter.value === currentValue }"
:data-filter-type="filterType(filter.value)"
- data-qa-selector="filter_menu_item"
+ data-testid="filter-menu-item"
@action="selectFilter(filter.value)"
>
<template #list-item>
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index d02327a37a7..bbfde7f2e0c 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -26,7 +26,7 @@ export default {
<template>
<li
class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note"
- data-qa-selector="discussion_filter_container"
+ data-testid="discussion-filter-container"
>
<div
class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 7f23ee70086..5a1795d7479 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -337,7 +337,7 @@ export default {
icon="pencil"
category="tertiary"
class="note-action-button js-note-edit gl-display-none gl-sm-display-block"
- data-qa-selector="note_edit_button"
+ data-testid="note-edit-button"
@click="onEdit"
/>
<gl-button
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index ccfd4f2c502..deddbef918d 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -117,7 +117,7 @@ export default {
'aria-label': __('Reply to comment'),
placeholder: this.$options.i18n.bodyPlaceholder,
class: 'note-textarea js-gfm-input js-note-text markdown-area js-vue-issue-note-form',
- 'data-qa-selector': 'reply_field',
+ 'data-testid': 'reply-field',
},
};
},
@@ -439,7 +439,7 @@ export default {
:disabled="isDisabled"
category="primary"
variant="confirm"
- data-qa-selector="reply_comment_button"
+ data-testid="reply-comment-button"
class="gl-sm-mr-3 gl-xs-mb-3 js-vue-issue-save js-comment-button"
@click="handleUpdate()"
>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 198be51aba1..c3701c01ee2 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -208,7 +208,7 @@ export default {
</template>
<span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta">
- <span class="system-note-message" data-qa-selector="system_note_content">
+ <span class="system-note-message" data-testid="system-note-content">
<slot></slot>
</span>
<template v-if="createdAt">
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 94d5dc25b9e..e0b1f7a8c6a 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -310,7 +310,7 @@ export default {
:data-discussion-resolvable="discussion.resolvable"
:data-discussion-resolved="discussion.resolved"
class="discussion js-discussion-container"
- data-qa-selector="discussion_content"
+ data-testid="discussion-content"
>
<diff-discussion-header v-if="shouldRenderDiffs" :discussion="discussion" />
<div v-if="!shouldHideDiscussionBody" class="discussion-body">
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9a7cc1a4d37..809b1716b91 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -421,7 +421,7 @@ export default {
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
class="note note-wrapper note-comment"
- data-qa-selector="noteable_note_container"
+ data-testid="noteable-note-container"
>
<div
v-if="showMultiLineComment"
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index a012b4411bc..981b9324688 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -84,12 +84,7 @@ export default {
:tooltip-text="author.name"
tooltip-placement="bottom"
/>
- <gl-button
- class="gl-mr-2"
- variant="link"
- data-qa-selector="expand_replies_button"
- @click="toggle"
- >
+ <gl-button class="gl-mr-2" variant="link" data-testid="expand-replies-button" @click="toggle">
{{ n__('%d reply', '%d replies', replies.length) }}
</gl-button>
<gl-sprintf :message="$options.i18n.lastReplyBy">
@@ -111,7 +106,7 @@ export default {
v-else
class="gl-text-body! gl-text-decoration-none!"
variant="link"
- data-qa-selector="collapse_replies_button"
+ data-testid="collapse-replies-button"
@click="toggle"
>
{{ $options.i18n.collapseReplies }}
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index fac32bfdb24..cb9b85b9ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -12,7 +12,7 @@ export default {
</script>
<template>
- <timeline-entry-item class="note note-wrapper" data-qa-selector="skeleton_note_placeholder">
+ <timeline-entry-item class="note note-wrapper">
<div
class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
></div>
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 9568195bb6e..4b443870d43 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -131,6 +131,8 @@ en:
Grants create access to the runners.
k8s_proxy:
Grants permission to perform Kubernetes API calls using the agent for Kubernetes.
+ ai_features:
+ Grants access to GitLab Duo related API endpoints.
flash:
applications:
create:
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index 713d58de326..ac5cf4f8875 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -623,6 +623,11 @@ To configure multiple **parallel** stop actions on an environment, specify the
When an environment is stopped, the matching `on_stop` actions from only successful deployment jobs are run in parallel, in no particular order.
+NOTE:
+All `on_stop` actions for an environment must belong to the same pipeline. To use multiple `on_stop` actions in
+[downstream pipelines](../pipelines/downstream_pipelines.md), you must configure the environment actions in
+the parent pipeline. For more information, see [downstream pipelines for deployments](../pipelines/downstream_pipelines.md#advanced-example).
+
In the following example, for the `test` environment there are two deployment jobs:
- `deploy-to-cloud-a`
diff --git a/doc/user/storage_management_automation.md b/doc/user/storage_management_automation.md
index 43cd5868f28..96f9ecd11a8 100644
--- a/doc/user/storage_management_automation.md
+++ b/doc/user/storage_management_automation.md
@@ -5,7 +5,7 @@ group: Utilization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Storage management automation **(FREE ALL)**
+# Automate storage management **(FREE ALL)**
This page describes how to automate storage analysis and cleanup to manage your storage usage
with the GitLab REST API.
@@ -86,9 +86,9 @@ For more information about other API client libraries, see [Third-party clients]
NOTE:
Use [GitLab Duo Code Suggestions](project/repository/code_suggestions/index.md) to write code more efficiently.
-## Strategies for storage analysis
+## Storage analysis
-### Identify the storage types
+### Identify storage types
The [projects API endpoint](../api/projects.md#list-all-projects) provides statistics for projects
in your GitLab instance. To use the projects API endpoint, set the `statistics` key to boolean `true`.
@@ -179,7 +179,7 @@ Project Developer Evangelism and Technical Marketing at GitLab / playground / A
}
```
-### Analyzing multiple subgroups and projects
+### Analyze storage in projects and groups
You can automate analysis of multiple projects and groups. For example, you can start at the top namespace level,
and recursively analyze all subgroups and projects. You can also analyze different storage types.
@@ -317,45 +317,15 @@ The script outputs the project job artifacts in a JSON formatted list:
]
```
-### Helper functions
+## Manage CI/CD pipeline storage
-You might need to convert timestamp seconds into a duration format, or print raw bytes in a more
-representative format. You can use the following helper functions to transform values for improved
-readability:
-
-```shell
-# Current Unix timestamp
-date +%s
-
-# Convert `created_at` date time with timezone to Unix timestamp
-date -d '2023-08-08T18:59:47.581Z' +%s
-```
-
-Example with Python that uses the `python-gitlab` API library:
-
-```python
-def render_size_mb(v):
- return "%.4f" % (v / 1024 / 1024)
-
-def render_age_time(v):
- return str(datetime.timedelta(seconds = v))
-
-# Convert `created_at` date time with timezone to Unix timestamp
-def calculate_age(created_at_datetime):
- created_at_ts = datetime.datetime.strptime(created_at_datetime, '%Y-%m-%dT%H:%M:%S.%fZ')
- now = datetime.datetime.now()
- return (now - created_at_ts).total_seconds()
-```
-
-## Managing storage in CI/CD pipelines
+Job artifacts consume most of the pipeline storage, and job logs can also generate several hundreds of kilobytes.
+You should delete the unnecessary job artifacts first and then clean up job logs after analysis.
WARNING:
Deleting job log and artifacts is a destructive action that cannot be reverted. Use with caution. Deleting certain files, including report artifacts, job logs, and metadata files, affects GitLab features that use these files as data sources.
-Job artifacts consume most of the pipeline storage, and job logs can also generate several hundreds of kilobytes.
-You should delete the unnecessary job artifacts first and then clean up job logs after analysis.
-
-### Analyze pipeline storage
+### List job artifacts
To analyze pipeline storage, you can use the [Job API endpoint](../api/jobs.md#list-project-jobs) to retrieve a list of
job artifacts. The endpoint returns the job artifacts `file_type` key in the `artifacts` attribute.
@@ -462,7 +432,7 @@ $ python3 get_all_projects_top_level_namespace_storage_analysis_cleanup_example.
| [gitlab-de/playground/artifact-gen-group/gen-job-artifacts-4](Gen Job Artifacts 4) | 4828297945 | job.log | trace | 0.0030 |
```
-### Delete job artifacts
+### Delete job artifacts in bulk
You can use a Python script to filter the types of job artifacts to delete in bulk.
@@ -521,7 +491,7 @@ When the collection loops remove the object locks, the script deletes the job ar
# Print collection summary (removed for readability)
```
-#### Delete all job artifacts for a project
+### Delete all job artifacts for a project
If you do not need the project's [job artifacts](../ci/jobs/job_artifacts.md), you can
use the following command to delete all job artifacts. This action cannot be reverted.
@@ -595,7 +565,87 @@ that delete the job artifact:
Support for creating a retention policy for job logs is proposed in [issue 374717](https://gitlab.com/gitlab-org/gitlab/-/issues/374717).
-### Inventory of job artifacts expiry settings
+### Delete old pipelines
+
+Pipelines do not add to the overall storage consumption, but if required you can delete them with the following methods.
+
+Automatic deletion of old pipelines is proposed in [issue 338480](https://gitlab.com/gitlab-org/gitlab/-/issues/338480).
+
+Example with the GitLab CLI:
+
+```shell
+export GL_PROJECT_ID=48349590
+
+glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.id,.created_at'
+960031926
+"2023-08-08T22:09:52.745Z"
+959884072
+"2023-08-08T18:59:47.581Z"
+
+glab api --method DELETE projects/$GL_PROJECT_ID/pipelines/960031926
+
+glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.id,.created_at'
+959884072
+"2023-08-08T18:59:47.581Z"
+```
+
+The `created_at` key must be converted from a timestamp to Unix epoch time,
+for example with `date -d '2023-08-08T18:59:47.581Z' +%s`. In the next step, the
+age can be calculated with the difference between now, and the pipeline creation
+date. If the age is larger than the threshold, the pipeline should be deleted.
+
+The following example uses a Bash script that expects `jq` and the GitLab CLI installed, and authorized, and the exported environment variable `GL_PROJECT_ID`.
+
+The full script `get_cicd_pipelines_compare_age_threshold_example.sh` is located in the [GitLab API with Linux Shell](https://gitlab.com/gitlab-de/use-cases/gitlab-api/gitlab-api-linux-shell) project.
+
+```shell
+#/bin/bash
+
+CREATED_AT_ARR=$(glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.created_at' | jq --raw-output @sh)
+
+for row in ${CREATED_AT_ARR[@]}
+do
+ stripped=$(echo $row | xargs echo)
+ #echo $stripped #DEBUG
+
+ CREATED_AT_TS=$(date -d "$stripped" +%s)
+ NOW=$(date +%s)
+
+ AGE=$(($NOW-$CREATED_AT_TS))
+ AGE_THRESHOLD=$((90*24*60*60)) # 90 days
+
+ if [ $AGE -gt $AGE_THRESHOLD ];
+ then
+ echo "Pipeline age $AGE older than threshold $AGE_THRESHOLD, should be deleted."
+ # TODO call glab to delete the pipeline. Needs an ID collected from the glab call above.
+ else
+ echo "Pipeline age $AGE not older than threshold $AGE_THRESHOLD. Ignore."
+ fi
+done
+```
+
+You can use the [`python-gitlab` API library](https://python-gitlab.readthedocs.io/en/stable/gl_objects/pipelines_and_jobs.html#project-pipelines) and
+the `created_at` attribute to implement a similar algorithm that compares the job artifact age:
+
+```python
+ # ...
+
+ for pipeline in project.pipelines.list(iterator=True):
+ pipeline_obj = project.pipelines.get(pipeline.id)
+ print("DEBUG: {p}".format(p=json.dumps(pipeline_obj.attributes, indent=4)))
+
+ created_at = datetime.datetime.strptime(pipeline.created_at, '%Y-%m-%dT%H:%M:%S.%fZ')
+ now = datetime.datetime.now()
+ age = (now - created_at).total_seconds()
+
+ threshold_age = 90 * 24 * 60 * 60
+
+ if (float(age) > float(threshold_age)):
+ print("Deleting pipeline", pipeline.id)
+ pipeline_obj.delete()
+```
+
+### List expiry settings for job artifacts
To manage artifact storage, you can update or configure when an artifact expires.
The expiry setting for artifacts are configured in each job configuration in the `.gitlab-ci.yml`.
@@ -708,9 +758,9 @@ glab api --method GET projects/$GL_PROJECT_ID/search --field "scope=blobs" --fie
For more information about the inventory approach, see [How GitLab can help mitigate deletion of open source container images on Docker Hub](https://about.gitlab.com/blog/2023/03/16/how-gitlab-can-help-mitigate-deletion-open-source-images-docker-hub/).
-### Set the default expiry for job artifacts in projects
+### Set default expiry for job artifacts
-Define the default expiry for job artifacts in your `.gitlab-ci.yml` configuration:
+To set the default expiry for job artifacts in a project, specify the `expire_in` value in the `.gitlab-ci.yml` file:
```yaml
default:
@@ -718,91 +768,13 @@ default:
expire_in: 1 week
```
-### Delete old pipelines
-
-Pipelines do not add to the overall storage consumption, but if you want to delete them you can use the following methods.
-
-Example with the GitLab CLI:
-
-```shell
-export GL_PROJECT_ID=48349590
-
-glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.id,.created_at'
-960031926
-"2023-08-08T22:09:52.745Z"
-959884072
-"2023-08-08T18:59:47.581Z"
-
-glab api --method DELETE projects/$GL_PROJECT_ID/pipelines/960031926
-
-glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.id,.created_at'
-959884072
-"2023-08-08T18:59:47.581Z"
-```
-
-The `created_at` key must be converted from a timestamp to Unix epoch time,
-for example with `date -d '2023-08-08T18:59:47.581Z' +%s`. In the next step, the
-age can be calculated with the difference between now, and the pipeline creation
-date. If the age is larger than the threshold, the pipeline should be deleted.
-
-The following example uses a Bash script that expects `jq` and the GitLab CLI installed, and authorized, and the exported environment variable `GL_PROJECT_ID`.
-
-The full script `get_cicd_pipelines_compare_age_threshold_example.sh` is located in the [GitLab API with Linux Shell](https://gitlab.com/gitlab-de/use-cases/gitlab-api/gitlab-api-linux-shell) project.
-
-```shell
-#/bin/bash
-
-CREATED_AT_ARR=$(glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.created_at' | jq --raw-output @sh)
-
-for row in ${CREATED_AT_ARR[@]}
-do
- stripped=$(echo $row | xargs echo)
- #echo $stripped #DEBUG
-
- CREATED_AT_TS=$(date -d "$stripped" +%s)
- NOW=$(date +%s)
-
- AGE=$(($NOW-$CREATED_AT_TS))
- AGE_THRESHOLD=$((90*24*60*60)) # 90 days
-
- if [ $AGE -gt $AGE_THRESHOLD ];
- then
- echo "Pipeline age $AGE older than threshold $AGE_THRESHOLD, should be deleted."
- # TODO call glab to delete the pipeline. Needs an ID collected from the glab call above.
- else
- echo "Pipeline age $AGE not older than threshold $AGE_THRESHOLD. Ignore."
- fi
-done
-```
-
-You can use the [`python-gitlab` API library](https://python-gitlab.readthedocs.io/en/stable/gl_objects/pipelines_and_jobs.html#project-pipelines) and
-the `created_at` attribute to implement a similar algorithm that compares the job artifact age:
-
-```python
- # ...
-
- for pipeline in project.pipelines.list(iterator=True):
- pipeline_obj = project.pipelines.get(pipeline.id)
- print("DEBUG: {p}".format(p=json.dumps(pipeline_obj.attributes, indent=4)))
-
- created_at = datetime.datetime.strptime(pipeline.created_at, '%Y-%m-%dT%H:%M:%S.%fZ')
- now = datetime.datetime.now()
- age = (now - created_at).total_seconds()
-
- threshold_age = 90 * 24 * 60 * 60
-
- if (float(age) > float(threshold_age)):
- print("Deleting pipeline", pipeline.id)
- pipeline_obj.delete()
-```
-
-Automatic deletion of old pipelines is proposed in [issue 338480](https://gitlab.com/gitlab-org/gitlab/-/issues/338480).
+## Manage Container Registries storage
-## Manage storage for Container Registries
+Container registries are available [in a project](../api/container_registry.md#within-a-project) or [in a group](../api/container_registry.md#within-a-group). You can analyze both locations to implement a cleanup strategy.
-Container registries are available [in a project](../api/container_registry.md#within-a-project) or [in a group](../api/container_registry.md#within-a-group). Both locations require analysis and cleanup strategies.
+### List container registries
-To analyze and cleanup Container Registries in a project:
+To list Container Registries in a project:
::Tabs
@@ -848,8 +820,6 @@ glab api --method GET projects/$GL_PROJECT_ID/registry/repositories/4435617/tags
A similar automation shell script is created in the [delete old pipelines](#delete-old-pipelines) section.
-The `python-gitlab` API library provides bulk deletion interfaces explained in the next section.
-
### Delete container images in bulk
When you [delete container image tags in bulk](../api/container_registry.md#delete-registry-repository-tags-in-bulk),
@@ -862,7 +832,7 @@ you can configure:
WARNING:
On GitLab.com, due to the scale of the Container Registry, the number of tags deleted by this API is limited.
If your Container Registry has a large number of tags to delete, only some of them are deleted. You might need
-to call the API multiple times. To schedule tags for automatic deletion, use a [cleanup policy](#cleanup-policy-for-containers) instead.
+to call the API multiple times. To schedule tags for automatic deletion, use a [cleanup policy](#create-a-cleanup-policy-for-containers) instead.
The following example uses the [`python-gitlab` API library](https://python-gitlab.readthedocs.io/en/stable/gl_objects/repository_tags.html) to fetch a list of tags, and calls the `delete_in_bulk()` method with filter parameters.
@@ -880,15 +850,17 @@ The following example uses the [`python-gitlab` API library](https://python-gitl
repository.tags.delete_in_bulk(name_regex_delete="v.+", keep_n=2)
```
-### Cleanup policy for containers
+### Create a cleanup policy for containers
-Use the project REST API endpoint to [create cleanup policies](packages/container_registry/reduce_container_registry_storage.md#use-the-cleanup-policy-api). The following example uses the GitLab CLI to create a cleanup policy.
+Use the project REST API endpoint to [create cleanup policies](packages/container_registry/reduce_container_registry_storage.md#use-the-cleanup-policy-api) for containers. After you set the cleanup policy, all container images that match your specifications are deleted automatically. You do not need additional API automation scripts.
-To send the attributes as a body parameter, you must:
+To send the attributes as a body parameter:
- Use the `--input -` parameter to read from the standard input.
- Set the `Content-Type` header.
+The following example uses the GitLab CLI to create a cleanup policy:
+
```shell
export GL_PROJECT_ID=48057080
@@ -908,13 +880,11 @@ echo '{"container_expiration_policy_attributes":{"cadence":"1month","enabled":tr
```
-After you set up the cleanup policy, all container images that match your specifications are deleted automatically. You do not need additional API automation scripts.
-
### Optimize container images
You can optimize container images to reduce the image size and overall storage consumption in the container registry. Learn more in the [pipeline efficiency documentation](../ci/pipelines/pipeline_efficiency.md#optimize-docker-images).
-## Manage storage for Package Registry
+## Manage Package Registry storage
Package registries are available [in a project](../api/packages.md#within-a-project) or [in a group](../api/packages.md#within-a-group).
@@ -1034,12 +1004,35 @@ Package size 20.0033 > threshold 10.0000, deleting package.
Review the [cleanup policy](packages/dependency_proxy/reduce_dependency_proxy_storage.md#cleanup-policies) and how to [purge the cache using the API](packages/dependency_proxy/reduce_dependency_proxy_storage.md#use-the-api-to-clear-the-cache)
-## Community resources
+## Improve output readability
-These resources are not officially supported. Ensure to test scripts and tutorials before running destructive cleanup commands that may not be reverted.
+You might need to convert timestamp seconds into a duration format, or print raw bytes in a more
+representative format. You can use the following helper functions to transform values for improved
+readability:
-- Forum topic: [Storage management automation resources](https://forum.gitlab.com/t/storage-management-automation-resources/)
-- Script: [GitLab Storage Analyzer](https://gitlab.com/gitlab-de/use-cases/gitlab-api/gitlab-storage-analyzer), unofficial project by the [GitLab Developer Evangelism team](https://gitlab.com/gitlab-de/). You find similar code examples in this documentation how-to here.
+```shell
+# Current Unix timestamp
+date +%s
+
+# Convert `created_at` date time with timezone to Unix timestamp
+date -d '2023-08-08T18:59:47.581Z' +%s
+```
+
+Example with Python that uses the `python-gitlab` API library:
+
+```python
+def render_size_mb(v):
+ return "%.4f" % (v / 1024 / 1024)
+
+def render_age_time(v):
+ return str(datetime.timedelta(seconds = v))
+
+# Convert `created_at` date time with timezone to Unix timestamp
+def calculate_age(created_at_datetime):
+ created_at_ts = datetime.datetime.strptime(created_at_datetime, '%Y-%m-%dT%H:%M:%S.%fZ')
+ now = datetime.datetime.now()
+ return (now - created_at_ts).total_seconds()
+```
## Testing for storage management automation
@@ -1190,3 +1183,10 @@ Use the following projects to test storage usage with [cost factors for forks](u
- Fork [`gitlab-org/gitlab`](https://gitlab.com/gitlab-org/gitlab) into a new namespace or group (includes LFS, Git repository).
- Fork [`gitlab-com/www-gitlab-com`](https://gitlab.com/gitlab-com/www-gitlab-comgitlab-com/www-gitlab-com) into a new namespace or group.
+
+## Community resources
+
+The following resources are not officially supported. Ensure to test scripts and tutorials before running destructive cleanup commands that may not be reverted.
+
+- Forum topic: [Storage management automation resources](https://forum.gitlab.com/t/storage-management-automation-resources/)
+- Script: [GitLab Storage Analyzer](https://gitlab.com/gitlab-de/use-cases/gitlab-api/gitlab-storage-analyzer), unofficial project by the [GitLab Developer Evangelism team](https://gitlab.com/gitlab-de/). You find similar code examples in this documentation how-to here.
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 9ddfc995535..634eeae6acb 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -426,7 +426,8 @@ module Gitlab
end
def unavailable_scopes_for_resource(resource)
- unavailable_observability_scopes_for_resource(resource)
+ unavailable_observability_scopes_for_resource(resource) +
+ unavailable_ai_features_scopes_for_resource(resource)
end
def unavailable_observability_scopes_for_resource(resource)
@@ -435,6 +436,10 @@ module Gitlab
OBSERVABILITY_SCOPES
end
+ def unavailable_ai_features_scopes_for_resource(_resource)
+ AI_FEATURES_SCOPES
+ end
+
def non_admin_available_scopes
API_SCOPES + REPOSITORY_SCOPES + registry_scopes + OBSERVABILITY_SCOPES + AI_FEATURES_SCOPES
end
diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb
index 26dd08b477c..da87691e7a6 100644
--- a/qa/qa/page/component/note.rb
+++ b/qa/qa/page/component/note.rb
@@ -10,115 +10,110 @@ module QA
super
base.view 'app/assets/javascripts/diffs/components/diff_file_header.vue' do
- element :toggle_comments_button
+ element 'toggle-comments-button'
end
base.view 'app/assets/javascripts/notes/components/comment_form.vue' do
- element :comment_field
+ element 'comment-field'
end
base.view 'app/assets/javascripts/notes/components/comment_type_dropdown.vue' do
- element :comment_button
- element :discussion_menu_item
+ element 'comment-button'
+ element 'discussion-menu-item'
end
base.view 'app/assets/javascripts/notes/components/discussion_actions.vue' do
- element :discussion_reply_tab
- element :resolve_discussion_button
+ element 'discussion-reply-tab'
+ element 'resolve-discussion-button'
end
base.view 'app/assets/javascripts/notes/components/discussion_filter.vue' do
- element :discussion_preferences_dropdown
- element :filter_menu_item
+ element 'discussion-preferences-dropdown'
+ element 'filter-menu-item'
end
base.view 'app/assets/javascripts/notes/components/discussion_filter_note.vue' do
- element :discussion_filter_container
+ element 'discussion-filter-container'
end
base.view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do
- element :discussion_content
+ element 'discussion-content'
end
base.view 'app/assets/javascripts/notes/components/noteable_note.vue' do
- element :noteable_note_container
+ element 'noteable-note-container'
end
base.view 'app/assets/javascripts/notes/components/note_actions.vue' do
- element :note_edit_button
+ element 'note-edit-button'
end
base.view 'app/assets/javascripts/notes/components/note_form.vue' do
- element :reply_field
- element :reply_comment_button
+ element 'reply-field'
+ element 'reply-comment-button'
end
base.view 'app/assets/javascripts/notes/components/note_header.vue' do
- element :system_note_content
+ element 'system-note-content'
end
base.view 'app/assets/javascripts/notes/components/toggle_replies_widget.vue' do
- element :expand_replies_button
- element :collapse_replies_button
- end
-
- base.view 'app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue' do
- element :skeleton_note_placeholder
- end
-
- base.view 'app/views/shared/notes/_form.html.haml' do
- element :new_note_form, 'new-note' # rubocop:disable QA/ElementWithPattern
- element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern
+ element 'expand-replies-button'
+ element 'collapse-replies-button'
end
end
def collapse_replies
- click_element :collapse_replies_button
+ click_element 'collapse-replies-button'
end
# Attachment option should be an absolute path
def comment(text, attachment: nil, filter: :all_activities)
method("select_#{filter}_filter").call
- fill_element :comment_field, "#{text}\n"
+ fill_element 'comment-field', "#{text}\n"
unless attachment.nil?
QA::Page::Component::Dropzone.new(self, '.new-note')
.attach_file(attachment)
end
- click_element :comment_button
+ click_element 'comment-button'
end
def edit_comment(text)
- click_element :note_edit_button
- fill_element :reply_field, text
- click_element :reply_comment_button
+ click_element 'note-edit-button'
+ fill_element 'reply-field', text
+ click_element 'reply-comment-button'
end
def expand_replies
- click_element :expand_replies_button
+ click_element 'expand-replies-button'
end
def has_comment?(comment_text)
- has_element?(:noteable_note_container, text: comment_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+ has_element?(
+ 'noteable-note-container',
+ text: comment_text,
+ wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME
+ )
end
def has_system_note?(note_text)
- has_element?(:system_note_content, text: note_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
+ has_element?('system-note-content', text: note_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
end
def noteable_note_item
- find_element(:noteable_note_container)
+ find_element('noteable-note-container')
end
def reply_to_discussion(position, reply_text)
type_reply_to_discussion(position, reply_text)
- click_element :reply_comment_button
+ click_element 'reply-comment-button'
end
def resolve_discussion_at_index(index)
- within_element_by_index(:discussion_content, index) do
- click_element :resolve_discussion_button
+ within_element_by_index('discussion-content', index) do
+ click_element 'resolve-discussion-button'
end
end
@@ -126,7 +121,7 @@ module QA
select_filter_with_text('Show all activity')
wait_until do
- has_no_element?(:discussion_filter_container) && has_element?(:comment_field)
+ has_no_element?('discussion-filter-container') && has_element?('comment-field')
end
end
@@ -134,7 +129,7 @@ module QA
select_filter_with_text('Show comments only')
wait_until do
- has_no_element?(:discussion_filter_container) && has_no_element?(:system_note_content)
+ has_no_element?('discussion-filter-container') && has_no_element?('system-note-content')
end
end
@@ -142,26 +137,26 @@ module QA
select_filter_with_text('Show history only')
wait_until do
- has_element?(:discussion_filter_container) && has_no_element?(:noteable_note_container)
+ has_element?('discussion-filter-container') && has_no_element?('noteable-note-container')
end
end
def start_discussion(text)
- fill_element :comment_field, text
- within_element(:comment_button) { click_button(class: 'gl-new-dropdown-toggle') }
- click_element :discussion_menu_item
- click_element :comment_button
+ fill_element 'comment-field', text
+ within_element('comment-button') { click_button(class: 'gl-new-dropdown-toggle') }
+ click_element 'discussion-menu-item'
+ click_element 'comment-button'
has_comment?(text)
end
def toggle_comments(position)
- all_elements(:toggle_comments_button, minimum: position)[position - 1].click
+ all_elements('toggle-comments-button', minimum: position)[position - 1].click
end
def type_reply_to_discussion(position, reply_text)
- all_elements(:discussion_reply_tab, minimum: position)[position - 1].click
- fill_element :reply_field, reply_text
+ all_elements('discussion-reply-tab', minimum: position)[position - 1].click
+ fill_element 'reply-field', reply_text
end
private
@@ -169,8 +164,8 @@ module QA
def select_filter_with_text(text)
retry_on_exception do
click_element('issue-title')
- click_element :discussion_preferences_dropdown
- find_element(:filter_menu_item, text: text).click
+ click_element 'discussion-preferences-dropdown'
+ find_element('filter-menu-item', text: text).click
wait_for_requests
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 66a709c4d23..f626cb2dd78 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -192,7 +192,7 @@ module QA
click_element(:diff_comment_button)
click_element(:dismiss_suggestion_popover_button) if has_element?(:dismiss_suggestion_popover_button, wait: 1)
- fill_element(:reply_field, text)
+ fill_element('reply-field', text)
end
def click_discussions_tab
@@ -444,9 +444,9 @@ module QA
find("a[data-linenumber='#{line}']").hover
click_element(:diff_comment_button)
click_element(:suggestion_button)
- initial_content = find_element(:reply_field).value
- fill_element(:reply_field, '')
- fill_element(:reply_field, initial_content.gsub(/(```suggestion:-0\+0\n).*(\n```)/, "\\1#{suggestion}\\2"))
+ initial_content = find_element('reply-field').value
+ fill_element('reply-field', '')
+ fill_element('reply-field', initial_content.gsub(/(```suggestion:-0\+0\n).*(\n```)/, "\\1#{suggestion}\\2"))
click_element(:comment_now_button)
wait_for_requests
end
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
index 657c2c00642..73d89d2d073 100644
--- a/qa/qa/page/project/pipeline/index.rb
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -30,10 +30,14 @@ module QA
# If no status provided, wait for pipeline to complete
def wait_for_latest_pipeline(status: nil, wait: nil, reload: false)
wait ||= Support::Repeater::DEFAULT_MAX_WAIT_TIME
- finished_status = %w[Passed Failed Canceled Skipped Manual]
+ finished_status = %w[passed failed canceled skipped manual]
wait_until(max_duration: wait, reload: reload, sleep_interval: 1, message: "Wait for latest pipeline") do
- status ? latest_pipeline_status == status : finished_status.include?(latest_pipeline_status)
+ if status
+ latest_pipeline_status.casecmp(status) == 0
+ else
+ finished_status.include?(latest_pipeline_status.downcase)
+ end
end
end
diff --git a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
index 510a1c10378..7be18f9d949 100644
--- a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
@@ -99,9 +99,10 @@ module QA
let(:github_client) do
Octokit::Client.new(
access_token: ENV['QA_LARGE_IMPORT_GH_TOKEN'] || Runtime::Env.github_access_token,
- auto_paginate: true,
+ per_page: 100,
middleware: Faraday::RackBuilder.new do |builder|
builder.use(Faraday::Retry::Middleware, exceptions: [Octokit::InternalServerError, Octokit::ServerError])
+ builder.use(Faraday::Response::RaiseError) # faraday retry swallows errors, so it needs to be re-raised
end
)
end
@@ -110,96 +111,51 @@ module QA
let(:gh_branches) do
logger.info("= Fetching branches =")
- github_client.branches(github_repo).map(&:name)
+ with_paginated_request { github_client.branches(github_repo) }.map(&:name)
end
let(:gh_commits) do
logger.info("= Fetching commits =")
- github_client.commits(github_repo).map(&:sha)
+ with_paginated_request { github_client.commits(github_repo) }.map(&:sha)
end
let(:gh_labels) do
logger.info("= Fetching labels =")
- github_client.labels(github_repo).map { |label| { name: label.name, color: "##{label.color}" } }
+ with_paginated_request { github_client.labels(github_repo) }.map do |label|
+ { name: label.name, color: "##{label.color}" }
+ end
end
let(:gh_milestones) do
logger.info("= Fetching milestones =")
- github_client
- .list_milestones(github_repo, state: 'all')
- .map { |ms| { title: ms.title, description: ms.description } }
- end
-
- let(:gh_prs) do
- gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash|
- id = pr.number
- hash[id] = {
- url: pr.html_url,
- title: pr.title,
- body: pr.body || '',
- comments: [*gh_pr_comments[id], *gh_issue_comments[id]].compact,
- events: gh_pr_events[id].reject { |event| unsupported_events.include?(event) }
- }
- end
- end
-
- let(:gh_issues) do
- gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash|
- id = issue.number
- hash[id] = {
- url: issue.html_url,
- title: issue.title,
- body: issue.body || '',
- comments: gh_issue_comments[id],
- events: gh_issue_events[id].reject { |event| unsupported_events.include?(event) }
- }
+ with_paginated_request { github_client.list_milestones(github_repo, state: 'all') }.map do |ms|
+ { title: ms.title, description: ms.description }
end
end
let(:gh_all_issues) do
logger.info("= Fetching issues and prs =")
- github_client.list_issues(github_repo, state: 'all')
- end
-
- let(:gh_all_events) do
- logger.info("- Fetching issue and pr events -")
- github_client.repository_issue_events(github_repo).map do |event|
- { name: event[:event], **(event[:issue] || {}) } # some events don't have issue object at all
- end
- end
-
- let(:gh_issue_events) do
- gh_all_events.each_with_object(Hash.new { |h, k| h[k] = [] }) do |event, hash|
- next if event[:pull_request] || !event[:number]
-
- hash[event[:number]] << event[:name]
- end
- end
-
- let(:gh_pr_events) do
- gh_all_events.each_with_object(Hash.new { |h, k| h[k] = [] }) do |event, hash|
- next unless event[:pull_request]
-
- hash[event[:number]] << event[:name]
- end
+ with_paginated_request { github_client.list_issues(github_repo, state: 'all') }
end
+ # rubocop:disable Layout/LineLength
let(:gh_issue_comments) do
logger.info("- Fetching issue comments -")
- github_client.issues_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
+ with_paginated_request { github_client.issues_comments(github_repo) }.each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
hash[id_from_url(c.html_url)] << c.body&.gsub(gh_link_pattern, dummy_url)
end
end
let(:gh_pr_comments) do
logger.info("- Fetching pr comments -")
- github_client.pull_requests_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
+ with_paginated_request { github_client.pull_requests_comments(github_repo) }.each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
hash[id_from_url(c.html_url)] << c.body
# some suggestions can contain extra whitespaces which gitlab will remove
&.gsub(/suggestion\s+\r/, "suggestion\r")
&.gsub(gh_link_pattern, dummy_url)
end
end
+ # rubocop:enable Layout/LineLength
let(:imported_project) do
Resource::ProjectImportedFromGithub.fabricate_via_api! do |project|
@@ -334,8 +290,8 @@ module QA
gh_commits
gh_labels
gh_milestones
- gh_prs
gh_issues
+ gh_prs
end
# Verify repository imported correctly
@@ -383,8 +339,68 @@ module QA
@issue_diff = verify_mrs_or_issues('issue')
end
+ # This has no real effect, mostly used to group the methods that are used directly from spec body and helpers
+ #
private
+ # Github prs
+ #
+ # Instance variable is used because parallel doesn't play nice with memoized rspec vars
+ #
+ # @return [Hash]
+ def gh_prs
+ @gh_prs ||= begin
+ prs = gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash|
+ id = pr.number
+ hash[id] = {
+ url: pr.html_url,
+ title: pr.title,
+ body: pr.body || '',
+ comments: [*gh_pr_comments[id], *gh_issue_comments[id]].compact
+ }
+ end
+ logger.info("- Fetching pr events 8 parallel threads -")
+ Parallel.map(prs, in_threads: 8) do |id, pr|
+ logger.debug("Fetching events for pr !#{id}")
+ [id, pr.merge({ events: fetch_github_events(id) })]
+ end.to_h
+ end
+ end
+
+ # Github issues
+ #
+ # Instance variable is used because parallel doesn't play nice with memoized rspec vars
+ #
+ # @return [Hash]
+ def gh_issues
+ @gh_issues ||= begin
+ issues = gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash|
+ id = issue.number
+ hash[id] = {
+ url: issue.html_url,
+ title: issue.title,
+ body: issue.body || '',
+ comments: gh_issue_comments[id]
+ }
+ end
+ logger.info("- Fetching issue events in 8 parallel threads -")
+ Parallel.map(issues, in_threads: 8) do |id, issue|
+ logger.debug("Fetching events for issue !#{id}")
+ [id, issue.merge({ events: fetch_github_events(id) })]
+ end.to_h
+ end
+ end
+
+ # Fetch github events for issue/pr
+ #
+ # @param [Integer] id
+ # @return [Array]
+ def fetch_github_events(id)
+ with_paginated_request { github_client.issue_events(github_repo, id) }
+ .map { |event| event[:event] }
+ .reject { |event| unsupported_events.include?(event) }
+ end
+
# Verify imported mrs or issues and return missing items
#
# @param [String] type verification object, 'mrs' or 'issues'
@@ -514,7 +530,7 @@ module QA
logger.debug("= Fetching merge requests =")
imported_mrs = imported_project.merge_requests(**api_request_params)
- logger.debug("= Fetching merge request comments =")
+ logger.debug("- Fetching merge request comments #{Etc.nprocessors} parallel threads -")
Parallel.map(imported_mrs, in_threads: Etc.nprocessors) do |mr|
resource = Resource::MergeRequest.init do |resource|
resource.project = imported_project
@@ -522,7 +538,6 @@ module QA
resource.api_client = api_client
end
- logger.debug("Fetching events and comments for mr '!#{mr[:iid]}'")
comments = resource.comments(**api_request_params)
label_events = resource.label_events(**api_request_params)
state_events = resource.state_events(**api_request_params)
@@ -547,11 +562,10 @@ module QA
logger.debug("= Fetching issues =")
imported_issues = imported_project.issues(**api_request_params)
- logger.debug("= Fetching issue comments =")
+ logger.debug("- Fetching issue comments #{Etc.nprocessors} parallel threads -")
Parallel.map(imported_issues, in_threads: Etc.nprocessors) do |issue|
resource = build(:issue, project: imported_project, iid: issue[:iid], api_client: api_client)
- logger.debug("Fetching events and comments for issue '!#{issue[:iid]}'")
comments = resource.comments(**api_request_params)
label_events = resource.label_events(**api_request_params)
state_events = resource.state_events(**api_request_params)
@@ -639,6 +653,40 @@ module QA
def id_from_url(url)
url.match(%r{(?<type>issues|pull)/(?<id>\d+)})&.named_captures&.fetch("id", nil).to_i
end
+
+ # Custom pagination for github requests
+ #
+ # Default autopagination doesn't work correctly with rate limit
+ #
+ # @return [Array]
+ def with_paginated_request(&block)
+ resources = with_rate_limit(&block)
+
+ loop do
+ next_link = github_client.last_response.rels[:next]&.href
+ break unless next_link
+
+ logger.debug("Fetching resources from next page: '#{next_link}'")
+ resources.concat(with_rate_limit { github_client.get(next_link) })
+ end
+
+ resources
+ end
+
+ # Handle rate limit
+ #
+ # @return [Array]
+ def with_rate_limit
+ yield
+ rescue Faraday::ForbiddenError => e
+ raise e unless e.response[:status] == 403
+
+ wait = github_client.rate_limit.resets_in + 5
+ logger.warn("GitHub rate api rate limit reached, resuming in '#{wait}' seconds")
+ sleep(wait)
+
+ retry
+ end
end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 8da617175ca..708ddebcf7a 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
end
- context 'available_scopes' do
+ describe 'available_scopes' do
before do
stub_container_registry_config(enabled: true)
end
@@ -43,26 +43,26 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode read_observability write_observability create_runner k8s_proxy ai_features]
end
- it 'contains for non-admin user all non-default scopes without ADMIN access and without observability scopes' do
+ it 'contains for non-admin user all non-default scopes without ADMIN access and without observability scopes and ai_features' do
user = build_stubbed(:user, admin: false)
- expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy]
end
- it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes' do
+ it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes and ai_features' do
user = build_stubbed(:user, admin: true)
- expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy]
end
- it 'contains for project all resource bot scopes without observability scopes' do
- expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features]
+ it 'contains for project all resource bot scopes without observability scopes and ai_features' do
+ expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy]
end
it 'contains for group all resource bot scopes' do
- group = build_stubbed(:group)
+ group = build_stubbed(:group).tap { |g| g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) }
- expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy]
end
it 'contains for unsupported type no scopes' do
@@ -73,6 +73,34 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode openid profile email read_observability write_observability create_runner k8s_proxy ai_features]
end
+ describe 'ai_features scope' do
+ let(:resource) { nil }
+
+ subject { described_class.available_scopes_for(resource) }
+
+ context 'when resource is user', 'and user has a group with ai features' do
+ let(:resource) { build_stubbed(:user) }
+
+ it { is_expected.not_to include(:ai_features) }
+ end
+
+ context 'when resource is project' do
+ let(:resource) { build_stubbed(:project) }
+
+ it 'does not include ai_features scope' do
+ is_expected.not_to include(:ai_features)
+ end
+ end
+
+ context 'when resource is group' do
+ let(:resource) { build_stubbed(:group) }
+
+ it 'does not include ai_features scope' do
+ is_expected.not_to include(:ai_features)
+ end
+ end
+ end
+
context 'with observability_group_tab feature flag' do
context 'when disabled' do
before do
@@ -80,37 +108,43 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
end
it 'contains for group all resource bot scopes without observability scopes' do
- group = build_stubbed(:group)
+ group = build_stubbed(:group).tap do |g|
+ g.namespace_settings = build_stubbed(:namespace_settings, namespace: g)
+ end
- expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy]
end
end
context 'when enabled for specific group' do
- let(:group) { build_stubbed(:group) }
+ let(:group) do
+ build_stubbed(:group).tap { |g| g.namespace_settings = build_stubbed(:namespace_settings, namespace: g) }
+ end
before do
stub_feature_flags(observability_group_tab: group)
end
it 'contains for other group all resource bot scopes including observability scopes' do
- expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry read_observability write_observability create_runner k8s_proxy]
end
it 'contains for admin user all non-default scopes with ADMIN access and without observability scopes' do
user = build_stubbed(:user, admin: true)
- expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode create_runner k8s_proxy]
end
it 'contains for project all resource bot scopes without observability scopes' do
- expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(project)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy]
end
it 'contains for other group all resource bot scopes without observability scopes' do
- other_group = build_stubbed(:group)
+ other_group = build_stubbed(:group).tap do |g|
+ g.namespace_settings = build_stubbed(:namespace_settings, namespace: g)
+ end
- expect(subject.available_scopes_for(other_group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy ai_features]
+ expect(subject.available_scopes_for(other_group)).to match_array %i[api read_api read_repository write_repository read_registry write_registry create_runner k8s_proxy]
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8fe0dc556ac..630b5dec62e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -4590,7 +4590,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
describe '#unlock_mr' do
subject { create(:merge_request, state: 'locked', source_project: project, merge_jid: 123) }
- it 'updates merge request head pipeline and sets merge_jid to nil', :sidekiq_might_not_need_inline do
+ it 'updates merge request head pipeline and sets merge_jid to nil', :sidekiq_inline do
pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)
subject.unlock_mr