Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1906d8f913
commit
5f2254b006
|
|
@ -139,8 +139,7 @@ export default {
|
|||
: this.$options.i18n.runButtonText;
|
||||
},
|
||||
variableSettings() {
|
||||
// eslint-disable-next-line local-rules/require-valid-help-page-path
|
||||
return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
|
||||
return helpPagePath('ci/variables/index', { anchor: 'for-a-project' });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ export default {
|
|||
GlLink,
|
||||
GlFormCheckbox,
|
||||
},
|
||||
// eslint-disable-next-line local-rules/require-valid-help-page-path
|
||||
forcePushHelpPath: helpPagePath('topics/git/git_rebase', { anchor: 'force-push' }),
|
||||
forcePushHelpPath: helpPagePath('topics/git/git_rebase', { anchor: 'force-pushing' }),
|
||||
props: {
|
||||
membersAllowedToPush: {
|
||||
type: Array,
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
= render Pajamas::ButtonComponent.new(variant: :link, icon: icon, icon_classes: "js-chevron-icon", button_options: { "aria-controls": "accordion-item", "aria-expanded": expanded }) do
|
||||
= @title
|
||||
|
||||
.accordion-item.gl-mt-3.gl-font-base.collapse{ **body_class }
|
||||
.accordion-item.gl-mt-3.gl-text-base.collapse{ **body_class }
|
||||
= content
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
-# https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2324
|
||||
|
||||
.gl-banner.gl-card.gl-pl-6.gl-pr-8.gl-py-6{ @banner_options, class: banner_class }
|
||||
.gl-card-body.gl-display-flex.gl-p-0!
|
||||
.gl-card-body.gl-flex{ :class => "!gl-p-0" }
|
||||
- if illustration?
|
||||
.gl-banner-illustration
|
||||
= illustration
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ module Pajamas
|
|||
|
||||
def banner_class
|
||||
classes = []
|
||||
classes.push('gl-bg-gray-10!') unless introduction?
|
||||
classes.push('gl-banner-introduction') if introduction?
|
||||
classes.join(' ')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
.gl-single-stat.gl-display-flex.gl-flex-direction-column.gl-p-2
|
||||
.gl-display-flex.gl-align-items-center.gl-text-gray-700.gl-mb-2
|
||||
.gl-single-stat.gl-flex.gl-flex-col.gl-p-2
|
||||
.gl-flex.gl-items-center.gl-text-gray-700.gl-mb-2
|
||||
- if title_icon?
|
||||
= sprite_icon(@title_icon, css_class: 'gl-mr-2')
|
||||
%span.gl-font-base.gl-font-normal{ data: { testid: 'title-text' } }
|
||||
%span.gl-text-base.gl-font-normal{ data: { testid: 'title-text' } }
|
||||
= title || @title
|
||||
.gl-single-stat-content.gl-display-flex.gl-align-items-baseline.gl-font-bold.gl-text-gray-900
|
||||
.gl-single-stat-content.gl-flex.gl-items-baseline.gl-font-bold.gl-text-gray-900
|
||||
%span.gl-single-stat-number.gl-leading-1{ class: unit_class, data: { testid: 'displayValue' } }
|
||||
%span{ data: { testid: @stat_value_testid } }
|
||||
= stat_value || @stat_value
|
||||
- if unit?
|
||||
%span.gl-font-sm.gl-mx-2.gl-transition-all.gl-opacity-10{ data: { testid: 'unit' } }
|
||||
%span.gl-text-sm.gl-mx-2.gl-transition-all.gl-opacity-10{ data: { testid: 'unit' } }
|
||||
= @unit
|
||||
- if meta_icon? && !meta_text?
|
||||
= sprite_icon(@meta_icon, css_class: @text_color)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ module Pajamas
|
|||
private
|
||||
|
||||
def spinner_class
|
||||
["gl-spinner", "gl-spinner-#{@size}", "gl-spinner-#{@color} gl-vertical-align-text-bottom!"]
|
||||
["gl-spinner", "gl-spinner-#{@size}", "gl-spinner-#{@color} !gl-align-text-bottom"]
|
||||
end
|
||||
|
||||
def html_options
|
||||
|
|
|
|||
|
|
@ -52,10 +52,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def metrics_dashboard_path(cluster)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Will be overridden in EE
|
||||
def environments_cluster_path(cluster)
|
||||
nil
|
||||
|
|
|
|||
|
|
@ -59,9 +59,7 @@ module Clusters
|
|||
def health_data(clusterable)
|
||||
{
|
||||
'clusters-path': clusterable.index_path,
|
||||
'dashboard-endpoint': clusterable.metrics_dashboard_path(cluster),
|
||||
'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
|
||||
'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
|
||||
'settings-path': '',
|
||||
'project-path': '',
|
||||
'tags-path': ''
|
||||
|
|
|
|||
|
|
@ -34,10 +34,6 @@ class GroupClusterablePresenter < ClusterablePresenter
|
|||
def learn_more_link
|
||||
ApplicationController.helpers.link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/group/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
|
||||
end
|
||||
|
||||
def metrics_dashboard_path(cluster)
|
||||
metrics_dashboard_group_cluster_path(clusterable, cluster)
|
||||
end
|
||||
end
|
||||
|
||||
GroupClusterablePresenter.prepend_mod_with('GroupClusterablePresenter')
|
||||
|
|
|
|||
|
|
@ -62,10 +62,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
|
|||
def learn_more_link
|
||||
ApplicationController.helpers.link_to(s_('ClusterIntegration|Learn more about instance Kubernetes clusters'), help_page_path('user/instance/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
|
||||
end
|
||||
|
||||
def metrics_dashboard_path(cluster)
|
||||
metrics_dashboard_admin_cluster_path(cluster)
|
||||
end
|
||||
end
|
||||
|
||||
InstanceClusterablePresenter.prepend_mod_with('InstanceClusterablePresenter')
|
||||
|
|
|
|||
|
|
@ -29,10 +29,6 @@ class ProjectClusterablePresenter < ClusterablePresenter
|
|||
def learn_more_link
|
||||
ApplicationController.helpers.link_to(s_('ClusterIntegration|Learn more about Kubernetes.'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
|
||||
end
|
||||
|
||||
def metrics_dashboard_path(cluster)
|
||||
metrics_dashboard_project_cluster_path(clusterable, cluster)
|
||||
end
|
||||
end
|
||||
|
||||
ProjectClusterablePresenter.prepend_mod_with('ProjectClusterablePresenter')
|
||||
|
|
|
|||
|
|
@ -10,29 +10,58 @@ class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
feature_category :system_access
|
||||
worker_resource_boundary :cpu
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
BATCH_DELAY = 10.seconds
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def perform
|
||||
Member.expired.preload(:user, :source).find_each do |member|
|
||||
context = {
|
||||
user: member.user,
|
||||
# The ApplicationContext will reject type-mismatches. So a GroupMemeber will only populate `namespace`.
|
||||
# while a `ProjectMember` will populate `project
|
||||
project: member.source,
|
||||
namespace: member.source
|
||||
}
|
||||
with_context(context) do
|
||||
Members::DestroyService.new.execute(member, skip_authorization: true)
|
||||
def perform(cursor = nil)
|
||||
@updated_count = 0
|
||||
paginator = paginate(cursor)
|
||||
|
||||
expired_user = member.user
|
||||
paginator.each { |member| process_member(member) }
|
||||
|
||||
if expired_user.project_bot?
|
||||
Users::DestroyService.new(nil).execute(expired_user, skip_authorization: true)
|
||||
end
|
||||
status = paginator.has_next_page? ? :limit_reached : :completed
|
||||
log_extra_metadata_on_done(:result,
|
||||
status: status,
|
||||
updated_rows: @updated_count
|
||||
)
|
||||
|
||||
return unless paginator.has_next_page?
|
||||
|
||||
self.class.perform_in(BATCH_DELAY, paginator.cursor_for_next_page)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def paginate(cursor)
|
||||
Member.expired
|
||||
.includes(:user, :source)
|
||||
.order(expires_at: :desc, id: :desc)
|
||||
.keyset_paginate(cursor: cursor, per_page: BATCH_SIZE)
|
||||
end
|
||||
|
||||
def process_member(member)
|
||||
context = {
|
||||
user: member.user,
|
||||
# The ApplicationContext will reject type-mismatches. So a GroupMemeber will only populate `namespace`.
|
||||
# while a `ProjectMember` will populate `project
|
||||
project: member.source,
|
||||
namespace: member.source
|
||||
}
|
||||
with_context(context) do
|
||||
Members::DestroyService.new.execute(member, skip_authorization: true)
|
||||
|
||||
expired_user = member.user
|
||||
|
||||
if expired_user.project_bot?
|
||||
Users::DestroyService.new(nil).execute(expired_user, skip_authorization: true)
|
||||
end
|
||||
rescue StandardError => ex
|
||||
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ex)
|
||||
|
||||
@updated_count += 1
|
||||
end
|
||||
rescue StandardError => ex
|
||||
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ex)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
|||
|
|
@ -237,13 +237,14 @@ The following Geo data types exist:
|
|||
- `Upload`
|
||||
- `DependencyProxy::Manifest`
|
||||
- `DependencyProxy::Blob`
|
||||
- **Repository types:**
|
||||
- `ContainerRepositoryRegistry`
|
||||
- **Git Repository types:**
|
||||
- `DesignManagement::Repository`
|
||||
- `ProjectRepository`
|
||||
- `ProjectWikiRepository`
|
||||
- `SnippetRepository`
|
||||
- `GroupWikiRepository`
|
||||
- **Other types:**
|
||||
- `ContainerRepository`
|
||||
|
||||
The main kinds of classes are Registry, Model, and Replicator. If you have an instance of one of these classes, you can get the others. The Registry and Model mostly manage PostgreSQL DB state. The Replicator knows how to replicate/verify (or it can call a service to do it):
|
||||
|
||||
|
|
@ -445,6 +446,90 @@ end
|
|||
p "#{uploads_deleted} remote objects were destroyed."
|
||||
```
|
||||
|
||||
### Error: `Error syncing repository: 13:fatal: could not read Username`
|
||||
|
||||
The `last_sync_failure` error
|
||||
`Error syncing repository: 13:fatal: could not read Username for 'https://gitlab.example.com': terminal prompts disabled`
|
||||
indicates that JWT authentication is failing during a Geo clone or fetch request.
|
||||
See [Geo (development) > Authentication](../../../../development/geo.md#authentication) for more context.
|
||||
|
||||
First, check that system clocks are synced. Run the [Health check Rake task](common.md#health-check-rake-task), or
|
||||
manually check that `date`, on all Sidekiq nodes on the secondary site and all Puma nodes on the primary site, are the
|
||||
same.
|
||||
|
||||
If system clocks are synced, then the JWT token may be expiring while Git fetch is performing calculations between its
|
||||
two separate HTTP requests. See [issue 464101](https://gitlab.com/gitlab-org/gitlab/-/issues/464101), which existed in
|
||||
all GitLab versions until it was fixed in GitLab 17.1.0, 17.0.5, and 16.11.7.
|
||||
|
||||
To validate if you are experiencing this issue:
|
||||
|
||||
1. Monkey patch the code in a [Rails console](../../../operations/rails_console.md#starting-a-rails-console-session) to increase the validity period of the token from 1 minute to 10 minutes. Run
|
||||
this in Rails console on the secondary site:
|
||||
|
||||
```ruby
|
||||
module Gitlab; module Geo; class BaseRequest
|
||||
private
|
||||
def geo_auth_token(message)
|
||||
signed_data = Gitlab::Geo::SignedData.new(geo_node: requesting_node, validity_period: 10.minutes).sign_and_encode_data(message)
|
||||
|
||||
"#{GITLAB_GEO_AUTH_TOKEN_TYPE} #{signed_data}"
|
||||
end
|
||||
end;end;end
|
||||
```
|
||||
|
||||
1. In the same Rails console, resync an affected project:
|
||||
|
||||
```ruby
|
||||
Project.find_by_full_path('mygroup/mysubgroup/myproject').replicator.resync
|
||||
```
|
||||
|
||||
1. Look at the sync state:
|
||||
|
||||
```ruby
|
||||
Project.find_by_full_path('mygroup/mysubgroup/myproject').replicator.registry
|
||||
```
|
||||
|
||||
1. If `last_sync_failure` no longer includes the error `fatal: could not read Username`, then you are
|
||||
affected by this issue. The state should now be `2`, meaning "synced". If so, then you should upgrade to
|
||||
a GitLab version with the fix. You may also wish to upvote or comment on
|
||||
[issue 466681](https://gitlab.com/gitlab-org/gitlab/-/issues/466681) which would have reduced the severity of this
|
||||
issue.
|
||||
|
||||
To workaround the issue, you must hot-patch all Sidekiq nodes in the secondary site to extend the JWT expiration time:
|
||||
|
||||
1. Edit `/opt/gitlab/embedded/service/gitlab-rails/ee/lib/gitlab/geo/signed_data.rb`.
|
||||
1. Find `Gitlab::Geo::SignedData.new(geo_node: requesting_node)` and add `, validity_period: 10.minutes` to it:
|
||||
|
||||
```diff
|
||||
- Gitlab::Geo::SignedData.new(geo_node: requesting_node)
|
||||
+ Gitlab::Geo::SignedData.new(geo_node: requesting_node, validity_period: 10.minutes)
|
||||
```
|
||||
|
||||
1. Restart Sidekiq:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl restart sidekiq
|
||||
```
|
||||
|
||||
1. Unless you upgrade to a version containing the fix, you would have to repeat this workaround after every GitLab upgrade.
|
||||
|
||||
### Error: `fetch remote: signal: terminated: context deadline exceeded` at exactly 3 hours
|
||||
|
||||
If Git fetch fails at exactly three hours while syncing a Git repository:
|
||||
|
||||
1. Edit `/etc/gitlab/gitlab.rb` to increase the Git timeout from the default of 10800 seconds:
|
||||
|
||||
```ruby
|
||||
# Git timeout in seconds
|
||||
gitlab_rails['gitlab_shell_git_timeout'] = 21600
|
||||
```
|
||||
|
||||
1. Reconfigure GitLab:
|
||||
|
||||
```shell
|
||||
sudo gitlab-ctl reconfigure
|
||||
```
|
||||
|
||||
## Investigate causes of database replication lag
|
||||
|
||||
If the output of `sudo gitlab-rake geo:status` shows that `Database replication lag` remains significantly high over time, the primary node in database replication can be checked to determine the status of lag for
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ following month after you submit your license usage data.
|
|||
Fifteen days before the license expires, a notification banner with the upcoming expiration
|
||||
date displays to GitLab administrators.
|
||||
|
||||
Licenses expire at the start of the expiration date, 00:00 server time.
|
||||
|
||||
When your license expires, GitLab locks features, like Git pushes
|
||||
and issue creation. Your instance becomes read-only and
|
||||
an expiration message displays to all administrators. You have a 14-day grace period
|
||||
|
|
@ -85,10 +87,10 @@ before this occurs.
|
|||
|
||||
For example, if a license has a start date of January 1, 2024 and an end date of January 1, 2025:
|
||||
|
||||
- It expires at 11:59:59 PM UTC December 31, 2024.
|
||||
- It is considered expired from 12:00:00 AM UTC January 1, 2025.
|
||||
- The grace period of 14 days starts at 12:00:00 AM UTC January 1, 2025 and ends at 11:59:59 PM UTC January 14, 2025.
|
||||
- Your instance becomes read-only at 12:00:00 AM UTC January 15, 2025.
|
||||
- It expires at 11:59:59 PM server time December 31, 2024.
|
||||
- It is considered expired from 12:00:00 AM server time January 1, 2025.
|
||||
- The grace period of 14 days starts at 12:00:00 AM server time January 1, 2025 and ends at 11:59:59 PM server time January 14, 2025.
|
||||
- Your instance becomes read-only at 12:00:00 AM server time January 15, 2025.
|
||||
|
||||
To resume functionality, [renew your subscription](../subscriptions/self_managed/index.md#renew-subscription-manually).
|
||||
|
||||
|
|
|
|||
|
|
@ -14,112 +14,14 @@ info: Any user with at least the Maintainer role can merge updates to this conte
|
|||
|
||||
**How:**
|
||||
Follow [these instructions](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitlab_ai_gateway.md#install)
|
||||
to install the AI Gateway with GDK.
|
||||
to install the AI Gateway with GDK. We recommend this route for most users.
|
||||
|
||||
### Required: Setup Google Cloud Platform in AI Gateway
|
||||
You can also install AI Gateway by:
|
||||
|
||||
**Why:** You may not be able to boot AI Gateway if Google Cloud Platform
|
||||
credentials isn't correctly setup because AI Gateway checks the access at boot
|
||||
time.
|
||||
1. [Cloning the repository directly](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist).
|
||||
1. [Running the server locally](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist#how-to-run-the-server-locally).
|
||||
|
||||
**How:**
|
||||
|
||||
1. Set up a Google Cloud project
|
||||
1. Option 1 (recommended for GitLab team members): use the existing
|
||||
`ai-enablement-dev-69497ba7`) Google Cloud project.
|
||||
Using the existing project is recommended because this project has Vertex
|
||||
APIs and Vertex AI Search already enabled.
|
||||
Also, all GitLab team members should already have access to this project.
|
||||
Visit the [Google Cloud console](https://console.cloud.google.com) to
|
||||
confirm that you already have access.
|
||||
1. Option 2: Create a sandbox Google Cloud project by following the instructions
|
||||
in [the handbook](https://handbook.gitlab.com/handbook/infrastructure-standards/#individual-environment).
|
||||
If you are using an individual Google Cloud project, you may also need to
|
||||
enable the Vertex AI API:
|
||||
1. Visit [welcome page](https://console.cloud.google.com/welcome), choose
|
||||
your project (for example: `jdoe-5d23dpe`).
|
||||
1. Go to **APIs & Services > Enabled APIs & services**.
|
||||
1. Select **Enable APIs and Services**.
|
||||
1. Search for `Vertex AI API`.
|
||||
1. Select **Vertex AI API**, then select **Enable**.
|
||||
1. Install the [`gcloud` CLI](https://cloud.google.com/sdk/docs/install)
|
||||
1. If you already use [`asdf`](https://asdf-vm.com/) for runtime version
|
||||
management, you can install `gcloud` with the
|
||||
[`asdf gcloud` plugin](https://github.com/jthegedus/asdf-gcloud)
|
||||
1. Authenticate locally with Google Cloud using the
|
||||
[`gcloud auth application-default login`](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) command.
|
||||
|
||||
NOTE:
|
||||
This command tries to find a quota project from gcloud's context
|
||||
and write it to ADC(Application Default Credentials)
|
||||
so that Google client libraries can use it for billing and quota.
|
||||
To be able to use a project as the quota project, the account in
|
||||
ADC must have the `serviceusage.services.use` permission on the project.
|
||||
If you don't have a project with this permission and you always want to
|
||||
bill the project owning the resources,
|
||||
you can disable the quota project and authenticate using
|
||||
`gcloud auth application-default login --disable-quota-project` command.
|
||||
1. Update the [application settings file](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/application_settings.md) in AI Gateway:
|
||||
|
||||
```shell
|
||||
# <GDK-root>/gitlab-ai-gateway/.env
|
||||
|
||||
# PROJECT_ID = "ai-enablement-dev-69497ba7" for GitLab team members with access
|
||||
# to the shared project
|
||||
|
||||
# PROJECT_ID = "your-google-cloud-project-name" for those with their own sandbox
|
||||
# Google Cloud project.
|
||||
|
||||
AIGW_GOOGLE_CLOUD_PLATFORM__PROJECT='PROJECT_ID'
|
||||
```
|
||||
|
||||
### Required: Setup Anthropic in the AI Gateway
|
||||
|
||||
**Why:** some GitLab Duo features use Anthropic models.
|
||||
|
||||
**How:**
|
||||
After filling out an
|
||||
[access request](https://gitlab.com/gitlab-com/team-member-epics/access-requests/-/issues/new?issuable_template=AI_Access_Request),
|
||||
you can sign up for an Anthropic account and
|
||||
[create an API key](https://docs.anthropic.com/en/docs/getting-access-to-claude).
|
||||
|
||||
Update the [application settings file](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/application_settings.md) in AI Gateway:
|
||||
|
||||
```shell
|
||||
# <GDK-root>/gitlab-ai-gateway/.env
|
||||
|
||||
ANTHROPIC_API_KEY='<your-anthropic-api-key>'
|
||||
```
|
||||
|
||||
### Required: Setup AI Gateway endpoint in GitLab-Rails
|
||||
|
||||
**Why:** Your need to tell your local GitLab instance to talk to your local AI
|
||||
Gateway. Otherwise, it tries to talk to the production AI Gateway
|
||||
(`cloud.gitlab.com`), which results in an authentication error.
|
||||
|
||||
**How:**
|
||||
|
||||
Update following variable in the `env.runit` file in your GDK root:
|
||||
|
||||
```shell
|
||||
# <GDK-root>/env.runit
|
||||
|
||||
export AI_GATEWAY_URL=http://0.0.0.0:5052
|
||||
```
|
||||
|
||||
By default, the above URL works as-is.
|
||||
|
||||
You can also change it to a different URL by updating the [application settings file](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/application_settings.md) in AI Gateway:
|
||||
|
||||
```shell
|
||||
# <GDK-root>/gitlab-ai-gateway/.env
|
||||
|
||||
AIGW_FASTAPI__API_HOST=0.0.0.0
|
||||
AIGW_FASTAPI__API_PORT=5052
|
||||
```
|
||||
|
||||
To check if your monolith is using correct AI Gateway, please visit `http://<your-gdk-url>/help/instance_configuration#ai_gateway_url`
|
||||
page.
|
||||
We only recommend this for users who know what they are doing.
|
||||
|
||||
### Required: Setup Licenses in GitLab-Rails
|
||||
|
||||
|
|
@ -180,7 +82,7 @@ It has no effect if you run you local GDK as SaaS, so you can always keep it set
|
|||
|
||||
Setting this environment variable will allow the local GL instance to issue tokens itself, without syncing with CustomersDot first.
|
||||
This is similar how GitLab.com operates, and we allow it for development purposes to simplify the setup.
|
||||
With it you can skip the [CustomersDot setup](#option-2-use-your-customersdot-instance-as-a-provider).
|
||||
With it you can skip the [CustomersDot setup](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitlab_ai_gateway.md#option-2-use-your-customersdot-instance-as-a-provider).
|
||||
This can done by either:
|
||||
|
||||
- setting it in the `env.runit` file in your GDK root
|
||||
|
|
@ -192,7 +94,7 @@ If you plan to use local CustomersDot or test cross-service integration, you may
|
|||
|
||||
### Option A: Run GDK in SaaS mode and enable AI features for a test group
|
||||
|
||||
**How:** the following should be set in the `env.runit` file in your GDK root:
|
||||
This is automatically set up when setting up AI Gateway with GDK. If you would like to turn it off, set the `env.runit` file in your GDK root as follows:
|
||||
|
||||
```shell
|
||||
# <GDK-root>/env.runit
|
||||
|
|
@ -286,29 +188,6 @@ correctly reach to AI Gateway:
|
|||
NOTE:
|
||||
See [this doc](../cloud_connector/index.md) for registering unit primitives in Cloud Connector.
|
||||
|
||||
### Optional: Enable logging in AI Gateway
|
||||
|
||||
**Why:** Logging makes it easier to debug any issues with GitLab Duo requests.
|
||||
|
||||
**How:**
|
||||
|
||||
Update the [application settings file](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/application_settings.md) in AI Gateway:
|
||||
|
||||
```shell
|
||||
# <GDK-root>/gitlab-ai-gateway/.env
|
||||
|
||||
AIGW_LOGGING__LEVEL=debug
|
||||
AIGW_LOGGING__FORMAT_JSON=false
|
||||
AIGW_LOGGING__TO_FILE='./ai-gateway.log'
|
||||
```
|
||||
|
||||
For example, you can watch the log file with the following command when in the
|
||||
`gitlab-ai-gateway` directory:
|
||||
|
||||
```shell
|
||||
tail -f ai-gateway.log | fblog -a prefix -a suffix -a current_file_name -a suggestion -a language -a input -a parameters -a score -a exception
|
||||
```
|
||||
|
||||
### Optional: Enable authentication and authorization in AI Gateway
|
||||
|
||||
**Why:** The AI Gateway has [authentication and authorization](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/auth.md)
|
||||
|
|
@ -724,7 +603,7 @@ end
|
|||
```
|
||||
|
||||
NOTE:
|
||||
See [this section](#optional-enable-authentication-and-authorization-in-ai-gateway) about authentication and authorization in AI Gateway.
|
||||
For more information, see [the GitLab AI Gateway documentation](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitlab_ai_gateway.md#optional-enable-authentication-and-authorization-in-ai-gateway) about authentication and authorization in AI Gateway.
|
||||
|
||||
### Pairing requests with responses
|
||||
|
||||
|
|
|
|||
|
|
@ -318,22 +318,30 @@ sequenceDiagram
|
|||
|
||||
## Authentication
|
||||
|
||||
To authenticate file transfers, each `GeoNode` record has two fields:
|
||||
To authenticate Git and file transfers, each `GeoNode` record has two fields:
|
||||
|
||||
- A public access key (`access_key` field).
|
||||
- A secret access key (`secret_access_key` field).
|
||||
|
||||
The **secondary** site authenticates itself via a [JWT request](https://jwt.io/).
|
||||
When the **secondary** site wishes to download a file, it sends an
|
||||
HTTP request with the `Authorization` header:
|
||||
|
||||
The **secondary** site authorizes HTTP requests with the `Authorization` header:
|
||||
|
||||
```plaintext
|
||||
Authorization: GL-Geo <access_key>:<JWT payload>
|
||||
```
|
||||
|
||||
The **primary** site uses the `access_key` field to look up the
|
||||
corresponding **secondary** site and decrypts the JWT payload,
|
||||
which contains additional information to identify the file
|
||||
The **primary** site uses the `access_key` field to look up the corresponding
|
||||
**secondary** site and decrypts the JWT payload.
|
||||
|
||||
NOTE:
|
||||
JWT requires synchronized clocks between the machines involved, otherwise the
|
||||
**primary** site may reject the request.
|
||||
|
||||
### File transfers
|
||||
|
||||
When the **secondary** site wishes to download a file, the JWT payload
|
||||
contains additional information to identify the file
|
||||
request. This ensures that the **secondary** site downloads the right
|
||||
file for the right database ID. For example, for an LFS object, the
|
||||
request must also include the SHA256 sum of the file. An example JWT
|
||||
|
|
@ -348,9 +356,17 @@ If the requested file matches the requested SHA256 sum, then the Geo
|
|||
feature, which allows NGINX to handle the file transfer without tying
|
||||
up Rails or Workhorse.
|
||||
|
||||
NOTE:
|
||||
JWT requires synchronized clocks between the machines
|
||||
involved, otherwise it may fail with an encryption error.
|
||||
### Git transfers
|
||||
|
||||
When the **secondary** site wishes to clone or fetch a Git repository from the
|
||||
**primary** site, the JWT payload contains additional information to identify
|
||||
the Git repository request. This ensures that the **secondary** site downloads
|
||||
the right Git repository for the right database ID. An example JWT
|
||||
payload looks like:
|
||||
|
||||
```yaml
|
||||
{"data": {scope: "mygroup/myproject"}, iat: "1234567890"}
|
||||
```
|
||||
|
||||
## Git Push to Geo secondary
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,104 @@ track_event(
|
|||
)
|
||||
```
|
||||
|
||||
### Backend testing
|
||||
|
||||
When testing code that simply triggers an internal event and make sure it increments all the related metrics,
|
||||
you can use the `internal_event_tracking` shared example.
|
||||
|
||||
```ruby
|
||||
it_behaves_like 'internal event tracking' do
|
||||
let(:event) { 'create_new_issue' }
|
||||
let(:project) { issue.project }
|
||||
let(:user) { issue.author }
|
||||
let(:namespace) { group }
|
||||
subject(:service_action) { described_class.new(issue).save }
|
||||
end
|
||||
```
|
||||
|
||||
It requires a context containing:
|
||||
|
||||
- `subject` - the action that triggers the event
|
||||
- `event` - the name of the event
|
||||
|
||||
Optionally, the context can contain:
|
||||
|
||||
- `user`
|
||||
- `project`
|
||||
- `namespace`. If not provided, `project.namespace` will be used (if `project` is available).
|
||||
- `category`
|
||||
- `label`
|
||||
- `property`
|
||||
- `value`
|
||||
- `event_attribute_overrides` - is used when its necessary to override the attributes available in parent context. For example:
|
||||
|
||||
```ruby
|
||||
let(:event) { 'create_new_issue' }
|
||||
|
||||
it_behaves_like 'internal event tracking' do
|
||||
let(:event_attribute_overrides) { { event: 'create_new_milestone'} }
|
||||
|
||||
subject(:service_action) { described_class.new(issue).save }
|
||||
end
|
||||
```
|
||||
|
||||
#### Composable matchers
|
||||
|
||||
When a singe action triggers an event multiple times, triggers multiple different events, or increments some metrics but not others for the event,
|
||||
you can use the `trigger_internal_events` and `increment_usage_metrics` matchers.
|
||||
|
||||
```ruby
|
||||
expect { subject }
|
||||
.to trigger_internal_events('web_ide_viewed')
|
||||
.with(user: user, project: project, namespace: namespace)
|
||||
.and increment_usage_metrics('counts.web_views')
|
||||
```
|
||||
|
||||
The `trigger_internal_events` matcher accepts the same chain methods as the [`receive`](https://rubydoc.info/github/rspec/rspec-mocks/RSpec/Mocks/ExampleMethods#receive-instance_method) matcher (`#once`, `#at_most`, etc). By default, it expects the provided events to be triggered only once.
|
||||
|
||||
The chain method `#with` accepts following parameters:
|
||||
|
||||
- `user` - User object
|
||||
- `project` - Project object
|
||||
- `namespace` - Namespace object. If not provided, it will be set to `project.namespace`
|
||||
- `additional_properties` - Hash. Additional properties to be sent with the event. For example: `{ label: 'scheduled', value: 20 }`
|
||||
- `category` - String. If not provided, it will be set to the class name of the object that triggers the event
|
||||
|
||||
The `increment_usage_metrics` matcher accepts the same chain methods as the [`change`](https://rubydoc.info/gems/rspec-expectations/RSpec%2FMatchers:change) matcher (`#by`, `#from`, `#to`, etc). By default, it expects the provided metrics to be incremented by one.
|
||||
|
||||
```ruby
|
||||
expect { subject }
|
||||
.to trigger_internal_events('web_ide_viewed')
|
||||
.with(user: user, project: project, namespace: namespace)
|
||||
.exactly(3).times
|
||||
```
|
||||
|
||||
Both matchers are composable with other matchers that act on a block (like `change` matcher).
|
||||
|
||||
```ruby
|
||||
expect { subject }
|
||||
.to trigger_internal_events('mr_created')
|
||||
.with(user: user, project: project, category: category, additional_properties: { label: label } )
|
||||
.and increment_usage_metrics('counts.deployments')
|
||||
.at_least(:once)
|
||||
.and change { mr.notes.count }.by(1)
|
||||
```
|
||||
|
||||
To test that an event was not triggered, you can use the `not_trigger_internal_events` matcher. It does not accept message chains.
|
||||
|
||||
```ruby
|
||||
expect { subject }.to trigger_internal_events('mr_created')
|
||||
.with(user: user, project: project, namespace: namespace)
|
||||
.and increment_usage_metrics('counts.deployments')
|
||||
.and not_trigger_internal_events('pipeline_started')
|
||||
```
|
||||
|
||||
Or you can use the `not_to` syntax:
|
||||
|
||||
```ruby
|
||||
expect { subject }.not_to trigger_internal_events('mr_created', 'member_role_created')
|
||||
```
|
||||
|
||||
### Frontend tracking
|
||||
|
||||
Any frontend tracking call automatically passes the values `user.id`, `namespace.id`, and `project.id` from the current context of the page.
|
||||
|
|
|
|||
|
|
@ -451,15 +451,17 @@ When your license expires, GitLab locks down features, like Git pushes
|
|||
and issue creation. Then, your instance becomes read-only and
|
||||
an expiration message is displayed to all administrators.
|
||||
|
||||
Licenses expire at the start of the expiration date, 00:00 server time.
|
||||
|
||||
For GitLab self-managed instances, you have a 14-day grace period
|
||||
before this occurs.
|
||||
|
||||
For example, if a license has a start date of January 1, 2024 and an end date of January 1, 2025:
|
||||
|
||||
- It expires at 11:59:59 PM UTC December 31, 2024.
|
||||
- It is considered expired from 12:00:00 AM UTC January 1, 2025.
|
||||
- The grace period of 14 days starts at 12:00:00 AM UTC January 1, 2025 and ends at 11:59:59 PM UTC January 14, 2025.
|
||||
- Your instance becomes read-only at 12:00:00 AM UTC January 15, 2025.
|
||||
- It expires at 11:59:59 PM server time December 31, 2024.
|
||||
- It is considered expired from 12:00:00 AM server time January 1, 2025.
|
||||
- The grace period of 14 days starts at 12:00:00 AM server time January 1, 2025 and ends at 11:59:59 PM server time January 14, 2025.
|
||||
- Your instance becomes read-only at 12:00:00 AM server time January 15, 2025.
|
||||
|
||||
- To resume functionality, activate a new license.
|
||||
- To fall back to Free features, delete the expired license.
|
||||
|
|
|
|||
|
|
@ -19490,6 +19490,9 @@ msgstr ""
|
|||
msgid "Due to inactivity, this project is scheduled to be deleted on %{deletion_date}. %{link_start}Why is this scheduled?%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoChat|An administrator has turned off GitLab Duo for this %{reason}"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoChat|Ask a question about GitLab"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19517,6 +19520,12 @@ msgstr ""
|
|||
msgid "DuoChat|What is a fork?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoChat|group"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoChat|project"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoCodeReview|Hey :wave: I'm starting to review your merge request and I will let you know when I'm finished."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ gem 'chemlab-library-www-gitlab-com', '~> 0.1', '>= 0.1.1'
|
|||
gem 'chemlab-library-gitlab', path: 'gems/chemlab-gitlab'
|
||||
|
||||
# dependencies for jenkins client
|
||||
gem 'nokogiri', '~> 1.16', '>= 1.16.6'
|
||||
gem 'nokogiri', '~> 1.16', '>= 1.16.7'
|
||||
|
||||
gem 'deprecation_toolkit', '~> 2.2.0', require: false
|
||||
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ GEM
|
|||
net-http (0.4.1)
|
||||
uri
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.16.6)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
octokit (9.1.0)
|
||||
|
|
@ -410,7 +410,7 @@ DEPENDENCIES
|
|||
influxdb-client (~> 3.1)
|
||||
junit_merge (~> 0.1.2)
|
||||
knapsack (~> 4.0)
|
||||
nokogiri (~> 1.16, >= 1.16.6)
|
||||
nokogiri (~> 1.16, >= 1.16.7)
|
||||
octokit (~> 9.1.0)
|
||||
parallel (~> 1.25, >= 1.25.1)
|
||||
parallel_tests (~> 4.7, >= 4.7.1)
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Page
|
||||
module Group
|
||||
module Settings
|
||||
class Billing < Chemlab::Page
|
||||
h4 :billing_plan_header
|
||||
link :start_your_free_trial
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Page
|
||||
module Group
|
||||
module Settings
|
||||
module Billing
|
||||
# @note Defined as +h4 :billing_plan_header+
|
||||
# @return [String] The text content or value of +billing_plan_header+
|
||||
def billing_plan_header
|
||||
# This is a stub, used for indexing. The method is dynamically generated.
|
||||
end
|
||||
|
||||
# @example
|
||||
# Gitlab::Page::Group::Settings::Billing.perform do |billing|
|
||||
# expect(billing.billing_plan_header_element).to exist
|
||||
# end
|
||||
# @return [Watir::H4] The raw +H4+ element
|
||||
def billing_plan_header_element
|
||||
# This is a stub, used for indexing. The method is dynamically generated.
|
||||
end
|
||||
|
||||
# @example
|
||||
# Gitlab::Page::Group::Settings::Billing.perform do |billing|
|
||||
# expect(billing).to be_billing_plan_header
|
||||
# end
|
||||
# @return [Boolean] true if the +billing_plan_header+ element is present on the page
|
||||
def billing_plan_header?
|
||||
# This is a stub, used for indexing. The method is dynamically generated.
|
||||
end
|
||||
|
||||
# @note Defined as +link :start_your_free_trial+
|
||||
# Clicks +start_your_free_trial+
|
||||
def start_your_free_trial
|
||||
# This is a stub, used for indexing. The method is dynamically generated.
|
||||
end
|
||||
|
||||
# @example
|
||||
# Gitlab::Page::Group::Settings::Billing.perform do |billing|
|
||||
# expect(billing.start_your_free_trial_element).to exist
|
||||
# end
|
||||
# @return [Watir::Link] The raw +Link+ element
|
||||
def start_your_free_trial_element
|
||||
# This is a stub, used for indexing. The method is dynamically generated.
|
||||
end
|
||||
|
||||
# @example
|
||||
# Gitlab::Page::Group::Settings::Billing.perform do |billing|
|
||||
# expect(billing).to be_start_your_free_trial
|
||||
# end
|
||||
# @return [Boolean] true if the +start_your_free_trial+ element is present on the page
|
||||
def start_your_free_trial?
|
||||
# This is a stub, used for indexing. The method is dynamically generated.
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -66,7 +66,6 @@ RSpec.describe Pajamas::BannerComponent, type: :component do
|
|||
context 'by default (promotion)' do
|
||||
it 'does not apply introduction class' do
|
||||
expect(page).not_to have_css ".gl-banner-introduction"
|
||||
expect(page).to have_css ".gl-banner.gl-bg-gray-10\\!"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,17 @@
|
|||
module Pajamas
|
||||
class AccordionComponentPreview < ViewComponent::Preview
|
||||
# @param title text
|
||||
# @param body text
|
||||
# @param state
|
||||
def default(title: "Accordion title (open)", body: "Accordion body", state: :opened)
|
||||
render(Pajamas::AccordionComponent.new(
|
||||
def default(title: "Accordion title (open)", state: :opened)
|
||||
render(Pajamas::AccordionItemComponent.new(
|
||||
title: title,
|
||||
body: body,
|
||||
state: state
|
||||
))
|
||||
end
|
||||
|
||||
def closed(title: "Accordion title (closed)", body: "Accordion body", state: :closed)
|
||||
render(Pajamas::AccordionComponent.new(
|
||||
def closed(title: "Accordion title (closed)", state: :closed)
|
||||
render(Pajamas::AccordionItemComponent.new(
|
||||
title: title,
|
||||
body: body,
|
||||
state: state
|
||||
))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -116,9 +116,7 @@ describe('Manual Variables Form', () => {
|
|||
|
||||
it('renders help text with provided link', () => {
|
||||
expect(findHelpText().exists()).toBe(true);
|
||||
expect(findHelpLink().attributes('href')).toBe(
|
||||
'/help/ci/variables/index#add-a-cicd-variable-to-a-project',
|
||||
);
|
||||
expect(findHelpLink().attributes('href')).toBe('/help/ci/variables/index#for-a-project');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ RSpec.describe IconsHelper do
|
|||
describe 'gl_loading_icon' do
|
||||
it 'returns the default spinner markup' do
|
||||
expect(gl_loading_icon.to_s)
|
||||
.to eq '<div class="gl-spinner-container" role="status"><span aria-label="Loading" class="gl-spinner gl-spinner-sm gl-spinner-dark gl-vertical-align-text-bottom!"></span></div>'
|
||||
.to eq '<div class="gl-spinner-container" role="status"><span aria-label="Loading" class="gl-spinner gl-spinner-sm gl-spinner-dark !gl-align-text-bottom"></span></div>'
|
||||
end
|
||||
|
||||
context 'when css_class is provided' do
|
||||
|
|
|
|||
|
|
@ -121,9 +121,7 @@ RSpec.describe Clusters::ClusterPresenter do
|
|||
it do
|
||||
is_expected.to include(
|
||||
'clusters-path': clusterable_presenter.index_path,
|
||||
'dashboard-endpoint': clusterable_presenter.metrics_dashboard_path(cluster),
|
||||
'documentation-path': help_page_path('user/infrastructure/clusters/manage/clusters_health'),
|
||||
'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index', anchor: 'add-a-new-dashboard-to-your-project'),
|
||||
'settings-path': '',
|
||||
'project-path': '',
|
||||
'tags-path': ''
|
||||
|
|
|
|||
|
|
@ -67,12 +67,6 @@ RSpec.describe GroupClusterablePresenter do
|
|||
it { is_expected.to eq(group_cluster_path(group, cluster)) }
|
||||
end
|
||||
|
||||
describe '#metrics_dashboard_path' do
|
||||
subject { presenter.metrics_dashboard_path(cluster) }
|
||||
|
||||
it { is_expected.to eq(metrics_dashboard_group_cluster_path(group, cluster)) }
|
||||
end
|
||||
|
||||
describe '#learn_more_link' do
|
||||
subject { presenter.learn_more_link }
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,6 @@ RSpec.describe InstanceClusterablePresenter do
|
|||
it { is_expected.to eq(clear_cache_admin_cluster_path(cluster)) }
|
||||
end
|
||||
|
||||
describe '#metrics_dashboard_path' do
|
||||
subject { presenter.metrics_dashboard_path(cluster) }
|
||||
|
||||
it { is_expected.to eq(metrics_dashboard_admin_cluster_path(cluster)) }
|
||||
end
|
||||
|
||||
describe '#learn_more_link' do
|
||||
subject { presenter.learn_more_link }
|
||||
|
||||
|
|
|
|||
|
|
@ -73,12 +73,6 @@ RSpec.describe ProjectClusterablePresenter, feature_category: :environment_manag
|
|||
it { is_expected.to eq(project_cluster_path(project, cluster)) }
|
||||
end
|
||||
|
||||
describe '#metrics_dashboard_path' do
|
||||
subject { presenter.metrics_dashboard_path(cluster) }
|
||||
|
||||
it { is_expected.to eq(metrics_dashboard_project_cluster_path(project, cluster)) }
|
||||
end
|
||||
|
||||
describe '#learn_more_link' do
|
||||
subject { presenter.learn_more_link }
|
||||
|
||||
|
|
|
|||
|
|
@ -158,5 +158,59 @@ RSpec.describe RemoveExpiredMembersWorker, feature_category: :system_access do
|
|||
worker.perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'pagination' do
|
||||
let_it_be(:expired_group_member) { create(:group_member, expires_at: 1.day.from_now, access_level: GroupMember::DEVELOPER) }
|
||||
let(:instance) { described_class.new }
|
||||
let(:cursor) { nil }
|
||||
let(:has_next_page) { true }
|
||||
let(:cursor_for_next_page) { 'next-page-cursor' }
|
||||
|
||||
let(:paginator) do
|
||||
instance_double(
|
||||
Gitlab::Pagination::Keyset::Paginator,
|
||||
has_next_page?: has_next_page,
|
||||
cursor_for_next_page: cursor_for_next_page
|
||||
)
|
||||
end
|
||||
|
||||
subject(:perform) { instance.perform(cursor) }
|
||||
|
||||
before do
|
||||
allow(paginator).to receive(:each).and_yield(expired_group_member)
|
||||
travel_to(3.days.from_now)
|
||||
end
|
||||
|
||||
it 'logs completed row count and enqueues next batch' do
|
||||
allow(instance).to receive(:paginate).and_return(paginator)
|
||||
expect(instance).to receive(:log_extra_metadata_on_done).with(:result, status: :limit_reached, updated_rows: 1)
|
||||
expect(described_class).to receive(:perform_in).with(described_class::BATCH_DELAY, 'next-page-cursor')
|
||||
|
||||
perform
|
||||
end
|
||||
|
||||
context 'when initialized with cursor' do
|
||||
let(:cursor) { 'fake-base64-encoded-data' }
|
||||
|
||||
it 'passes cursor to paginate method' do
|
||||
expect(instance).to receive(:paginate).with(cursor).and_return(paginator)
|
||||
|
||||
perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'when last page is reached' do
|
||||
let(:has_next_page) { false }
|
||||
let(:cursor_for_next_page) { nil }
|
||||
|
||||
it 'logs completed row count and does not enqueue next batch' do
|
||||
allow(instance).to receive(:paginate).and_return(paginator)
|
||||
expect(instance).to receive(:log_extra_metadata_on_done).with(:result, status: :completed, updated_rows: 1)
|
||||
expect(described_class).not_to receive(:perform_async)
|
||||
|
||||
perform
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue