Commit bd3df45f authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 25ab40a4 7018506a
#import "./release_for_editing.fragment.graphql"
query oneReleaseForEditing($fullPath: ID!, $tagName: String!) {
project(fullPath: $fullPath) {
release(tagName: $tagName) {
...ReleaseForEditing
}
}
}
fragment ReleaseForEditing on Release {
name
tagName
description
assets {
links {
nodes {
id
name
url
linkType
}
}
}
links {
selfUrl
}
milestones {
nodes {
title
}
}
}
...@@ -52,24 +52,37 @@ const convertScalarProperties = (graphQLRelease) => ...@@ -52,24 +52,37 @@ const convertScalarProperties = (graphQLRelease) =>
'name', 'name',
'tagName', 'tagName',
'tagPath', 'tagPath',
'description',
'descriptionHtml', 'descriptionHtml',
'releasedAt', 'releasedAt',
'upcomingRelease', 'upcomingRelease',
]); ]);
const convertAssets = (graphQLRelease) => ({ const convertAssets = (graphQLRelease) => {
assets: { let sources = [];
count: graphQLRelease.assets.count, if (graphQLRelease.assets.sources?.nodes) {
sources: [...graphQLRelease.assets.sources.nodes], sources = [...graphQLRelease.assets.sources.nodes];
links: graphQLRelease.assets.links.nodes.map((l) => ({ }
let links = [];
if (graphQLRelease.assets.links?.nodes) {
links = graphQLRelease.assets.links.nodes.map((l) => ({
...l, ...l,
linkType: l.linkType?.toLowerCase(), linkType: l.linkType?.toLowerCase(),
})), }));
}
return {
assets: {
count: graphQLRelease.assets.count,
sources,
links,
}, },
}); };
};
const convertEvidences = (graphQLRelease) => ({ const convertEvidences = (graphQLRelease) => ({
evidences: graphQLRelease.evidences.nodes.map((e) => e), evidences: (graphQLRelease.evidences?.nodes ?? []).map((e) => ({ ...e })),
}); });
const convertLinks = (graphQLRelease) => ({ const convertLinks = (graphQLRelease) => ({
...@@ -100,10 +113,12 @@ const convertMilestones = (graphQLRelease) => ({ ...@@ -100,10 +113,12 @@ const convertMilestones = (graphQLRelease) => ({
...m, ...m,
webUrl: m.webPath, webUrl: m.webPath,
webPath: undefined, webPath: undefined,
issueStats: { issueStats: m.stats
? {
total: m.stats.totalIssuesCount, total: m.stats.totalIssuesCount,
closed: m.stats.closedIssuesCount, closed: m.stats.closedIssuesCount,
}, }
: {},
stats: undefined, stats: undefined,
})), })),
}); });
......
...@@ -23,7 +23,7 @@ module Types ...@@ -23,7 +23,7 @@ module Types
field :stage, Types::Ci::StageType, null: true, field :stage, Types::Ci::StageType, null: true,
description: 'Stage of the job.' description: 'Stage of the job.'
field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false, field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false,
description: 'Whether this job is allowed to fail.' description: 'Whether the job is allowed to fail.'
field :duration, GraphQL::INT_TYPE, null: true, field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the job in seconds.' description: 'Duration of the job in seconds.'
field :tags, [GraphQL::STRING_TYPE], null: true, field :tags, [GraphQL::STRING_TYPE], null: true,
...@@ -41,6 +41,12 @@ module Types ...@@ -41,6 +41,12 @@ module Types
field :scheduled_at, Types::TimeType, null: true, field :scheduled_at, Types::TimeType, null: true,
description: 'Schedule for the build.' description: 'Schedule for the build.'
# Life-cycle durations:
field :queued_duration,
type: Types::DurationType,
null: true,
description: 'How long the job was enqueued before starting.'
field :detailed_status, Types::Ci::DetailedStatusType, null: true, field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job.' description: 'Detailed status of the job.'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true, field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
......
...@@ -39,6 +39,9 @@ module Types ...@@ -39,6 +39,9 @@ module Types
field :duration, GraphQL::INT_TYPE, null: true, field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the pipeline in seconds.' description: 'Duration of the pipeline in seconds.'
field :queued_duration, Types::DurationType, null: true,
description: 'How long the pipeline was queued before starting.'
field :coverage, GraphQL::FLOAT_TYPE, null: true, field :coverage, GraphQL::FLOAT_TYPE, null: true,
description: 'Coverage percentage.' description: 'Coverage percentage.'
......
# frozen_string_literal: true
module Types
class DurationType < BaseScalar
graphql_name 'Duration'
description <<~DESC
Duration between two instants, represented as a fractional number of seconds.
For example: 12.3334
DESC
def self.coerce_input(value, ctx)
case value
when Float
value
when Integer
value.to_f
when NilClass
raise GraphQL::CoercionError, 'Cannot be nil'
else
raise GraphQL::CoercionError, "Expected number: got #{value.class}"
end
end
def self.coerce_result(value, ctx)
value.to_f
end
end
end
...@@ -1047,7 +1047,7 @@ module Ci ...@@ -1047,7 +1047,7 @@ module Ci
end end
def build_data def build_data
@build_data ||= Gitlab::DataBuilder::Build.build(self) strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) }
end end
def successful_deployment_status def successful_deployment_status
......
...@@ -214,8 +214,14 @@ class CommitStatus < ApplicationRecord ...@@ -214,8 +214,14 @@ class CommitStatus < ApplicationRecord
allow_failure? && (failed? || canceled?) allow_failure? && (failed? || canceled?)
end end
# Time spent running.
def duration def duration
calculate_duration calculate_duration(started_at, finished_at)
end
# Time spent in the pending state.
def queued_duration
calculate_duration(queued_at, started_at)
end end
def latest? def latest?
......
...@@ -122,12 +122,10 @@ module Ci ...@@ -122,12 +122,10 @@ module Ci
private private
def calculate_duration def calculate_duration(start_time, end_time)
if started_at && finished_at return unless start_time
finished_at - started_at
elsif started_at (end_time || Time.current) - start_time
Time.current - started_at
end
end end
end end
end end
---
title: Expose job and project queued duration in all APIs
merge_request: 59901
author:
type: changed
...@@ -15,7 +15,7 @@ performance, data, or could even exhaust the allocated resources for the applica ...@@ -15,7 +15,7 @@ performance, data, or could even exhaust the allocated resources for the applica
Rate limits can be used to improve the security and durability of GitLab. Rate limits can be used to improve the security and durability of GitLab.
For example, a simple script can make thousands of web requests per second. Whether malicious, apathetic, or just a bug, your application and infrastructure may not be able to cope with the load. Rate limits can help mitigate these types of attacks. For example, one script can make thousands of web requests per second. Whether malicious, apathetic, or just a bug, your application and infrastructure may not be able to cope with the load. Rate limits can help to mitigate these types of attacks.
Read more about [configuring rate limits](../security/rate_limits.md) in the Security documentation. Read more about [configuring rate limits](../security/rate_limits.md) in the Security documentation.
...@@ -25,17 +25,17 @@ Read more about [configuring rate limits](../security/rate_limits.md) in the Sec ...@@ -25,17 +25,17 @@ Read more about [configuring rate limits](../security/rate_limits.md) in the Sec
This setting limits the request rate to the issue creation endpoint. This setting limits the request rate to the issue creation endpoint.
Read more on [issue creation rate limits](../user/admin_area/settings/rate_limit_on_issues_creation.md). Read more about [issue creation rate limits](../user/admin_area/settings/rate_limit_on_issues_creation.md).
- **Default rate limit** - Disabled by default - **Default rate limit**: Disabled by default.
### By User or IP ### By User or IP
This setting limits the request rate per user or IP. This setting limits the request rate per user or IP.
Read more on [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md). Read more about [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md).
- **Default rate limit** - Disabled by default - **Default rate limit**: Disabled by default.
### By raw endpoint ### By raw endpoint
...@@ -43,9 +43,9 @@ Read more on [User and IP rate limits](../user/admin_area/settings/user_and_ip_r ...@@ -43,9 +43,9 @@ Read more on [User and IP rate limits](../user/admin_area/settings/user_and_ip_r
This setting limits the request rate per endpoint. This setting limits the request rate per endpoint.
Read more on [raw endpoint rate limits](../user/admin_area/settings/rate_limits_on_raw_endpoints.md). Read more about [raw endpoint rate limits](../user/admin_area/settings/rate_limits_on_raw_endpoints.md).
- **Default rate limit** - 300 requests per project, per commit and per file path - **Default rate limit**: 300 requests per project, per commit and per file path.
### By protected path ### By protected path
...@@ -65,9 +65,9 @@ GitLab rate limits the following paths by default: ...@@ -65,9 +65,9 @@ GitLab rate limits the following paths by default:
'/admin/session' '/admin/session'
``` ```
Read more on [protected path rate limits](../user/admin_area/settings/protected_paths.md). Read more about [protected path rate limits](../user/admin_area/settings/protected_paths.md).
- **Default rate limit** - After 10 requests, the client must wait 60 seconds before trying again - **Default rate limit**: After 10 requests, the client must wait 60 seconds before trying again.
### Package Registry ### Package Registry
...@@ -76,7 +76,7 @@ Read more on [protected path rate limits](../user/admin_area/settings/protected_ ...@@ -76,7 +76,7 @@ Read more on [protected path rate limits](../user/admin_area/settings/protected_
This setting limits the request rate on the Packages API per user or IP. For more information, see This setting limits the request rate on the Packages API per user or IP. For more information, see
[Package Registry Rate Limits](../user/admin_area/settings/package_registry_rate_limits.md). [Package Registry Rate Limits](../user/admin_area/settings/package_registry_rate_limits.md).
- **Default rate limit:** Disabled by default - **Default rate limit**: Disabled by default.
### Import/Export ### Import/Export
...@@ -85,15 +85,15 @@ This setting limits the request rate on the Packages API per user or IP. For mor ...@@ -85,15 +85,15 @@ This setting limits the request rate on the Packages API per user or IP. For mor
This setting limits the import/export actions for groups and projects. This setting limits the import/export actions for groups and projects.
| Limit | Default (per minute per user) | | Limit | Default (per minute per user) |
| ----- | ----------------------------- | |-------------------------|-------------------------------|
| Project Import | 6 | | Project Import | 6 |
| Project Export | 6 | | Project Export | 6 |
| Project Export Download | 1 | | Project Export Download | 1 |
| Group Import | 6 | | Group Import | 6 |
| Group Export | 6 | | Group Export | 6 |
| Group Export | Download | 1 | | Group Export Download | 1 |
Read more on [import/export rate limits](../user/admin_area/settings/import_export_rate_limits.md). Read more about [import/export rate limits](../user/admin_area/settings/import_export_rate_limits.md).
### Rack attack ### Rack attack
...@@ -101,9 +101,9 @@ This method of rate limiting is cumbersome, but has some advantages. It allows ...@@ -101,9 +101,9 @@ This method of rate limiting is cumbersome, but has some advantages. It allows
throttling of specific paths, and is also integrated into Git and container throttling of specific paths, and is also integrated into Git and container
registry requests. registry requests.
Read more on the [Rack Attack initializer](../security/rack_attack.md) method of setting rate limits. Read more about the [Rack Attack initializer](../security/rack_attack.md) method of setting rate limits.
- **Default rate limit** - Disabled - **Default rate limit**: Disabled.
### Member Invitations ### Member Invitations
...@@ -116,11 +116,11 @@ Limit the maximum daily member invitations allowed per group hierarchy. ...@@ -116,11 +116,11 @@ Limit the maximum daily member invitations allowed per group hierarchy.
Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitaly's configuration file. Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitaly's configuration file.
Read more on [Gitaly concurrency limits](gitaly/configure_gitaly.md#limit-rpc-concurrency). Read more about [Gitaly concurrency limits](gitaly/configure_gitaly.md#limit-rpc-concurrency).
- **Default rate limit** - Disabled - **Default rate limit**: Disabled.
## Number of comments per issue, merge request or commit ## Number of comments per issue, merge request, or commit
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22388) in GitLab 12.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22388) in GitLab 12.4.
...@@ -129,7 +129,7 @@ merge request, or commit. When the limit is reached, system notes can still be ...@@ -129,7 +129,7 @@ merge request, or commit. When the limit is reached, system notes can still be
added so that the history of events is not lost, but user-submitted comments added so that the history of events is not lost, but user-submitted comments
will fail. will fail.
- **Max limit:** 5.000 comments - **Max limit**: 5,000 comments.
## Size of comments and descriptions of issues, merge requests, and epics ## Size of comments and descriptions of issues, merge requests, and epics
...@@ -141,7 +141,7 @@ item will not be created. ...@@ -141,7 +141,7 @@ item will not be created.
It's possible that this limit will be changed to a lower number in the future. It's possible that this limit will be changed to a lower number in the future.
- **Max size:** ~1 million characters / ~1 MB - **Max size**: ~1 million characters / ~1 MB.
## Size of commit titles and descriptions ## Size of commit titles and descriptions
...@@ -161,7 +161,7 @@ The maximum number of issues loaded on the milestone overview page is 500. ...@@ -161,7 +161,7 @@ The maximum number of issues loaded on the milestone overview page is 500.
When the number exceeds the limit the page displays an alert and links to a paginated When the number exceeds the limit the page displays an alert and links to a paginated
[issue list](../user/project/issues/managing_issues.md) of all issues in the milestone. [issue list](../user/project/issues/managing_issues.md) of all issues in the milestone.
- **Limit:** 500 issues - **Limit**: 500 issues.
## Number of pipelines per Git push ## Number of pipelines per Git push
...@@ -183,13 +183,13 @@ Activity history for projects and individuals' profiles was limited to one year ...@@ -183,13 +183,13 @@ Activity history for projects and individuals' profiles was limited to one year
There is a limit when embedding metrics in GFM for performance reasons. There is a limit when embedding metrics in GFM for performance reasons.
- **Max limit:** 100 embeds - **Max limit**: 100 embeds.
## Number of webhooks ## Number of webhooks
On GitLab.com, the [maximum number of webhooks and their size](../user/gitlab_com/index.md#webhooks) per project, and per group, is limited. On GitLab.com, the [maximum number of webhooks and their size](../user/gitlab_com/index.md#webhooks) per project, and per group, is limited.
To set this limit on a self-managed installation, where the default is `100` project webhooks and `50` group webhooks, run the following in the To set this limit for a self-managed installation, where the default is `100` project webhooks and `50` group webhooks, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby ```ruby
...@@ -212,7 +212,7 @@ Set the limit to `0` to disable it. ...@@ -212,7 +212,7 @@ Set the limit to `0` to disable it.
The [minimum time between pull refreshes](../user/project/repository/repository_mirroring.md) The [minimum time between pull refreshes](../user/project/repository/repository_mirroring.md)
defaults to 300 seconds (5 minutes). defaults to 300 seconds (5 minutes).
To change this limit on a self-managed installation, run the following in the To change this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby ```ruby
...@@ -229,14 +229,14 @@ Plan.default.actual_limits.update!(pull_mirror_interval_seconds: 200) ...@@ -229,14 +229,14 @@ Plan.default.actual_limits.update!(pull_mirror_interval_seconds: 200)
GitLab ignores all incoming emails sent from auto-responders by looking for the `X-Autoreply` GitLab ignores all incoming emails sent from auto-responders by looking for the `X-Autoreply`
header. Such emails don't create comments on issues or merge requests. header. Such emails don't create comments on issues or merge requests.
## Amount of data sent from Sentry via Error Tracking ## Amount of data sent from Sentry through Error Tracking
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14926) in GitLab 12.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14926) in GitLab 12.6.
Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons
and to limit memory consumption. and to limit memory consumption.
## Max offset allowed via REST API for offset-based pagination ## Max offset allowed by the REST API for offset-based pagination
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0.
...@@ -245,7 +245,7 @@ requested offset into the set of results. This limit is only applied to endpoint ...@@ -245,7 +245,7 @@ requested offset into the set of results. This limit is only applied to endpoint
support keyset-based pagination. More information about pagination options can be support keyset-based pagination. More information about pagination options can be
found in the [API docs section on pagination](../api/README.md#pagination). found in the [API docs section on pagination](../api/README.md#pagination).
To set this limit on a self-managed installation, run the following in the To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby ```ruby
...@@ -255,7 +255,7 @@ To set this limit on a self-managed installation, run the following in the ...@@ -255,7 +255,7 @@ To set this limit on a self-managed installation, run the following in the
Plan.default.actual_limits.update!(offset_pagination_limit: 10000) Plan.default.actual_limits.update!(offset_pagination_limit: 10000)
``` ```
- **Default offset pagination limit:** 50000 - **Default offset pagination limit**: `50000`.
Set the limit to `0` to disable it. Set the limit to `0` to disable it.
...@@ -281,7 +281,7 @@ will fail with a `job_activity_limit_exceeded` error. ...@@ -281,7 +281,7 @@ will fail with a `job_activity_limit_exceeded` error.
higher installations, this limit is defined under a `default` plan that affects all higher installations, this limit is defined under a `default` plan that affects all
projects. This limit is disabled (`0`) by default. projects. This limit is disabled (`0`) by default.
To set this limit on a self-managed installation, run the following in the To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby ```ruby
...@@ -304,7 +304,7 @@ too many deployments fail with a `deployments_limit_exceeded` error. ...@@ -304,7 +304,7 @@ too many deployments fail with a `deployments_limit_exceeded` error.
The default limit is 500 for all [GitLab self-managed and SaaS plans](https://about.gitlab.com/pricing/). The default limit is 500 for all [GitLab self-managed and SaaS plans](https://about.gitlab.com/pricing/).
To change the limit on a self-managed installation, change the `default` plan's limit with the following To change the limit for a self-managed installation, change the `default` plan's limit with the following
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session) command: [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session) command:
```ruby ```ruby
...@@ -332,7 +332,7 @@ limit, the subscription will be considered invalid. ...@@ -332,7 +332,7 @@ limit, the subscription will be considered invalid.
or higher installations, this limit is defined under a `default` plan that or higher installations, this limit is defined under a `default` plan that
affects all projects. By default, there is a limit of `2` subscriptions. affects all projects. By default, there is a limit of `2` subscriptions.
To set this limit on a self-managed installation, run the following in the To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby ```ruby
...@@ -357,7 +357,7 @@ On [GitLab Premium](https://about.gitlab.com/pricing/) self-managed or ...@@ -357,7 +357,7 @@ On [GitLab Premium](https://about.gitlab.com/pricing/) self-managed or
higher installations, this limit is defined under a `default` plan that affects all higher installations, this limit is defined under a `default` plan that affects all
projects. By default, there is a limit of `10` pipeline schedules. projects. By default, there is a limit of `10` pipeline schedules.
To set this limit on a self-managed installation, run the following in the To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby ```ruby
...@@ -505,11 +505,11 @@ See [Environment Dashboard](../ci/environments/environments_dashboard.md#adding- ...@@ -505,11 +505,11 @@ See [Environment Dashboard](../ci/environments/environments_dashboard.md#adding-
Pods and Deployments. However, data over 10 MB for a certain environment read from Pods and Deployments. However, data over 10 MB for a certain environment read from
Kubernetes won't be shown. Kubernetes won't be shown.
## Merge Request reports ## Merge request reports
Reports that go over the 20 MB limit won't be loaded. Affected reports: Reports that go over the 20 MB limit won't be loaded. Affected reports:
- [Merge Request security reports](../user/project/merge_requests/testing_and_reports_in_merge_requests.md#security-reports) - [Merge request security reports](../user/project/merge_requests/testing_and_reports_in_merge_requests.md#security-reports)
- [CI/CD parameter `artifacts:expose_as`](../ci/yaml/README.md#artifactsexpose_as) - [CI/CD parameter `artifacts:expose_as`](../ci/yaml/README.md#artifactsexpose_as)
- [Unit test reports](../ci/unit_test_reports.md) - [Unit test reports](../ci/unit_test_reports.md)
...@@ -523,8 +523,8 @@ You can set a limit on the content of repository files that are indexed in ...@@ -523,8 +523,8 @@ You can set a limit on the content of repository files that are indexed in
Elasticsearch. Any files larger than this limit will not be indexed, and thus Elasticsearch. Any files larger than this limit will not be indexed, and thus
will not be searchable. will not be searchable.
Setting a limit helps reduce the memory usage of the indexing processes as well Setting a limit helps reduce the memory usage of the indexing processes and
as the overall index size. This value defaults to `1024 KiB` (1 MiB) as any the overall index size. This value defaults to `1024 KiB` (1 MiB) as any
text files larger than this likely aren't meant to be read by humans. text files larger than this likely aren't meant to be read by humans.
You must set a limit, as unlimited file sizes aren't supported. Setting this You must set a limit, as unlimited file sizes aren't supported. Setting this
...@@ -544,8 +544,8 @@ This is applicable to all indexed data except repository files that get ...@@ -544,8 +544,8 @@ This is applicable to all indexed data except repository files that get
indexed, which have a separate limit (see [Maximum file size indexed, which have a separate limit (see [Maximum file size
indexed](#maximum-file-size-indexed)). indexed](#maximum-file-size-indexed)).
- On GitLab.com this is limited to 20000 characters - On GitLab.com, this is limited to 20,000 characters
- For self-managed installations it is unlimited by default - For self-managed installations, this is unlimited by default
This limit can be configured for self-managed installations when [enabling This limit can be configured for self-managed installations when [enabling
Elasticsearch](../integration/elasticsearch.md#enabling-advanced-search). Elasticsearch](../integration/elasticsearch.md#enabling-advanced-search).
...@@ -559,7 +559,7 @@ Set the limit to `0` to disable it. ...@@ -559,7 +559,7 @@ Set the limit to `0` to disable it.
## Snippets limits ## Snippets limits
See the [documentation on Snippets settings](snippets/index.md). See the [documentation about Snippets settings](snippets/index.md).
## Design Management limits ## Design Management limits
...@@ -596,14 +596,14 @@ More information can be found in the [Push event activities limit and bulk push ...@@ -596,14 +596,14 @@ More information can be found in the [Push event activities limit and bulk push
On GitLab.com, the maximum file size for a package that's uploaded to the [GitLab Package Registry](../user/packages/package_registry/index.md) varies by format: On GitLab.com, the maximum file size for a package that's uploaded to the [GitLab Package Registry](../user/packages/package_registry/index.md) varies by format:
- Conan: 5GB - Conan: 5 GB
- Generic: 5GB - Generic: 5 GB
- Maven: 5GB - Maven: 5 GB
- npm: 5GB - npm: 5 GB
- NuGet: 5GB - NuGet: 5 GB
- PyPI: 5GB - PyPI: 5 GB
To set this limit on a self-managed installation, run the following in the To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby ```ruby
......
...@@ -7162,7 +7162,7 @@ Represents the total number of issues and their weights for a particular day. ...@@ -7162,7 +7162,7 @@ Represents the total number of issues and their weights for a particular day.
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="cijobactive"></a>`active` | [`Boolean!`](#boolean) | Indicates the job is active. | | <a id="cijobactive"></a>`active` | [`Boolean!`](#boolean) | Indicates the job is active. |
| <a id="cijoballowfailure"></a>`allowFailure` | [`Boolean!`](#boolean) | Whether this job is allowed to fail. | | <a id="cijoballowfailure"></a>`allowFailure` | [`Boolean!`](#boolean) | Whether the job is allowed to fail. |
| <a id="cijobartifacts"></a>`artifacts` | [`CiJobArtifactConnection`](#cijobartifactconnection) | Artifacts generated by the job. | | <a id="cijobartifacts"></a>`artifacts` | [`CiJobArtifactConnection`](#cijobartifactconnection) | Artifacts generated by the job. |
| <a id="cijobcancelable"></a>`cancelable` | [`Boolean!`](#boolean) | Indicates the job can be canceled. | | <a id="cijobcancelable"></a>`cancelable` | [`Boolean!`](#boolean) | Indicates the job can be canceled. |
| <a id="cijobcommitpath"></a>`commitPath` | [`String`](#string) | Path to the commit that triggered the job. | | <a id="cijobcommitpath"></a>`commitPath` | [`String`](#string) | Path to the commit that triggered the job. |
...@@ -7179,6 +7179,7 @@ Represents the total number of issues and their weights for a particular day. ...@@ -7179,6 +7179,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cijobpipeline"></a>`pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. | | <a id="cijobpipeline"></a>`pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. |
| <a id="cijobplayable"></a>`playable` | [`Boolean!`](#boolean) | Indicates the job can be played. | | <a id="cijobplayable"></a>`playable` | [`Boolean!`](#boolean) | Indicates the job can be played. |
| <a id="cijobqueuedat"></a>`queuedAt` | [`Time`](#time) | When the job was enqueued and marked as pending. | | <a id="cijobqueuedat"></a>`queuedAt` | [`Time`](#time) | When the job was enqueued and marked as pending. |
| <a id="cijobqueuedduration"></a>`queuedDuration` | [`Duration`](#duration) | How long the job was enqueued before starting. |
| <a id="cijobrefname"></a>`refName` | [`String`](#string) | Ref name of the job. | | <a id="cijobrefname"></a>`refName` | [`String`](#string) | Ref name of the job. |
| <a id="cijobrefpath"></a>`refPath` | [`String`](#string) | Path to the ref. | | <a id="cijobrefpath"></a>`refPath` | [`String`](#string) | Path to the ref. |
| <a id="cijobretryable"></a>`retryable` | [`Boolean!`](#boolean) | Indicates the job can be retried. | | <a id="cijobretryable"></a>`retryable` | [`Boolean!`](#boolean) | Indicates the job can be retried. |
...@@ -10354,6 +10355,7 @@ Information about pagination in a connection. ...@@ -10354,6 +10355,7 @@ Information about pagination in a connection.
| <a id="pipelineiid"></a>`iid` | [`String!`](#string) | Internal ID of the pipeline. | | <a id="pipelineiid"></a>`iid` | [`String!`](#string) | Internal ID of the pipeline. |
| <a id="pipelinepath"></a>`path` | [`String`](#string) | Relative path to the pipeline's page. | | <a id="pipelinepath"></a>`path` | [`String`](#string) | Relative path to the pipeline's page. |
| <a id="pipelineproject"></a>`project` | [`Project`](#project) | Project the pipeline belongs to. | | <a id="pipelineproject"></a>`project` | [`Project`](#project) | Project the pipeline belongs to. |
| <a id="pipelinequeuedduration"></a>`queuedDuration` | [`Duration`](#duration) | How long the pipeline was queued before starting. |
| <a id="pipelineretryable"></a>`retryable` | [`Boolean!`](#boolean) | Specifies if a pipeline can be retried. | | <a id="pipelineretryable"></a>`retryable` | [`Boolean!`](#boolean) | Specifies if a pipeline can be retried. |
| <a id="pipelinesecurityreportsummary"></a>`securityReportSummary` | [`SecurityReportSummary`](#securityreportsummary) | Vulnerability and scanned resource counts for each security scanner of the pipeline. | | <a id="pipelinesecurityreportsummary"></a>`securityReportSummary` | [`SecurityReportSummary`](#securityreportsummary) | Vulnerability and scanned resource counts for each security scanner of the pipeline. |
| <a id="pipelinesha"></a>`sha` | [`String!`](#string) | SHA of the pipeline's commit. | | <a id="pipelinesha"></a>`sha` | [`String!`](#string) | SHA of the pipeline's commit. |
...@@ -14464,6 +14466,12 @@ A `DiscussionID` is a global ID. It is encoded as a string. ...@@ -14464,6 +14466,12 @@ A `DiscussionID` is a global ID. It is encoded as a string.
An example `DiscussionID` is: `"gid://gitlab/Discussion/1"`. An example `DiscussionID` is: `"gid://gitlab/Discussion/1"`.
### `Duration`
Duration between two instants, represented as a fractional number of seconds.
For example: 12.3334.
### `EnvironmentID` ### `EnvironmentID`
A `EnvironmentID` is a global ID. It is encoded as a string. A `EnvironmentID` is a global ID. It is encoded as a string.
......
...@@ -43,6 +43,7 @@ Example of response ...@@ -43,6 +43,7 @@ Example of response
"started_at": "2015-12-24T17:54:27.722Z", "started_at": "2015-12-24T17:54:27.722Z",
"finished_at": "2015-12-24T17:54:27.895Z", "finished_at": "2015-12-24T17:54:27.895Z",
"duration": 0.173, "duration": 0.173,
"queued_duration": 0.010,
"artifacts_file": { "artifacts_file": {
"filename": "artifacts.zip", "filename": "artifacts.zip",
"size": 1000 "size": 1000
...@@ -107,6 +108,7 @@ Example of response ...@@ -107,6 +108,7 @@ Example of response
"started_at": "2015-12-24T17:54:24.729Z", "started_at": "2015-12-24T17:54:24.729Z",
"finished_at": "2015-12-24T17:54:24.921Z", "finished_at": "2015-12-24T17:54:24.921Z",
"duration": 0.192, "duration": 0.192,
"queued_duration": 0.023,
"artifacts_expire_at": "2016-01-23T17:54:24.921Z", "artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"tag_list": [ "tag_list": [
"docker runner", "win10-2004" "docker runner", "win10-2004"
...@@ -187,6 +189,7 @@ Example of response ...@@ -187,6 +189,7 @@ Example of response
"started_at": "2015-12-24T17:54:24.729Z", "started_at": "2015-12-24T17:54:24.729Z",
"finished_at": "2015-12-24T17:54:24.921Z", "finished_at": "2015-12-24T17:54:24.921Z",
"duration": 0.192, "duration": 0.192,
"queued_duration": 0.023,
"artifacts_expire_at": "2016-01-23T17:54:24.921Z", "artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"tag_list": [ "tag_list": [
"docker runner", "ubuntu18" "docker runner", "ubuntu18"
...@@ -241,6 +244,7 @@ Example of response ...@@ -241,6 +244,7 @@ Example of response
"started_at": "2015-12-24T17:54:27.722Z", "started_at": "2015-12-24T17:54:27.722Z",
"finished_at": "2015-12-24T17:54:27.895Z", "finished_at": "2015-12-24T17:54:27.895Z",
"duration": 0.173, "duration": 0.173,
"queued_duration": 0.023,
"artifacts_file": { "artifacts_file": {
"filename": "artifacts.zip", "filename": "artifacts.zip",
"size": 1000 "size": 1000
...@@ -339,6 +343,7 @@ Example of response ...@@ -339,6 +343,7 @@ Example of response
"started_at": "2015-12-24T17:54:27.722Z", "started_at": "2015-12-24T17:54:27.722Z",
"finished_at": "2015-12-24T17:58:27.895Z", "finished_at": "2015-12-24T17:58:27.895Z",
"duration": 240, "duration": 240,
"queued_duration": 0.123,
"id": 7, "id": 7,
"name": "teaspoon", "name": "teaspoon",
"pipeline": { "pipeline": {
...@@ -422,6 +427,7 @@ Example of response ...@@ -422,6 +427,7 @@ Example of response
"started_at": "2015-12-24T17:54:30.733Z", "started_at": "2015-12-24T17:54:30.733Z",
"finished_at": "2015-12-24T17:54:31.198Z", "finished_at": "2015-12-24T17:54:31.198Z",
"duration": 0.465, "duration": 0.465,
"queued_duration": 0.123,
"artifacts_expire_at": "2016-01-23T17:54:31.198Z", "artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"id": 8, "id": 8,
"name": "rubocop", "name": "rubocop",
...@@ -575,6 +581,7 @@ Example of response ...@@ -575,6 +581,7 @@ Example of response
"started_at": "2015-12-24T17:54:30.733Z", "started_at": "2015-12-24T17:54:30.733Z",
"finished_at": "2015-12-24T17:54:31.198Z", "finished_at": "2015-12-24T17:54:31.198Z",
"duration": 0.465, "duration": 0.465,
"queued_duration": 0.010,
"artifacts_expire_at": "2016-01-23T17:54:31.198Z", "artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"tag_list": [ "tag_list": [
"docker runner", "macos-10.15" "docker runner", "macos-10.15"
...@@ -675,6 +682,7 @@ Example of response ...@@ -675,6 +682,7 @@ Example of response
"started_at": "2016-01-11T10:14:09.526Z", "started_at": "2016-01-11T10:14:09.526Z",
"finished_at": null, "finished_at": null,
"duration": 8, "duration": 8,
"queued_duration": 0.010,
"id": 42, "id": 42,
"name": "rubocop", "name": "rubocop",
"ref": "master", "ref": "master",
...@@ -724,6 +732,7 @@ Example of response ...@@ -724,6 +732,7 @@ Example of response
"started_at": null, "started_at": null,
"finished_at": null, "finished_at": null,
"duration": null, "duration": null,
"queued_duration": 0.010,
"id": 42, "id": 42,
"name": "rubocop", "name": "rubocop",
"ref": "master", "ref": "master",
...@@ -784,6 +793,7 @@ Example of response ...@@ -784,6 +793,7 @@ Example of response
"started_at": "2016-01-11T10:13:33.506Z", "started_at": "2016-01-11T10:13:33.506Z",
"finished_at": "2016-01-11T10:15:10.506Z", "finished_at": "2016-01-11T10:15:10.506Z",
"duration": 97.0, "duration": 97.0,
"queued_duration": 0.010,
"status": "failed", "status": "failed",
"tag": false, "tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42", "web_url": "https://example.com/foo/bar/-/jobs/42",
...@@ -827,13 +837,14 @@ Example of response ...@@ -827,13 +837,14 @@ Example of response
"started_at": null, "started_at": null,
"finished_at": null, "finished_at": null,
"duration": null, "duration": null,
"queued_duration": 0.010,
"id": 42, "id": 42,
"name": "rubocop", "name": "rubocop",
"ref": "master", "ref": "master",
"artifacts": [], "artifacts": [],
"runner": null, "runner": null,
"stage": "test", "stage": "test",
"status": "started", "status": "pending",
"tag": false, "tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42", "web_url": "https://example.com/foo/bar/-/jobs/42",
"user": null "user": null
......
...@@ -117,7 +117,8 @@ Example of response ...@@ -117,7 +117,8 @@ Example of response
"started_at": null, "started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z", "finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null, "committed_at": null,
"duration": null, "duration": 123.65,
"queued_duration": 0.010,
"coverage": "30.0", "coverage": "30.0",
"web_url": "https://example.com/foo/bar/pipelines/46" "web_url": "https://example.com/foo/bar/pipelines/46"
} }
...@@ -254,6 +255,7 @@ Example of response ...@@ -254,6 +255,7 @@ Example of response
"finished_at": null, "finished_at": null,
"committed_at": null, "committed_at": null,
"duration": null, "duration": null,
"queued_duration": 0.010,
"coverage": null, "coverage": null,
"web_url": "https://example.com/foo/bar/pipelines/61" "web_url": "https://example.com/foo/bar/pipelines/61"
} }
...@@ -302,6 +304,7 @@ Response: ...@@ -302,6 +304,7 @@ Response:
"finished_at": "2016-08-11T11:32:35.145Z", "finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null, "committed_at": null,
"duration": null, "duration": null,
"queued_duration": 0.010,
"coverage": null, "coverage": null,
"web_url": "https://example.com/foo/bar/pipelines/46" "web_url": "https://example.com/foo/bar/pipelines/46"
} }
...@@ -350,6 +353,7 @@ Response: ...@@ -350,6 +353,7 @@ Response:
"finished_at": "2016-08-11T11:32:35.145Z", "finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null, "committed_at": null,
"duration": null, "duration": null,
"queued_duration": 0.010,
"coverage": null, "coverage": null,
"web_url": "https://example.com/foo/bar/pipelines/46" "web_url": "https://example.com/foo/bar/pipelines/46"
} }
......
...@@ -166,6 +166,8 @@ Each file is expected to have its own primary ID and model. Geo strongly recomme ...@@ -166,6 +166,8 @@ Each file is expected to have its own primary ID and model. Geo strongly recomme
To implement Geo replication of a new blob-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%20Replicate%20a%20new%20blob%20type). To implement Geo replication of a new blob-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%20Replicate%20a%20new%20blob%20type).
To view the implementation steps without opening an issue, [view the issue template file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Geo%20Replicate%20a%20new%20blob%20type.md).
### Repository Replicator Strategy ### Repository Replicator Strategy
Models that refer to any Git repository on disk are supported by Geo with the `Geo::RepositoryReplicatorStrategy` module. For example, see how [Geo replication was implemented for Group-level Wikis](https://gitlab.com/gitlab-org/gitlab/-/issues/208147). Note that this issue does not implement verification, since verification of Git repositories was not yet added to the Geo self-service framework. An example implementing verification can be found in the merge request to [Add Snippet repository verification](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56596). Models that refer to any Git repository on disk are supported by Geo with the `Geo::RepositoryReplicatorStrategy` module. For example, see how [Geo replication was implemented for Group-level Wikis](https://gitlab.com/gitlab-org/gitlab/-/issues/208147). Note that this issue does not implement verification, since verification of Git repositories was not yet added to the Geo self-service framework. An example implementing verification can be found in the merge request to [Add Snippet repository verification](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56596).
...@@ -173,3 +175,5 @@ Models that refer to any Git repository on disk are supported by Geo with the `G ...@@ -173,3 +175,5 @@ Models that refer to any Git repository on disk are supported by Geo with the `G
Each Git repository is expected to have its own primary ID and model. Each Git repository is expected to have its own primary ID and model.
To implement Geo replication of a new Git repository-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%20Replicate%20a%20new%20Git%20repository%20type). To implement Geo replication of a new Git repository-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%20Replicate%20a%20new%20Git%20repository%20type).
To view the implementation steps without opening an issue, [view the issue template file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Geo%20Replicate%20a%20new%20Git%20repository%20type.md).
...@@ -51,15 +51,14 @@ describe('GeoNodesBetaApp', () => { ...@@ -51,15 +51,14 @@ describe('GeoNodesBetaApp', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findGeoNodesBetaContainer = () => wrapper.find('section'); const findGeoNodesBetaContainer = () => wrapper.find('section');
const findGeoLearnMoreLink = () => wrapper.find(GlLink); const findGeoLearnMoreLink = () => wrapper.findComponent(GlLink);
const findGeoAddSiteButton = () => wrapper.find(GlButton); const findGeoAddSiteButton = () => wrapper.findComponent(GlButton);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findGeoEmptyState = () => wrapper.find(GeoNodesEmptyState); const findGeoEmptyState = () => wrapper.findComponent(GeoNodesEmptyState);
const findGeoNodes = () => wrapper.findAll(GeoNodes); const findGeoNodes = () => wrapper.findAllComponents(GeoNodes);
describe('template', () => { describe('template', () => {
describe('always', () => { describe('always', () => {
...@@ -91,19 +90,19 @@ describe('GeoNodesBetaApp', () => { ...@@ -91,19 +90,19 @@ describe('GeoNodesBetaApp', () => {
}); });
describe(`when isLoading is ${isLoading} & nodes length ${nodes.length}`, () => { describe(`when isLoading is ${isLoading} & nodes length ${nodes.length}`, () => {
it(`does ${!showLoadingIcon ? 'not ' : ''}render GlLoadingIcon`, () => { it(`does ${showLoadingIcon ? '' : 'not '}render GlLoadingIcon`, () => {
expect(findGlLoadingIcon().exists()).toBe(showLoadingIcon); expect(findGlLoadingIcon().exists()).toBe(showLoadingIcon);
}); });
it(`does ${!showNodes ? 'not ' : ''}render GeoNodes`, () => { it(`does ${showNodes ? '' : 'not '}render GeoNodes`, () => {
expect(findGeoNodes().exists()).toBe(showNodes); expect(findGeoNodes().exists()).toBe(showNodes);
}); });
it(`does ${!showEmptyState ? 'not ' : ''}render EmptyState`, () => { it(`does ${showEmptyState ? '' : 'not '}render EmptyState`, () => {
expect(findGeoEmptyState().exists()).toBe(showEmptyState); expect(findGeoEmptyState().exists()).toBe(showEmptyState);
}); });
it(`does ${!showAddButton ? 'not ' : ''}render AddSiteButton`, () => { it(`does ${showAddButton ? '' : 'not '}render AddSiteButton`, () => {
expect(findGeoAddSiteButton().exists()).toBe(showAddButton); expect(findGeoAddSiteButton().exists()).toBe(showAddButton);
}); });
}); });
......
...@@ -37,10 +37,9 @@ describe('GeoNodesEmptyState', () => { ...@@ -37,10 +37,9 @@ describe('GeoNodesEmptyState', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findGeoEmptyState = () => wrapper.find(GlEmptyState); const findGeoEmptyState = () => wrapper.findComponent(GlEmptyState);
describe('template', () => { describe('template', () => {
beforeEach(() => { beforeEach(() => {
......
...@@ -22,7 +22,6 @@ describe('GeoNodes', () => { ...@@ -22,7 +22,6 @@ describe('GeoNodes', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findGeoNodesContainer = () => wrapper.find('div'); const findGeoNodesContainer = () => wrapper.find('div');
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
MOCK_PRIMARY_VERSION, MOCK_PRIMARY_VERSION,
MOCK_REPLICABLE_TYPES, MOCK_REPLICABLE_TYPES,
} from 'ee_jest/geo_nodes_beta/mock_data'; } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -28,23 +29,24 @@ describe('GeoNodeActionsDesktop', () => { ...@@ -28,23 +29,24 @@ describe('GeoNodeActionsDesktop', () => {
}, },
}); });
wrapper = shallowMount(GeoNodeActionsDesktop, { wrapper = extendedWrapper(
shallowMount(GeoNodeActionsDesktop, {
localVue, localVue,
store, store,
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
}); }),
);
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
const findGeoDesktopActionsButtons = () => wrapper.findAll(GlButton); const findGeoDesktopActionsButtons = () => wrapper.findAllComponents(GlButton);
const findGeoDesktopActionsRemoveButton = () => const findGeoDesktopActionsRemoveButton = () => wrapper.findByTestId('geo-desktop-remove-action');
wrapper.find('[data-testid="geo-desktop-remove-action"]');
describe('template', () => { describe('template', () => {
describe('always', () => { describe('always', () => {
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
MOCK_PRIMARY_VERSION, MOCK_PRIMARY_VERSION,
MOCK_REPLICABLE_TYPES, MOCK_REPLICABLE_TYPES,
} from 'ee_jest/geo_nodes_beta/mock_data'; } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -28,24 +29,26 @@ describe('GeoNodeActionsMobile', () => { ...@@ -28,24 +29,26 @@ describe('GeoNodeActionsMobile', () => {
}, },
}); });
wrapper = shallowMount(GeoNodeActionsMobile, { wrapper = extendedWrapper(
shallowMount(GeoNodeActionsMobile, {
localVue, localVue,
store, store,
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
}); }),
);
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
const findGeoMobileActionsDropdown = () => wrapper.find(GlDropdown); const findGeoMobileActionsDropdown = () => wrapper.findComponent(GlDropdown);
const findGeoMobileActionsDropdownItems = () => wrapper.findAll(GlDropdownItem); const findGeoMobileActionsDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGeoMobileActionsRemoveDropdownItem = () => const findGeoMobileActionsRemoveDropdownItem = () =>
wrapper.find('[data-testid="geo-mobile-remove-action"]'); wrapper.findByTestId('geo-mobile-remove-action');
describe('template', () => { describe('template', () => {
describe('always', () => { describe('always', () => {
......
...@@ -43,8 +43,8 @@ describe('GeoNodeActions', () => { ...@@ -43,8 +43,8 @@ describe('GeoNodeActions', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findGeoMobileActions = () => wrapper.find(GeoNodeActionsMobile); const findGeoMobileActions = () => wrapper.findComponent(GeoNodeActionsMobile);
const findGeoDesktopActions = () => wrapper.find(GeoNodeActionsDesktop); const findGeoDesktopActions = () => wrapper.findComponent(GeoNodeActionsDesktop);
describe('template', () => { describe('template', () => {
beforeEach(() => { beforeEach(() => {
......
...@@ -46,11 +46,11 @@ describe('GeoNodeHeader', () => { ...@@ -46,11 +46,11 @@ describe('GeoNodeHeader', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findHeaderCollapseButton = () => wrapper.find(GlButton); const findHeaderCollapseButton = () => wrapper.findComponent(GlButton);
const findCurrentNodeBadge = () => wrapper.find(GlBadge); const findCurrentNodeBadge = () => wrapper.findComponent(GlBadge);
const findGeoNodeHealthStatus = () => wrapper.find(GeoNodeHealthStatus); const findGeoNodeHealthStatus = () => wrapper.findComponent(GeoNodeHealthStatus);
const findGeoNodeLastUpdated = () => wrapper.find(GeoNodeLastUpdated); const findGeoNodeLastUpdated = () => wrapper.findComponent(GeoNodeLastUpdated);
const findGeoNodeActions = () => wrapper.find(GeoNodeActions); const findGeoNodeActions = () => wrapper.findComponent(GeoNodeActions);
describe('template', () => { describe('template', () => {
describe('always', () => { describe('always', () => {
......
...@@ -39,8 +39,8 @@ describe('GeoNodeHealthStatus', () => { ...@@ -39,8 +39,8 @@ describe('GeoNodeHealthStatus', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findGeoStatusBadge = () => wrapper.find(GlBadge); const findGeoStatusBadge = () => wrapper.findComponent(GlBadge);
const findGeoStatusIcon = () => wrapper.find(GlIcon); const findGeoStatusIcon = () => wrapper.findComponent(GlIcon);
const findGeoStatusText = () => wrapper.find('span'); const findGeoStatusText = () => wrapper.find('span');
describe.each` describe.each`
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
STATUS_DELAY_THRESHOLD_MS, STATUS_DELAY_THRESHOLD_MS,
} from 'ee/geo_nodes_beta/constants'; } from 'ee/geo_nodes_beta/constants';
import { MOCK_PRIMARY_VERSION, MOCK_REPLICABLE_TYPES } from 'ee_jest/geo_nodes_beta/mock_data'; import { MOCK_PRIMARY_VERSION, MOCK_REPLICABLE_TYPES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -34,25 +35,27 @@ describe('GeoNodeLastUpdated', () => { ...@@ -34,25 +35,27 @@ describe('GeoNodeLastUpdated', () => {
}, },
}); });
wrapper = shallowMount(GeoNodeLastUpdated, { wrapper = extendedWrapper(
shallowMount(GeoNodeLastUpdated, {
localVue, localVue,
store, store,
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
}); }),
);
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
const findMainText = () => wrapper.find('[data-testid="last-updated-main-text"]'); const findMainText = () => wrapper.findByTestId('last-updated-main-text');
const findGlIcon = () => wrapper.find(GlIcon); const findGlIcon = () => wrapper.findComponent(GlIcon);
const findGlPopover = () => wrapper.find(GlPopover); const findGlPopover = () => wrapper.findComponent(GlPopover);
const findPopoverText = () => findGlPopover().find('p'); const findPopoverText = () => findGlPopover().find('p');
const findPopoverLink = () => findGlPopover().find(GlLink); const findPopoverLink = () => findGlPopover().findComponent(GlLink);
describe('template', () => { describe('template', () => {
describe('always', () => { describe('always', () => {
...@@ -75,12 +78,12 @@ describe('GeoNodeLastUpdated', () => { ...@@ -75,12 +78,12 @@ describe('GeoNodeLastUpdated', () => {
}); });
it('renders the popover text correctly', () => { it('renders the popover text correctly', () => {
expect(findPopoverText().exists()).toBeTruthy(); expect(findPopoverText().exists()).toBe(true);
expect(findPopoverText().text()).toBe("Node's status was updated 10 minutes ago."); expect(findPopoverText().text()).toBe("Node's status was updated 10 minutes ago.");
}); });
it('renders the popover link always', () => { it('renders the popover link always', () => {
expect(findPopoverLink().exists()).toBeTruthy(); expect(findPopoverLink().exists()).toBe(true);
}); });
}); });
......
...@@ -6,7 +6,10 @@ module API ...@@ -6,7 +6,10 @@ module API
class JobBasic < Grape::Entity class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
expose :created_at, :started_at, :finished_at expose :created_at, :started_at, :finished_at
expose :duration expose :duration,
documentation: { type: 'Floating', desc: 'Time spent running' }
expose :queued_duration,
documentation: { type: 'Floating', desc: 'Time spent enqueued' }
expose :user, with: ::API::Entities::User expose :user, with: ::API::Entities::User
expose :commit, with: ::API::Entities::Commit expose :commit, with: ::API::Entities::Commit
expose :pipeline, with: ::API::Entities::Ci::PipelineBasic expose :pipeline, with: ::API::Entities::Ci::PipelineBasic
......
...@@ -9,6 +9,7 @@ module API ...@@ -9,6 +9,7 @@ module API
expose :user, with: Entities::UserBasic expose :user, with: Entities::UserBasic
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
expose :duration expose :duration
expose :queued_duration
expose :coverage expose :coverage
expose :detailed_status, using: DetailedStatusEntity do |pipeline, options| expose :detailed_status, using: DetailedStatusEntity do |pipeline, options|
pipeline.detailed_status(options[:current_user]) pipeline.detailed_status(options[:current_user])
......
...@@ -30,6 +30,7 @@ module Gitlab ...@@ -30,6 +30,7 @@ module Gitlab
build_started_at: build.started_at, build_started_at: build.started_at,
build_finished_at: build.finished_at, build_finished_at: build.finished_at,
build_duration: build.duration, build_duration: build.duration,
build_queued_duration: build.queued_duration,
build_allow_failure: build.allow_failure, build_allow_failure: build.allow_failure,
build_failure_reason: build.failure_reason, build_failure_reason: build.failure_reason,
pipeline_id: commit.id, pipeline_id: commit.id,
......
...@@ -31,6 +31,7 @@ module Gitlab ...@@ -31,6 +31,7 @@ module Gitlab
created_at: pipeline.created_at, created_at: pipeline.created_at,
finished_at: pipeline.finished_at, finished_at: pipeline.finished_at,
duration: pipeline.duration, duration: pipeline.duration,
queued_duration: pipeline.queued_duration,
variables: pipeline.variables.map(&:hook_attrs) variables: pipeline.variables.map(&:hook_attrs)
} }
end end
...@@ -59,6 +60,8 @@ module Gitlab ...@@ -59,6 +60,8 @@ module Gitlab
created_at: build.created_at, created_at: build.created_at,
started_at: build.started_at, started_at: build.started_at,
finished_at: build.finished_at, finished_at: build.finished_at,
duration: build.duration,
queued_duration: build.queued_duration,
when: build.when, when: build.when,
manual: build.action?, manual: build.action?,
allow_failure: build.allow_failure, allow_failure: build.allow_failure,
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"started_at", "started_at",
"finished_at", "finished_at",
"duration", "duration",
"queued_duration",
"user", "user",
"commit", "commit",
"pipeline", "pipeline",
...@@ -34,6 +35,7 @@ ...@@ -34,6 +35,7 @@
"started_at": { "type": ["null", "string"] }, "started_at": { "type": ["null", "string"] },
"finished_at": { "type": ["null", "string"] }, "finished_at": { "type": ["null", "string"] },
"duration": { "type": ["null", "number"] }, "duration": { "type": ["null", "number"] },
"queued_duration": { "type": ["null", "number"] },
"user": { "$ref": "user/basic.json" }, "user": { "$ref": "user/basic.json" },
"commit": { "commit": {
"oneOf": [ "oneOf": [
......
...@@ -121,14 +121,16 @@ RSpec.describe 'Releases (JavaScript fixtures)' do ...@@ -121,14 +121,16 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
all_releases_query_path = 'releases/queries/all_releases.query.graphql' all_releases_query_path = 'releases/queries/all_releases.query.graphql'
one_release_query_path = 'releases/queries/one_release.query.graphql' one_release_query_path = 'releases/queries/one_release.query.graphql'
fragment_paths = ['releases/queries/release.fragment.graphql'] one_release_for_editing_query_path = 'releases/queries/one_release_for_editing.query.graphql'
release_fragment_path = 'releases/queries/release.fragment.graphql'
release_for_editing_fragment_path = 'releases/queries/release_for_editing.fragment.graphql'
before(:all) do before(:all) do
clean_frontend_fixtures('graphql/releases/') clean_frontend_fixtures('graphql/releases/')
end end
it "graphql/#{all_releases_query_path}.json" do it "graphql/#{all_releases_query_path}.json" do
query = get_graphql_query_as_string(all_releases_query_path, fragment_paths) query = get_graphql_query_as_string(all_releases_query_path, [release_fragment_path])
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path }) post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })
...@@ -136,7 +138,15 @@ RSpec.describe 'Releases (JavaScript fixtures)' do ...@@ -136,7 +138,15 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
end end
it "graphql/#{one_release_query_path}.json" do it "graphql/#{one_release_query_path}.json" do
query = get_graphql_query_as_string(one_release_query_path, fragment_paths) query = get_graphql_query_as_string(one_release_query_path, [release_fragment_path])
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
expect_graphql_errors_to_be_empty
end
it "graphql/#{one_release_for_editing_query_path}.json" do
query = get_graphql_query_as_string(one_release_for_editing_query_path, [release_for_editing_fragment_path])
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag }) post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
......
...@@ -129,6 +129,68 @@ Object { ...@@ -129,6 +129,68 @@ Object {
} }
`; `;
exports[`releases/util.js convertOneReleaseForEditingGraphQLResponse matches snapshot 1`] = `
Object {
"data": Object {
"_links": Object {
"self": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
"selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
},
"assets": Object {
"count": undefined,
"links": Array [
Object {
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
"name": "Image",
"url": "https://example.com/image",
},
Object {
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
"name": "Package",
"url": "https://example.com/package",
},
Object {
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
"name": "Runbook",
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
"name": "linux-amd64 binaries",
"url": "https://downloads.example.com/bin/gitlab-linux-amd64",
},
],
"sources": Array [],
},
"author": undefined,
"description": "Best. Release. **Ever.** :rocket:",
"evidences": Array [],
"milestones": Array [
Object {
"issueStats": Object {},
"stats": undefined,
"title": "12.3",
"webPath": undefined,
"webUrl": undefined,
},
Object {
"issueStats": Object {},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": undefined,
},
],
"name": "The first release",
"tagName": "v1.1",
},
}
`;
exports[`releases/util.js convertOneReleaseGraphQLResponse matches snapshot 1`] = ` exports[`releases/util.js convertOneReleaseGraphQLResponse matches snapshot 1`] = `
Object { Object {
"data": Object { "data": Object {
......
...@@ -14,6 +14,9 @@ const originalAllReleasesQueryResponse = getJSONFixture( ...@@ -14,6 +14,9 @@ const originalAllReleasesQueryResponse = getJSONFixture(
const originalOneReleaseQueryResponse = getJSONFixture( const originalOneReleaseQueryResponse = getJSONFixture(
'graphql/releases/queries/one_release.query.graphql.json', 'graphql/releases/queries/one_release.query.graphql.json',
); );
const originalOneReleaseForEditingQueryResponse = getJSONFixture(
'graphql/releases/queries/one_release_for_editing.query.graphql.json',
);
describe('releases/util.js', () => { describe('releases/util.js', () => {
describe('releaseToApiJson', () => { describe('releaseToApiJson', () => {
...@@ -135,6 +138,26 @@ describe('releases/util.js', () => { ...@@ -135,6 +138,26 @@ describe('releases/util.js', () => {
expect(convertedRelease.assets.links[0].linkType).toBeUndefined(); expect(convertedRelease.assets.links[0].linkType).toBeUndefined();
}); });
it('handles assets that have no links', () => {
expect(convertedRelease.assets.links[0]).not.toBeUndefined();
delete releaseFromResponse.assets.links;
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.assets.links).toEqual([]);
});
it('handles assets that have no sources', () => {
expect(convertedRelease.assets.sources[0]).not.toBeUndefined();
delete releaseFromResponse.assets.sources;
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.assets.sources).toEqual([]);
});
}); });
describe('_links', () => { describe('_links', () => {
...@@ -160,6 +183,33 @@ describe('releases/util.js', () => { ...@@ -160,6 +183,33 @@ describe('releases/util.js', () => {
expect(convertedRelease.commit).toBeUndefined(); expect(convertedRelease.commit).toBeUndefined();
}); });
}); });
describe('milestones', () => {
it("handles releases that don't have any milestone stats", () => {
expect(convertedRelease.milestones[0].issueStats).not.toBeUndefined();
releaseFromResponse.milestones.nodes = releaseFromResponse.milestones.nodes.map((n) => ({
...n,
stats: undefined,
}));
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.milestones[0].issueStats).toEqual({});
});
});
describe('evidences', () => {
it("handles releases that don't have any evidences", () => {
expect(convertedRelease.evidences).not.toBeUndefined();
delete releaseFromResponse.evidences;
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.evidences).toEqual([]);
});
});
}); });
describe('convertAllReleasesGraphQLResponse', () => { describe('convertAllReleasesGraphQLResponse', () => {
...@@ -173,4 +223,12 @@ describe('releases/util.js', () => { ...@@ -173,4 +223,12 @@ describe('releases/util.js', () => {
expect(convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse)).toMatchSnapshot(); expect(convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse)).toMatchSnapshot();
}); });
}); });
describe('convertOneReleaseForEditingGraphQLResponse', () => {
it('matches snapshot', () => {
expect(
convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse),
).toMatchSnapshot();
});
});
}); });
...@@ -26,6 +26,7 @@ RSpec.describe Types::Ci::JobType do ...@@ -26,6 +26,7 @@ RSpec.describe Types::Ci::JobType do
pipeline pipeline
playable playable
queued_at queued_at
queued_duration
refName refName
refPath refPath
retryable retryable
......
...@@ -9,7 +9,8 @@ RSpec.describe Types::Ci::PipelineType do ...@@ -9,7 +9,8 @@ RSpec.describe Types::Ci::PipelineType do
it 'contains attributes related to a pipeline' do it 'contains attributes related to a pipeline' do
expected_fields = %w[ expected_fields = %w[
id iid sha before_sha status detailed_status config_source duration id iid sha before_sha status detailed_status config_source
duration queued_duration
coverage created_at updated_at started_at finished_at committed_at coverage created_at updated_at started_at finished_at committed_at
stages user retryable cancelable jobs source_job job downstream stages user retryable cancelable jobs source_job job downstream
upstream path project active user_permissions warnings commit_path uses_needs upstream path project active user_permissions warnings commit_path uses_needs
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Duration'] do
let(:duration) { 17.minutes }
it 'presents information as a floating point number' do
expect(described_class.coerce_isolated_result(duration)).to eq(duration.to_f)
end
it 'accepts integers as input' do
expect(described_class.coerce_isolated_input(100)).to eq(100.0)
end
it 'accepts floats as input' do
expect(described_class.coerce_isolated_input(0.5)).to eq(0.5)
end
it 'rejects invalid input' do
expect { described_class.coerce_isolated_input('not valid') }
.to raise_error(GraphQL::CoercionError)
end
it 'rejects nil' do
expect { described_class.coerce_isolated_input(nil) }
.to raise_error(GraphQL::CoercionError)
end
end
...@@ -9,6 +9,10 @@ RSpec.describe Gitlab::DataBuilder::Build do ...@@ -9,6 +9,10 @@ RSpec.describe Gitlab::DataBuilder::Build do
let(:build) { create(:ci_build, :running, runner: runner, user: user) } let(:build) { create(:ci_build, :running, runner: runner, user: user) }
describe '.build' do describe '.build' do
around do |example|
travel_to(Time.current) { example.run }
end
let(:data) do let(:data) do
described_class.build(build) described_class.build(build)
end end
...@@ -22,6 +26,8 @@ RSpec.describe Gitlab::DataBuilder::Build do ...@@ -22,6 +26,8 @@ RSpec.describe Gitlab::DataBuilder::Build do
it { expect(data[:build_created_at]).to eq(build.created_at) } it { expect(data[:build_created_at]).to eq(build.created_at) }
it { expect(data[:build_started_at]).to eq(build.started_at) } it { expect(data[:build_started_at]).to eq(build.started_at) }
it { expect(data[:build_finished_at]).to eq(build.finished_at) } it { expect(data[:build_finished_at]).to eq(build.finished_at) }
it { expect(data[:build_duration]).to eq(build.duration) }
it { expect(data[:build_queued_duration]).to eq(build.queued_duration) }
it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:build_failure_reason]).to eq(build.failure_reason) } it { expect(data[:build_failure_reason]).to eq(build.failure_reason) }
it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_id]).to eq(build.project.id) }
......
...@@ -4679,25 +4679,30 @@ RSpec.describe Ci::Build do ...@@ -4679,25 +4679,30 @@ RSpec.describe Ci::Build do
end end
describe '#execute_hooks' do describe '#execute_hooks' do
before do
build.clear_memoization(:build_data)
end
context 'with project hooks' do context 'with project hooks' do
let(:build_data) { double(:BuildData, dup: double(:DupedData)) }
before do before do
create(:project_hook, project: project, job_events: true) create(:project_hook, project: project, job_events: true)
end end
it 'execute hooks' do it 'calls project.execute_hooks(build_data, :job_hooks)' do
expect_any_instance_of(ProjectHook).to receive(:async_execute) expect(::Gitlab::DataBuilder::Build)
.to receive(:build).with(build).and_return(build_data)
expect(build.project)
.to receive(:execute_hooks).with(build_data.dup, :job_hooks)
build.execute_hooks build.execute_hooks
end end
end end
context 'without relevant project hooks' do context 'without project hooks' do
before do it 'does not call project.execute_hooks' do
create(:project_hook, project: project, job_events: false) expect(build.project).not_to receive(:execute_hooks)
end
it 'does not execute a hook' do
expect_any_instance_of(ProjectHook).not_to receive(:async_execute)
build.execute_hooks build.execute_hooks
end end
...@@ -4708,8 +4713,10 @@ RSpec.describe Ci::Build do ...@@ -4708,8 +4713,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: true, project: project) create(:service, active: true, job_events: true, project: project)
end end
it 'execute services' do it 'executes services' do
expect_any_instance_of(Service).to receive(:async_execute) allow_next_found_instance_of(Service) do |service|
expect(service).to receive(:async_execute)
end
build.execute_hooks build.execute_hooks
end end
...@@ -4720,8 +4727,10 @@ RSpec.describe Ci::Build do ...@@ -4720,8 +4727,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: false, project: project) create(:service, active: true, job_events: false, project: project)
end end
it 'execute services' do it 'does not execute services' do
expect_any_instance_of(Service).not_to receive(:async_execute) allow_next_found_instance_of(Service) do |service|
expect(service).not_to receive(:async_execute)
end
build.execute_hooks build.execute_hooks
end end
......
...@@ -259,6 +259,40 @@ RSpec.describe CommitStatus do ...@@ -259,6 +259,40 @@ RSpec.describe CommitStatus do
end end
end end
describe '#queued_duration' do
subject { commit_status.queued_duration }
around do |example|
travel_to(Time.current) { example.run }
end
context 'when created, then enqueued, then started' do
before do
commit_status.queued_at = 30.seconds.ago
commit_status.started_at = 25.seconds.ago
end
it { is_expected.to eq(5.0) }
end
context 'when created but not yet enqueued' do
before do
commit_status.queued_at = nil
end
it { is_expected.to be_nil }
end
context 'when enqueued, but not started' do
before do
commit_status.queued_at = Time.current - 1.minute
commit_status.started_at = nil
end
it { is_expected.to eq(1.minute) }
end
end
describe '.latest' do describe '.latest' do
subject { described_class.latest.order(:id) } subject { described_class.latest.order(:id) }
......
...@@ -362,6 +362,25 @@ RSpec.describe API::Ci::Pipelines do ...@@ -362,6 +362,25 @@ RSpec.describe API::Ci::Pipelines do
it do it do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response).to all match a_hash_including(
'duration' => be_nil,
'queued_duration' => (be >= 0.0)
)
end
end
context 'when filtering to only running jobs' do
let(:query) { { 'scope' => 'running' } }
it do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response).to all match a_hash_including(
'duration' => (be >= 0.0),
'queued_duration' => (be >= 0.0)
)
end end
end end
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
include GraphqlHelpers include GraphqlHelpers
around do |example|
travel_to(Time.current) { example.run }
end
let_it_be(:user) { create_default(:user) } let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
...@@ -35,13 +39,20 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do ...@@ -35,13 +39,20 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
let(:terminal_type) { 'CiJob' } let(:terminal_type) { 'CiJob' }
it 'retrieves scalar fields' do it 'retrieves scalar fields' do
job_2.update!(
created_at: 40.seconds.ago,
queued_at: 32.seconds.ago,
started_at: 30.seconds.ago,
finished_at: 5.seconds.ago
)
post_graphql(query, current_user: user) post_graphql(query, current_user: user)
expect(graphql_data_at(*path)).to match a_hash_including( expect(graphql_data_at(*path)).to match a_hash_including(
'id' => global_id_of(job_2), 'id' => global_id_of(job_2),
'name' => job_2.name, 'name' => job_2.name,
'allowFailure' => job_2.allow_failure, 'allowFailure' => job_2.allow_failure,
'duration' => job_2.duration, 'duration' => 25,
'queuedDuration' => 2.0,
'status' => job_2.status.upcase 'status' => job_2.status.upcase
) )
end end
......
...@@ -8,6 +8,49 @@ RSpec.describe 'Query.project(fullPath).pipelines' do ...@@ -8,6 +8,49 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
around do |example|
travel_to(Time.current) { example.run }
end
describe 'duration fields' do
let_it_be(:pipeline) do
create(:ci_pipeline, project: project)
end
let(:query_path) do
[
[:project, { full_path: project.full_path }],
[:pipelines],
[:nodes]
]
end
let(:query) do
wrap_fields(query_graphql_path(query_path, 'queuedDuration duration'))
end
before do
pipeline.update!(
created_at: 1.minute.ago,
started_at: 55.seconds.ago
)
create(:ci_build, :success,
pipeline: pipeline,
started_at: 55.seconds.ago,
finished_at: 10.seconds.ago)
pipeline.update_duration
pipeline.save!
post_graphql(query, current_user: user)
end
it 'includes the duration fields' do
path = query_path.map(&:first)
expect(graphql_data_at(*path, :queued_duration)).to eq [5.0]
expect(graphql_data_at(*path, :duration)).to eq [45]
end
end
describe '.jobs' do describe '.jobs' do
let(:first_n) { var('Int') } let(:first_n) { var('Int') }
let(:query_path) do let(:query_path) do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment