Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-29 09:07:34 +00:00
parent 1906d8f913
commit 5f2254b006
33 changed files with 359 additions and 305 deletions

View File

@ -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: {

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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': ''

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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 ""

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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');
});
});

View File

@ -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

View File

@ -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': ''

View File

@ -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 }

View File

@ -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 }

View File

@ -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 }

View File

@ -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