Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a95a884707
commit
d67a86595f
30 changed files with 757 additions and 267 deletions
|
@ -1 +1 @@
|
|||
de019fc19eeb8bc6a65a6dbd8bf236669c777815
|
||||
2ed9a2c78ec556eb8d64e03203c864355ea5a128
|
||||
|
|
|
@ -59,7 +59,7 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
private
|
||||
|
||||
def runner
|
||||
@runner ||= Ci::RunnersFinder.new(current_user: current_user, group: @group, params: {}).execute
|
||||
@runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute
|
||||
.except(:limit, :offset)
|
||||
.find(params[:id])
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ module Groups
|
|||
NUMBER_OF_RUNNERS_PER_PAGE = 4
|
||||
|
||||
def show
|
||||
runners_finder = Ci::RunnersFinder.new(current_user: current_user, group: @group, params: params)
|
||||
runners_finder = Ci::RunnersFinder.new(current_user: current_user, params: params.merge({ group: @group }))
|
||||
# We need all runners for count
|
||||
@all_group_runners = runners_finder.execute.except(:limit, :offset)
|
||||
@group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
|
||||
|
|
|
@ -7,9 +7,9 @@ module Ci
|
|||
ALLOWED_SORTS = %w[contacted_asc contacted_desc created_at_asc created_at_desc created_date].freeze
|
||||
DEFAULT_SORT = 'created_at_desc'
|
||||
|
||||
def initialize(current_user:, group: nil, params:)
|
||||
def initialize(current_user:, params:)
|
||||
@params = params
|
||||
@group = group
|
||||
@group = params.delete(:group)
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
|
@ -48,10 +48,16 @@ module Ci
|
|||
def group_runners
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_group, @group)
|
||||
|
||||
# Getting all runners from the group itself and all its descendants
|
||||
@runners = case @params[:membership]
|
||||
when :direct
|
||||
Ci::Runner.belonging_to_group(@group.id)
|
||||
when :descendants, nil
|
||||
# Getting all runners from the group itself and all its descendant groups/projects
|
||||
descendant_projects = Project.for_group_and_its_subgroups(@group)
|
||||
|
||||
@runners = Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
|
||||
Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
|
||||
else
|
||||
raise ArgumentError, 'Invalid membership filter'
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_status!
|
||||
|
|
26
app/graphql/resolvers/ci/group_runners_resolver.rb
Normal file
26
app/graphql/resolvers/ci/group_runners_resolver.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module Ci
|
||||
class GroupRunnersResolver < RunnersResolver
|
||||
type Types::Ci::RunnerType.connection_type, null: true
|
||||
|
||||
argument :membership, ::Types::Ci::RunnerMembershipFilterEnum,
|
||||
required: false,
|
||||
default_value: :descendants,
|
||||
description: 'Control which runners to include in the results.'
|
||||
|
||||
protected
|
||||
|
||||
def runners_finder_params(params)
|
||||
super(params).merge(membership: params[:membership])
|
||||
end
|
||||
|
||||
def parent_param
|
||||
raise 'Expected group missing' unless parent.is_a?(Group)
|
||||
|
||||
{ group: parent }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,7 +34,7 @@ module Resolvers
|
|||
.execute)
|
||||
end
|
||||
|
||||
private
|
||||
protected
|
||||
|
||||
def runners_finder_params(params)
|
||||
{
|
||||
|
@ -47,6 +47,19 @@ module Resolvers
|
|||
tag_name: node_selection&.selects?(:tag_list)
|
||||
}
|
||||
}.compact
|
||||
.merge(parent_param)
|
||||
end
|
||||
|
||||
def parent_param
|
||||
return {} unless parent
|
||||
|
||||
raise "Unexpected parent type: #{parent.class}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parent
|
||||
object.respond_to?(:sync) ? object.sync : object
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
18
app/graphql/types/ci/runner_membership_filter_enum.rb
Normal file
18
app/graphql/types/ci/runner_membership_filter_enum.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
class RunnerMembershipFilterEnum < BaseEnum
|
||||
graphql_name 'RunnerMembershipFilter'
|
||||
description 'Values for filtering runners in namespaces.'
|
||||
|
||||
value 'DIRECT',
|
||||
description: "Include runners that have a direct relationship.",
|
||||
value: :direct
|
||||
|
||||
value 'DESCENDANTS',
|
||||
description: "Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried).",
|
||||
value: :descendants
|
||||
end
|
||||
end
|
||||
end
|
|
@ -155,6 +155,12 @@ module Types
|
|||
complexity: 5,
|
||||
resolver: Resolvers::GroupsResolver
|
||||
|
||||
field :runners, Types::Ci::RunnerType.connection_type,
|
||||
null: true,
|
||||
resolver: Resolvers::Ci::GroupRunnersResolver,
|
||||
description: "Find runners visible to the current user.",
|
||||
feature_flag: :runner_graphql_query
|
||||
|
||||
def avatar_url
|
||||
object.avatar_url(only_path: false)
|
||||
end
|
||||
|
|
|
@ -16,7 +16,6 @@ module Ci
|
|||
"ci-config-path": project.ci_config_path_or_default,
|
||||
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
|
||||
"ci-help-page-path" => help_page_path('ci/index'),
|
||||
"commit-sha" => commit_sha,
|
||||
"default-branch" => project.default_branch_or_main,
|
||||
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
|
||||
"initial-branch-name" => initial_branch,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: dast_meta_tag_validation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67945
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337711
|
||||
milestone: '14.2'
|
||||
name: create_vulnerabilities_via_api
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68158
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338694
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::dynamic analysis
|
||||
default_enabled: true
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: dast_runner_site_validation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61649
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331082
|
||||
milestone: '14.0'
|
||||
type: development
|
||||
group: group::dynamic analysis
|
||||
default_enabled: true
|
115
db/post_migrate/20210731132939_backfill_stage_event_hash.rb
Normal file
115
db/post_migrate/20210731132939_backfill_stage_event_hash.rb
Normal file
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillStageEventHash < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
BATCH_SIZE = 100
|
||||
EVENT_ID_IDENTIFIER_MAPPING = {
|
||||
1 => :issue_created,
|
||||
2 => :issue_first_mentioned_in_commit,
|
||||
3 => :issue_closed,
|
||||
4 => :issue_first_added_to_board,
|
||||
5 => :issue_first_associated_with_milestone,
|
||||
7 => :issue_last_edited,
|
||||
8 => :issue_label_added,
|
||||
9 => :issue_label_removed,
|
||||
10 => :issue_deployed_to_production,
|
||||
100 => :merge_request_created,
|
||||
101 => :merge_request_first_deployed_to_production,
|
||||
102 => :merge_request_last_build_finished,
|
||||
103 => :merge_request_last_build_started,
|
||||
104 => :merge_request_merged,
|
||||
105 => :merge_request_closed,
|
||||
106 => :merge_request_last_edited,
|
||||
107 => :merge_request_label_added,
|
||||
108 => :merge_request_label_removed,
|
||||
109 => :merge_request_first_commit_at,
|
||||
1000 => :code_stage_start,
|
||||
1001 => :issue_stage_end,
|
||||
1002 => :plan_stage_start
|
||||
}.freeze
|
||||
|
||||
LABEL_BASED_EVENTS = Set.new([8, 9, 107, 108]).freeze
|
||||
|
||||
class GroupStage < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'analytics_cycle_analytics_group_stages'
|
||||
end
|
||||
|
||||
class ProjectStage < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'analytics_cycle_analytics_project_stages'
|
||||
end
|
||||
|
||||
class StageEventHash < ActiveRecord::Base
|
||||
self.table_name = 'analytics_cycle_analytics_stage_event_hashes'
|
||||
end
|
||||
|
||||
def up
|
||||
GroupStage.reset_column_information
|
||||
ProjectStage.reset_column_information
|
||||
StageEventHash.reset_column_information
|
||||
|
||||
update_stage_table(GroupStage)
|
||||
update_stage_table(ProjectStage)
|
||||
|
||||
add_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
|
||||
add_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
|
||||
remove_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_stage_table(klass)
|
||||
klass.each_batch(of: BATCH_SIZE) do |relation|
|
||||
klass.transaction do
|
||||
records = relation.where(stage_event_hash_id: nil).lock!.to_a # prevent concurrent modification (unlikely to happen)
|
||||
records = delete_invalid_records(records)
|
||||
next if records.empty?
|
||||
|
||||
hashes_by_stage = records.to_h { |stage| [stage, calculate_stage_events_hash(stage)] }
|
||||
hashes = hashes_by_stage.values.uniq
|
||||
|
||||
StageEventHash.insert_all(hashes.map { |hash| { hash_sha256: hash } })
|
||||
|
||||
stage_event_hashes_by_hash = StageEventHash.where(hash_sha256: hashes).index_by(&:hash_sha256)
|
||||
records.each do |stage|
|
||||
stage.update!(stage_event_hash_id: stage_event_hashes_by_hash[hashes_by_stage[stage]].id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_stage_events_hash(stage)
|
||||
start_event_hash = calculate_event_hash(stage.start_event_identifier, stage.start_event_label_id)
|
||||
end_event_hash = calculate_event_hash(stage.end_event_identifier, stage.end_event_label_id)
|
||||
|
||||
Digest::SHA256.hexdigest("#{start_event_hash}-#{end_event_hash}")
|
||||
end
|
||||
|
||||
def calculate_event_hash(event_identifier, label_id = nil)
|
||||
str = EVENT_ID_IDENTIFIER_MAPPING.fetch(event_identifier).to_s
|
||||
str << "-#{label_id}" if LABEL_BASED_EVENTS.include?(event_identifier)
|
||||
|
||||
Digest::SHA256.hexdigest(str)
|
||||
end
|
||||
|
||||
# Invalid records are safe to delete, since they are not working properly anyway
|
||||
def delete_invalid_records(records)
|
||||
to_be_deleted = records.select do |record|
|
||||
EVENT_ID_IDENTIFIER_MAPPING[record.start_event_identifier].nil? ||
|
||||
EVENT_ID_IDENTIFIER_MAPPING[record.end_event_identifier].nil?
|
||||
end
|
||||
|
||||
to_be_deleted.each(&:delete)
|
||||
records - to_be_deleted
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210731132939
Normal file
1
db/schema_migrations/20210731132939
Normal file
|
@ -0,0 +1 @@
|
|||
97d968bba0eb2bf6faa19de8a3e4fe93dc03a623b623dc802ab0fe0a4afb0370
|
|
@ -9102,7 +9102,8 @@ CREATE TABLE analytics_cycle_analytics_group_stages (
|
|||
custom boolean DEFAULT true NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
group_value_stream_id bigint NOT NULL,
|
||||
stage_event_hash_id bigint
|
||||
stage_event_hash_id bigint,
|
||||
CONSTRAINT check_e6bd4271b5 CHECK ((stage_event_hash_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE analytics_cycle_analytics_group_stages_id_seq
|
||||
|
@ -9146,7 +9147,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages (
|
|||
custom boolean DEFAULT true NOT NULL,
|
||||
name character varying(255) NOT NULL,
|
||||
project_value_stream_id bigint NOT NULL,
|
||||
stage_event_hash_id bigint
|
||||
stage_event_hash_id bigint,
|
||||
CONSTRAINT check_8f6019de1e CHECK ((stage_event_hash_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
|
||||
|
|
|
@ -4459,6 +4459,39 @@ Input type: `VulnerabilityConfirmInput`
|
|||
| <a id="mutationvulnerabilityconfirmerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationvulnerabilityconfirmvulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | The vulnerability after state change. |
|
||||
|
||||
### `Mutation.vulnerabilityCreate`
|
||||
|
||||
Input type: `VulnerabilityCreateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationvulnerabilitycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationvulnerabilitycreateconfidence"></a>`confidence` | [`VulnerabilityConfidence`](#vulnerabilityconfidence) | Confidence of the vulnerability (defaults to `unknown`). |
|
||||
| <a id="mutationvulnerabilitycreateconfirmedat"></a>`confirmedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to confirmed (defaults to creation time if status is `confirmed`). |
|
||||
| <a id="mutationvulnerabilitycreatedescription"></a>`description` | [`String!`](#string) | Description of the vulnerability. |
|
||||
| <a id="mutationvulnerabilitycreatedetectedat"></a>`detectedAt` | [`Time`](#time) | Timestamp of when the vulnerability was first detected (defaults to creation time). |
|
||||
| <a id="mutationvulnerabilitycreatedismissedat"></a>`dismissedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to dismissed (defaults to creation time if status is `dismissed`). |
|
||||
| <a id="mutationvulnerabilitycreateidentifiers"></a>`identifiers` | [`[VulnerabilityIdentifierInput!]!`](#vulnerabilityidentifierinput) | Array of CVE or CWE identifiers for the vulnerability. |
|
||||
| <a id="mutationvulnerabilitycreatemessage"></a>`message` | [`String`](#string) | Additional information about the vulnerability. |
|
||||
| <a id="mutationvulnerabilitycreateproject"></a>`project` | [`ProjectID!`](#projectid) | ID of the project to attach the vulnerability to. |
|
||||
| <a id="mutationvulnerabilitycreateresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state changed to resolved (defaults to creation time if status is `resolved`). |
|
||||
| <a id="mutationvulnerabilitycreatescannername"></a>`scannerName` | [`String!`](#string) | Name of the security scanner used to discover the vulnerability. |
|
||||
| <a id="mutationvulnerabilitycreatescannertype"></a>`scannerType` | [`SecurityScannerType!`](#securityscannertype) | Type of the security scanner used to discover the vulnerability. |
|
||||
| <a id="mutationvulnerabilitycreateseverity"></a>`severity` | [`VulnerabilitySeverity`](#vulnerabilityseverity) | Severity of the vulnerability (defaults to `unknown`). |
|
||||
| <a id="mutationvulnerabilitycreatesolution"></a>`solution` | [`String`](#string) | How to fix this vulnerability. |
|
||||
| <a id="mutationvulnerabilitycreatestate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | State of the vulnerability (defaults to `detected`). |
|
||||
| <a id="mutationvulnerabilitycreatetitle"></a>`title` | [`String!`](#string) | Title of the vulnerability. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationvulnerabilitycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationvulnerabilitycreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationvulnerabilitycreatevulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | Vulnerability created. |
|
||||
|
||||
### `Mutation.vulnerabilityDismiss`
|
||||
|
||||
Input type: `VulnerabilityDismissInput`
|
||||
|
@ -10004,6 +10037,27 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="groupprojectssearch"></a>`search` | [`String`](#string) | Search project with most similar names or paths. |
|
||||
| <a id="groupprojectssort"></a>`sort` | [`NamespaceProjectSort`](#namespaceprojectsort) | Sort projects by this criteria. |
|
||||
|
||||
##### `Group.runners`
|
||||
|
||||
Find runners visible to the current user. Available only when feature flag `runner_graphql_query` is enabled. This flag is enabled by default.
|
||||
|
||||
Returns [`CiRunnerConnection`](#cirunnerconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#connection-pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="grouprunnersmembership"></a>`membership` | [`RunnerMembershipFilter`](#runnermembershipfilter) | Control which runners to include in the results. |
|
||||
| <a id="grouprunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
|
||||
| <a id="grouprunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
|
||||
| <a id="grouprunnersstatus"></a>`status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. |
|
||||
| <a id="grouprunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
|
||||
| <a id="grouprunnerstype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. |
|
||||
|
||||
##### `Group.timelogs`
|
||||
|
||||
Time logged on issues and merge requests in the group and its subgroups.
|
||||
|
@ -13261,6 +13315,7 @@ Represents summary of a security report.
|
|||
| <a id="securityreportsummarycoveragefuzzing"></a>`coverageFuzzing` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `coverage_fuzzing` scan. |
|
||||
| <a id="securityreportsummarydast"></a>`dast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dast` scan. |
|
||||
| <a id="securityreportsummarydependencyscanning"></a>`dependencyScanning` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `dependency_scanning` scan. |
|
||||
| <a id="securityreportsummarygeneric"></a>`generic` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `generic` scan. |
|
||||
| <a id="securityreportsummarysast"></a>`sast` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `sast` scan. |
|
||||
| <a id="securityreportsummarysecretdetection"></a>`secretDetection` | [`SecurityReportSummarySection`](#securityreportsummarysection) | Aggregated counts for the `secret_detection` scan. |
|
||||
|
||||
|
@ -14141,7 +14196,7 @@ Represents a vulnerability.
|
|||
| <a id="vulnerabilitynotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
|
||||
| <a id="vulnerabilityprimaryidentifier"></a>`primaryIdentifier` | [`VulnerabilityIdentifier`](#vulnerabilityidentifier) | Primary identifier of the vulnerability. |
|
||||
| <a id="vulnerabilityproject"></a>`project` | [`Project`](#project) | The project on which the vulnerability was found. |
|
||||
| <a id="vulnerabilityreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING, API_FUZZING, CLUSTER_IMAGE_SCANNING). `Scan Type` in the UI. |
|
||||
| <a id="vulnerabilityreporttype"></a>`reportType` | [`VulnerabilityReportType`](#vulnerabilityreporttype) | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING, API_FUZZING, CLUSTER_IMAGE_SCANNING, GENERIC). `Scan Type` in the UI. |
|
||||
| <a id="vulnerabilityresolvedat"></a>`resolvedAt` | [`Time`](#time) | Timestamp of when the vulnerability state was changed to resolved. |
|
||||
| <a id="vulnerabilityresolvedby"></a>`resolvedBy` | [`UserCore`](#usercore) | The user that resolved the vulnerability. |
|
||||
| <a id="vulnerabilityresolvedondefaultbranch"></a>`resolvedOnDefaultBranch` | [`Boolean!`](#boolean) | Indicates whether the vulnerability is fixed on the default branch or not. |
|
||||
|
@ -14435,6 +14490,16 @@ Represents the location of a vulnerability found by a dependency security scan.
|
|||
| <a id="vulnerabilitylocationdependencyscanningdependency"></a>`dependency` | [`VulnerableDependency`](#vulnerabledependency) | Dependency containing the vulnerability. |
|
||||
| <a id="vulnerabilitylocationdependencyscanningfile"></a>`file` | [`String`](#string) | Path to the vulnerable file. |
|
||||
|
||||
### `VulnerabilityLocationGeneric`
|
||||
|
||||
Represents the location of a vulnerability found by a generic scanner.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="vulnerabilitylocationgenericdescription"></a>`description` | [`String`](#string) | Free-form description of where the vulnerability is located. |
|
||||
|
||||
### `VulnerabilityLocationSast`
|
||||
|
||||
Represents the location of a vulnerability found by a SAST scan.
|
||||
|
@ -15626,6 +15691,15 @@ Status of a requirement based on last test report.
|
|||
| <a id="requirementstatusfiltermissing"></a>`MISSING` | Requirements without any test report. |
|
||||
| <a id="requirementstatusfilterpassed"></a>`PASSED` | |
|
||||
|
||||
### `RunnerMembershipFilter`
|
||||
|
||||
Values for filtering runners in namespaces.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="runnermembershipfilterdescendants"></a>`DESCENDANTS` | Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried). |
|
||||
| <a id="runnermembershipfilterdirect"></a>`DIRECT` | Include runners that have a direct relationship. |
|
||||
|
||||
### `SastUiComponentSize`
|
||||
|
||||
Size of UI component in SAST configuration page.
|
||||
|
@ -15872,6 +15946,20 @@ Possible states of a user.
|
|||
| <a id="visibilityscopesenumprivate"></a>`private` | The snippet is visible only to the snippet creator. |
|
||||
| <a id="visibilityscopesenumpublic"></a>`public` | The snippet can be accessed without any authentication. |
|
||||
|
||||
### `VulnerabilityConfidence`
|
||||
|
||||
Confidence that a given vulnerability is present in the codebase.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="vulnerabilityconfidenceconfirmed"></a>`CONFIRMED` | |
|
||||
| <a id="vulnerabilityconfidenceexperimental"></a>`EXPERIMENTAL` | |
|
||||
| <a id="vulnerabilityconfidencehigh"></a>`HIGH` | |
|
||||
| <a id="vulnerabilityconfidenceignore"></a>`IGNORE` | |
|
||||
| <a id="vulnerabilityconfidencelow"></a>`LOW` | |
|
||||
| <a id="vulnerabilityconfidencemedium"></a>`MEDIUM` | |
|
||||
| <a id="vulnerabilityconfidenceunknown"></a>`UNKNOWN` | |
|
||||
|
||||
### `VulnerabilityDismissalReason`
|
||||
|
||||
The dismissal reason of the Vulnerability.
|
||||
|
@ -15933,6 +16021,7 @@ The type of the security scan that found the vulnerability.
|
|||
| <a id="vulnerabilityreporttypecoverage_fuzzing"></a>`COVERAGE_FUZZING` | |
|
||||
| <a id="vulnerabilityreporttypedast"></a>`DAST` | |
|
||||
| <a id="vulnerabilityreporttypedependency_scanning"></a>`DEPENDENCY_SCANNING` | |
|
||||
| <a id="vulnerabilityreporttypegeneric"></a>`GENERIC` | |
|
||||
| <a id="vulnerabilityreporttypesast"></a>`SAST` | |
|
||||
| <a id="vulnerabilityreporttypesecret_detection"></a>`SECRET_DETECTION` | |
|
||||
|
||||
|
@ -16573,6 +16662,7 @@ One of:
|
|||
- [`VulnerabilityLocationCoverageFuzzing`](#vulnerabilitylocationcoveragefuzzing)
|
||||
- [`VulnerabilityLocationDast`](#vulnerabilitylocationdast)
|
||||
- [`VulnerabilityLocationDependencyScanning`](#vulnerabilitylocationdependencyscanning)
|
||||
- [`VulnerabilityLocationGeneric`](#vulnerabilitylocationgeneric)
|
||||
- [`VulnerabilityLocationSast`](#vulnerabilitylocationsast)
|
||||
- [`VulnerabilityLocationSecretDetection`](#vulnerabilitylocationsecretdetection)
|
||||
|
||||
|
@ -17351,3 +17441,14 @@ A time-frame defined as a closed inclusive range of two dates.
|
|||
| <a id="updatediffimagepositioninputwidth"></a>`width` | [`Int`](#int) | Total width of the image. |
|
||||
| <a id="updatediffimagepositioninputx"></a>`x` | [`Int`](#int) | X position of the note. |
|
||||
| <a id="updatediffimagepositioninputy"></a>`y` | [`Int`](#int) | Y position of the note. |
|
||||
|
||||
### `VulnerabilityIdentifierInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="vulnerabilityidentifierinputexternalid"></a>`externalId` | [`String`](#string) | External ID of the vulnerability identifier. |
|
||||
| <a id="vulnerabilityidentifierinputexternaltype"></a>`externalType` | [`String`](#string) | External type of the vulnerability identifier. |
|
||||
| <a id="vulnerabilityidentifierinputname"></a>`name` | [`String!`](#string) | Name of the vulnerability identifier. |
|
||||
| <a id="vulnerabilityidentifierinputurl"></a>`url` | [`String!`](#string) | URL of the vulnerability identifier. |
|
||||
|
|
|
@ -1049,11 +1049,7 @@ When an API site type is selected, a [host override](#host-override) is used to
|
|||
#### Site profile validation
|
||||
|
||||
> - Site profile validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233020) in GitLab 13.8.
|
||||
> - Meta tag validation [enabled on GitLab.com](https://gitlab.com/groups/gitlab-org/-/epics/6460) in GitLab 14.2 and is ready for production use.
|
||||
> - Meta tag validation [enabled with `dast_meta_tag_validation flag` flag](https://gitlab.com/gitlab-org/gitlab/-/issues/337711) for self-managed GitLab in GitLab 14.2 and is ready for production use.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the `dast_meta_tag_validation` flag](../../../administration/feature_flags.md). On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
|
||||
> - Meta tag validation [introduced](https://gitlab.com/groups/gitlab-org/-/epics/6460) in GitLab 14.2.
|
||||
|
||||
Site profile validation reduces the risk of running an active scan against the wrong website. A site
|
||||
must be validated before an active scan can run against it. The site validation methods are as
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
BIN
doc/user/application_security/img/vulnerability-check_v14_2.png
Normal file
BIN
doc/user/application_security/img/vulnerability-check_v14_2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -194,14 +194,19 @@ merge request would introduce one of the following security issues:
|
|||
When the Vulnerability-Check merge request rule is enabled, additional merge request approval
|
||||
is required when the latest security report in a merge request:
|
||||
|
||||
- Contains a vulnerability of `high`, `critical`, or `unknown` severity that is not present in the
|
||||
- Contains vulnerabilities that are not present in the
|
||||
target branch. Note that approval is still required for dismissed vulnerabilities.
|
||||
- Contains vulnerabilities with severity levels (for example, `high`, `critical`, or `unknown`)
|
||||
matching the rule's severity levels.
|
||||
- Contains a vulnerability count higher than the rule allows.
|
||||
- Is not generated during pipeline execution.
|
||||
|
||||
An approval is optional when the security report:
|
||||
|
||||
- Contains no new vulnerabilities when compared to the target branch.
|
||||
- Contains only new vulnerabilities of `low` or `medium` severity.
|
||||
- Contains only vulnerabilities with severity levels (for example, `low`, `medium`) **NOT** matching
|
||||
the rule's severity levels.
|
||||
- Contains a vulnerability count equal to or less than what the rule allows.
|
||||
|
||||
When the License-Check merge request rule is enabled, additional approval is required if a merge
|
||||
request contains a denied license. For more details, see [Enabling license approvals within a project](../compliance/license_compliance/index.md#enabling-license-approvals-within-a-project).
|
||||
|
@ -219,16 +224,19 @@ Follow these steps to enable `Vulnerability-Check`:
|
|||
1. Go to your project and select **Settings > General**.
|
||||
1. Expand **Merge request approvals**.
|
||||
1. Select **Enable** or **Edit**.
|
||||
1. Add or change the **Rule name** to `Vulnerability-Check` (case sensitive).
|
||||
1. Set the **No. of approvals required** to greater than zero.
|
||||
1. Set the **Security scanners** that the rule applies to.
|
||||
1. Select the **Target branch**.
|
||||
1. Set the **Vulnerabilities allowed** to the number of vulnerabilities allowed before the rule is
|
||||
triggered.
|
||||
1. Set the **Severity levels** to the severity levels that the rule applies to.
|
||||
1. Set the **Approvals required** to the number of approvals that the rule requires.
|
||||
1. Select the users or groups to provide approval.
|
||||
1. Select **Add approval rule**.
|
||||
|
||||
Once this group is added to your project, the approval rule is enabled for all merge requests.
|
||||
Any code changes cause the approvals required to reset.
|
||||
|
||||
![Vulnerability Check Approver Rule](img/vulnerability-check_v13_4.png)
|
||||
![Vulnerability Check Approver Rule](img/vulnerability-check_v14_2.png)
|
||||
|
||||
## Using private Maven repositories
|
||||
|
||||
|
|
|
@ -6,9 +6,53 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Infrastructure management **(FREE)**
|
||||
|
||||
GitLab provides you with great solutions to help you manage your
|
||||
infrastructure:
|
||||
With the rise of DevOps and SRE approaches, infrastructure management becomes codified,
|
||||
automatable, and software development best practices gain their place around infrastructure
|
||||
management too. On one hand, the daily tasks of classical operations people changed
|
||||
and are more similar to traditional software development. On the other hand, software engineers
|
||||
are more likely to control their whole DevOps lifecycle, including deployments and delivery.
|
||||
|
||||
- [Infrastructure as Code and GitOps](iac/index.md)
|
||||
- [Kubernetes clusters](../project/clusters/index.md)
|
||||
- [Runbooks](../project/clusters/runbooks/index.md)
|
||||
GitLab offers various features to speed up and simplify your infrastructure management practices.
|
||||
|
||||
## Generic infrastructure management
|
||||
|
||||
GitLab has deep integrations with Terraform to run your infrastructure as code pipelines
|
||||
and support your processes. Terraform is considered the standard in cloud infrastructure provisioning.
|
||||
The various GitLab integrations help you:
|
||||
|
||||
- Get started quickly without any setup.
|
||||
- Collaborate around infrastructure changes in merge requests the same as you might
|
||||
with code changes.
|
||||
- Scale using a module registry.
|
||||
|
||||
Read more about the [Infrastructure as Code features](iac/index.md), including:
|
||||
|
||||
- [The GitLab Managed Terraform State](terraform_state.md).
|
||||
- [The Terraform MR widget](mr_integration.md).
|
||||
- [The Terraform module registry](../packages/terraform_module_registry/index.md).
|
||||
|
||||
## Integrated Kubernetes management
|
||||
|
||||
GitLab has special integrations with Kubernetes to help you deploy, manage and troubleshoot
|
||||
third-party or custom applications in Kubernetes clusters. Auto DevOps provides a full
|
||||
DevSecOps pipeline by default targeted at Kubernetes based deployments. To support
|
||||
all the GitLab features, GitLab offers a cluster management project for easy onboarding.
|
||||
The deploy boards provide quick insights into your cluster, including pod logs tailing.
|
||||
|
||||
The recommended approach to connect to a cluster is using [the GitLab Kubernetes Agent](../clusters/agent/index.md).
|
||||
|
||||
Read more about [the Kubernetes cluster support and integrations](../project/clusters/index.md), including:
|
||||
|
||||
- Certificate-based integration for [projects](../project/clusters/index.md),
|
||||
[groups](../group/clusters/index.md), or [instances](../instance/clusters/index.md).
|
||||
- [Agent-based integration](../clusters/agent/index.md). **(PREMIUM)**
|
||||
- The [Kubernetes Agent Server](../../administration/clusters/kas.md) is [available on GitLab.com](../clusters/agent/index.md#set-up-the-kubernetes-agent-server)
|
||||
at `wss://kas.gitlab.com`. **(PREMIUM)**
|
||||
- [Agent-based access from GitLab CI/CD](../clusters/agent/ci_cd_tunnel.md).
|
||||
|
||||
## Runbooks in GitLab
|
||||
|
||||
Runbooks are a collection of documented procedures that explain how to carry out a task,
|
||||
such as starting, stopping, debugging, or troubleshooting a system.
|
||||
|
||||
Read more about [how executable runbooks work in GitLab](../project/clusters/runbooks/index.md).
|
||||
|
|
|
@ -29472,7 +29472,7 @@ msgstr ""
|
|||
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability of high, critical, or unknown severity."
|
||||
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
|
||||
|
@ -29508,7 +29508,7 @@ msgstr ""
|
|||
msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}More information%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Requires approval for vulnerabilities of Critical, High, or Unknown severity. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgid "SecurityApprovals|Requires approval for vulnerabilities. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Test coverage must be enabled. %{linkStart}Learn more%{linkEnd}."
|
||||
|
@ -37184,9 +37184,6 @@ msgstr ""
|
|||
msgid "Vulnerability|Request/Response"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Scanner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Scanner Provider"
|
||||
msgstr ""
|
||||
|
||||
|
@ -37199,6 +37196,9 @@ msgstr ""
|
|||
msgid "Vulnerability|The unmodified response is the original response that had no mutations done to the request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Tool"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Unmodified Response"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -18,12 +18,14 @@ module QA
|
|||
:iid,
|
||||
:assignee_ids,
|
||||
:labels,
|
||||
:title
|
||||
:title,
|
||||
:description
|
||||
|
||||
def initialize
|
||||
@assignee_ids = []
|
||||
@labels = []
|
||||
@title = "Issue title #{SecureRandom.hex(8)}"
|
||||
@description = "Issue description #{SecureRandom.hex(8)}"
|
||||
end
|
||||
|
||||
def fabricate!
|
||||
|
@ -34,7 +36,7 @@ module QA
|
|||
Page::Project::Issue::New.perform do |new_page|
|
||||
new_page.fill_title(@title)
|
||||
new_page.choose_template(@template) if @template
|
||||
new_page.fill_description(@description) if @description
|
||||
new_page.fill_description(@description) if @description && !@template
|
||||
new_page.choose_milestone(@milestone) if @milestone
|
||||
new_page.create_new_issue
|
||||
end
|
||||
|
@ -64,6 +66,7 @@ module QA
|
|||
}.tap do |hash|
|
||||
hash[:milestone_id] = @milestone.id if @milestone
|
||||
hash[:weight] = @weight if @weight
|
||||
hash[:description] = @description if @description
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,10 @@ module QA
|
|||
extend self
|
||||
extend Support::Api
|
||||
|
||||
RETRY_MAX_ITERATION = 10
|
||||
RETRY_SLEEP_INTERVAL = 12
|
||||
INSERT_RECALL_THRESHOLD = RETRY_MAX_ITERATION * RETRY_SLEEP_INTERVAL
|
||||
|
||||
ElasticSearchServerError = Class.new(RuntimeError)
|
||||
|
||||
def assert_elasticsearch_responding
|
||||
|
@ -85,7 +89,7 @@ module QA
|
|||
private
|
||||
|
||||
def find_target_in_scope(scope, search_term)
|
||||
QA::Support::Retrier.retry_until(max_attempts: 10, sleep_interval: 10, raise_on_failure: true, retry_on_exception: true) do
|
||||
QA::Support::Retrier.retry_until(max_attempts: RETRY_MAX_ITERATION, sleep_interval: RETRY_SLEEP_INTERVAL, raise_on_failure: true, retry_on_exception: true) do
|
||||
result = search(scope, search_term)
|
||||
result && result.any? { |record| yield record }
|
||||
end
|
||||
|
|
|
@ -133,7 +133,7 @@ module QA
|
|||
it 'imports large Github repo via api' do
|
||||
start = Time.now
|
||||
|
||||
imported_project # import the project
|
||||
Runtime::Logger.info("Importing project '#{imported_project.full_path}'") # import the project and log path
|
||||
fetch_github_objects # fetch all objects right after import has started
|
||||
|
||||
import_status = lambda do
|
||||
|
@ -221,32 +221,39 @@ module QA
|
|||
# @return [void]
|
||||
def verify_mrs_or_issues(type)
|
||||
msg = ->(title) { "expected #{type} with title '#{title}' to have" }
|
||||
expected = type == 'mr' ? mrs : gl_issues
|
||||
actual = type == 'mr' ? gh_prs : gh_issues
|
||||
|
||||
# Compare length to have easy to read overview how many objects are missing
|
||||
expect(expected.length).to(
|
||||
eq(actual.length),
|
||||
"Expected to contain same amount of #{type}s. Expected: #{expected.length}, actual: #{actual.length}"
|
||||
)
|
||||
expected = type == 'mr' ? mrs : gl_issues
|
||||
actual = type == 'mr' ? gh_prs : gh_issues
|
||||
count_msg = "Expected to contain same amount of #{type}s. Gitlab: #{expected.length}, Github: #{actual.length}"
|
||||
expect(expected.length).to eq(actual.length), count_msg
|
||||
|
||||
logger.debug("= Comparing #{type}s =")
|
||||
actual.each do |title, actual_item|
|
||||
print "." # indicate that it is still going but don't spam the output with newlines
|
||||
|
||||
expected_item = expected[title]
|
||||
|
||||
# Print title in the error message to see which object is missing
|
||||
expect(expected_item).to be_truthy, "#{msg.call(title)} been imported"
|
||||
next unless expected_item
|
||||
|
||||
expect(expected_item[:body]).to(
|
||||
include(actual_item[:body]),
|
||||
"#{msg.call(title)} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])}"
|
||||
)
|
||||
expect(expected_item[:comments].length).to(
|
||||
eq(actual_item[:comments].length),
|
||||
"#{msg.call(title)} same amount of comments"
|
||||
)
|
||||
expect(expected_item[:comments]).to match_array(actual_item[:comments])
|
||||
# Print difference in the description
|
||||
expected_body = expected_item[:body]
|
||||
actual_body = actual_item[:body]
|
||||
body_msg = <<~MSG
|
||||
#{msg.call(title)} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])}
|
||||
MSG
|
||||
expect(expected_body).to include(actual_body), body_msg
|
||||
|
||||
# Print amount difference first
|
||||
expected_comments = expected_item[:comments]
|
||||
actual_comments = actual_item[:comments]
|
||||
comment_count_msg = <<~MSG
|
||||
#{msg.call(title)} same amount of comments. Gitlab: #{expected_comments.length}, Github: #{actual_comments.length}
|
||||
MSG
|
||||
expect(expected_comments.length).to eq(actual_comments.length), comment_count_msg
|
||||
expect(expected_comments).to match_array(actual_comments)
|
||||
end
|
||||
puts # print newline after last print to make output pretty
|
||||
end
|
||||
|
|
|
@ -18,6 +18,13 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with nil group' do
|
||||
it 'returns all runners' do
|
||||
expect(Ci::Runner).to receive(:with_tags).and_call_original
|
||||
expect(described_class.new(current_user: admin, params: { group: nil }).execute).to match_array [runner1, runner2]
|
||||
end
|
||||
end
|
||||
|
||||
context 'with preload param set to :tag_name true' do
|
||||
it 'requests tags' do
|
||||
expect(Ci::Runner).to receive(:with_tags).and_call_original
|
||||
|
@ -158,6 +165,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
let_it_be(:project_4) { create(:project, group: sub_group_2) }
|
||||
let_it_be(:project_5) { create(:project, group: sub_group_3) }
|
||||
let_it_be(:project_6) { create(:project, group: sub_group_4) }
|
||||
let_it_be(:runner_instance) { create(:ci_runner, :instance, contacted_at: 13.minutes.ago) }
|
||||
let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago) }
|
||||
let_it_be(:runner_sub_group_1) { create(:ci_runner, :group, active: false, contacted_at: 11.minutes.ago) }
|
||||
let_it_be(:runner_sub_group_2) { create(:ci_runner, :group, contacted_at: 10.minutes.ago) }
|
||||
|
@ -171,7 +179,10 @@ RSpec.describe Ci::RunnersFinder do
|
|||
let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5])}
|
||||
let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6])}
|
||||
|
||||
let(:params) { {} }
|
||||
let(:target_group) { nil }
|
||||
let(:membership) { nil }
|
||||
let(:extra_params) { {} }
|
||||
let(:params) { { group: target_group, membership: membership }.merge(extra_params).reject { |_, v| v.nil? } }
|
||||
|
||||
before do
|
||||
group.runners << runner_group
|
||||
|
@ -182,14 +193,9 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject { described_class.new(current_user: user, group: group, params: params).execute }
|
||||
subject { described_class.new(current_user: user, params: params).execute }
|
||||
|
||||
context 'with user as group owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'passing no params' do
|
||||
shared_examples 'membership equal to :descendants' do
|
||||
it 'returns all descendant runners' do
|
||||
expect(subject).to eq([runner_project_7, runner_project_6, runner_project_5,
|
||||
runner_project_4, runner_project_3, runner_project_2,
|
||||
|
@ -198,8 +204,51 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with user as group owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'with :group as target group' do
|
||||
let(:target_group) { group }
|
||||
|
||||
context 'passing no params' do
|
||||
it_behaves_like 'membership equal to :descendants'
|
||||
end
|
||||
|
||||
context 'with :descendants membership' do
|
||||
let(:membership) { :descendants }
|
||||
|
||||
it_behaves_like 'membership equal to :descendants'
|
||||
end
|
||||
|
||||
context 'with :direct membership' do
|
||||
let(:membership) { :direct }
|
||||
|
||||
it 'returns runners belonging to group' do
|
||||
expect(subject).to eq([runner_group])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown membership' do
|
||||
let(:membership) { :unsupported }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(ArgumentError, 'Invalid membership filter')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with nil group' do
|
||||
let(:target_group) { nil }
|
||||
|
||||
it 'returns no runners' do
|
||||
# Query should run against all runners, however since user is not admin, query returns no results
|
||||
expect(subject).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sort param' do
|
||||
let(:params) { { sort: 'contacted_asc' } }
|
||||
let(:extra_params) { { sort: 'contacted_asc' } }
|
||||
|
||||
it 'sorts by specified attribute' do
|
||||
expect(subject).to eq([runner_group, runner_sub_group_1, runner_sub_group_2,
|
||||
|
@ -211,7 +260,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
|
||||
context 'filtering' do
|
||||
context 'by search term' do
|
||||
let(:params) { { search: 'runner_project_search' } }
|
||||
let(:extra_params) { { search: 'runner_project_search' } }
|
||||
|
||||
it 'returns correct runner' do
|
||||
expect(subject).to eq([runner_project_3])
|
||||
|
@ -219,7 +268,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
|
||||
context 'by status' do
|
||||
let(:params) { { status_status: 'paused' } }
|
||||
let(:extra_params) { { status_status: 'paused' } }
|
||||
|
||||
it 'returns correct runner' do
|
||||
expect(subject).to eq([runner_sub_group_1])
|
||||
|
@ -227,7 +276,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
|
||||
context 'by tag_name' do
|
||||
let(:params) { { tag_name: %w[runner_tag] } }
|
||||
let(:extra_params) { { tag_name: %w[runner_tag] } }
|
||||
|
||||
it 'returns correct runner' do
|
||||
expect(subject).to eq([runner_project_5])
|
||||
|
@ -235,7 +284,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
|
||||
context 'by runner type' do
|
||||
let(:params) { { type_type: 'project_type' } }
|
||||
let(:extra_params) { { type_type: 'project_type' } }
|
||||
|
||||
it 'returns correct runners' do
|
||||
expect(subject).to eq([runner_project_7, runner_project_6,
|
||||
|
@ -245,6 +294,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not group owner' do
|
||||
where(:user_permission) do
|
||||
|
@ -278,7 +328,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
|
||||
describe '#sort_key' do
|
||||
subject { described_class.new(current_user: user, group: group, params: params).sort_key }
|
||||
subject { described_class.new(current_user: user, params: params.merge(group: group)).sort_key }
|
||||
|
||||
context 'without params' do
|
||||
it 'returns created_at_desc' do
|
||||
|
@ -287,7 +337,7 @@ RSpec.describe Ci::RunnersFinder do
|
|||
end
|
||||
|
||||
context 'with params' do
|
||||
let(:params) { { sort: 'contacted_asc' } }
|
||||
let(:extra_params) { { sort: 'contacted_asc' } }
|
||||
|
||||
it 'returns contacted_asc' do
|
||||
expect(subject).to eq('contacted_asc')
|
||||
|
|
94
spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
Normal file
94
spec/graphql/resolvers/ci/group_runners_resolver_spec.rb
Normal file
|
@ -0,0 +1,94 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::Ci::GroupRunnersResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
describe '#resolve' do
|
||||
subject { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) }
|
||||
|
||||
include_context 'runners resolver setup'
|
||||
|
||||
let(:obj) { group }
|
||||
let(:args) { {} }
|
||||
|
||||
# First, we can do a couple of basic real tests to verify common cases. That ensures that the code works.
|
||||
context 'when user cannot see runners' do
|
||||
it 'returns no runners' do
|
||||
expect(subject.items.to_a).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user as group owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns all the runners' do
|
||||
expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner)
|
||||
end
|
||||
|
||||
context 'with membership direct' do
|
||||
let(:args) { { membership: :direct } }
|
||||
|
||||
it 'returns only direct runners' do
|
||||
expect(subject.items.to_a).to contain_exactly(group_runner)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Then, we can check specific edge cases for this resolver
|
||||
context 'with obj set to nil' do
|
||||
let(:obj) { nil }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error('Expected group missing')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with obj not set to group' do
|
||||
let(:obj) { build(:project) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error('Expected group missing')
|
||||
end
|
||||
end
|
||||
|
||||
# Here we have a mocked part. We assume that all possible edge cases are covered in RunnersFinder spec. So we don't need to test them twice.
|
||||
# Only thing we can do is to verify that args from the resolver is correctly transformed to params of the Finder and we return the Finder's result back.
|
||||
describe 'Allowed query arguments' do
|
||||
let(:finder) { instance_double(::Ci::RunnersFinder) }
|
||||
let(:args) do
|
||||
{
|
||||
status: 'active',
|
||||
type: :group_type,
|
||||
tag_list: ['active_runner'],
|
||||
search: 'abc',
|
||||
sort: :contacted_asc,
|
||||
membership: :descendants
|
||||
}
|
||||
end
|
||||
|
||||
let(:expected_params) do
|
||||
{
|
||||
status_status: 'active',
|
||||
type_type: :group_type,
|
||||
tag_name: ['active_runner'],
|
||||
preload: { tag_name: nil },
|
||||
search: 'abc',
|
||||
sort: 'contacted_asc',
|
||||
membership: :descendants,
|
||||
group: group
|
||||
}
|
||||
end
|
||||
|
||||
it 'calls RunnersFinder with expected arguments' do
|
||||
allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
|
||||
allow(finder).to receive(:execute).once.and_return([:execute_return_value])
|
||||
|
||||
expect(subject.items.to_a).to eq([:execute_return_value])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,185 +5,70 @@ require 'spec_helper'
|
|||
RSpec.describe Resolvers::Ci::RunnersResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create_default(:user, :admin) }
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
|
||||
let_it_be(:inactive_project_runner) do
|
||||
create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
|
||||
end
|
||||
|
||||
let_it_be(:offline_project_runner) do
|
||||
create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
|
||||
end
|
||||
|
||||
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 1.second.ago) }
|
||||
let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
|
||||
|
||||
describe '#resolve' do
|
||||
subject { resolve(described_class, ctx: { current_user: user }, args: args).items.to_a }
|
||||
let(:obj) { nil }
|
||||
let(:args) { {} }
|
||||
|
||||
let(:args) do
|
||||
{}
|
||||
end
|
||||
subject { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) }
|
||||
|
||||
context 'when the user cannot see runners' do
|
||||
let(:user) { create(:user) }
|
||||
include_context 'runners resolver setup'
|
||||
|
||||
# First, we can do a couple of basic real tests to verify common cases. That ensures that the code works.
|
||||
context 'when user cannot see runners' do
|
||||
let(:user) { build(:user) }
|
||||
|
||||
it 'returns no runners' do
|
||||
is_expected.to be_empty
|
||||
expect(subject.items.to_a).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without sort' do
|
||||
context 'when user can see runners' do
|
||||
let(:obj) { nil }
|
||||
|
||||
it 'returns all the runners' do
|
||||
is_expected.to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, instance_runner)
|
||||
expect(subject.items.to_a).to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, subgroup_runner, instance_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a sort argument' do
|
||||
context "set to :contacted_asc" do
|
||||
# Then, we can check specific edge cases for this resolver
|
||||
context 'with obj not set to nil' do
|
||||
let(:obj) { build(:project) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(a_string_including('Unexpected parent type'))
|
||||
end
|
||||
end
|
||||
|
||||
# Here we have a mocked part. We assume that all possible edge cases are covered in RunnersFinder spec. So we don't need to test them twice.
|
||||
# Only thing we can do is to verify that args from the resolver is correctly transformed to params of the Finder and we return the Finder's result back.
|
||||
describe 'Allowed query arguments' do
|
||||
let(:finder) { instance_double(::Ci::RunnersFinder) }
|
||||
let(:args) do
|
||||
{ sort: :contacted_asc }
|
||||
{
|
||||
status: 'active',
|
||||
type: :instance_type,
|
||||
tag_list: ['active_runner'],
|
||||
search: 'abc',
|
||||
sort: :contacted_asc
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner]) }
|
||||
let(:expected_params) do
|
||||
{
|
||||
status_status: 'active',
|
||||
type_type: :instance_type,
|
||||
tag_name: ['active_runner'],
|
||||
preload: { tag_name: nil },
|
||||
search: 'abc',
|
||||
sort: 'contacted_asc'
|
||||
}
|
||||
end
|
||||
|
||||
context "set to :contacted_desc" do
|
||||
let(:args) do
|
||||
{ sort: :contacted_desc }
|
||||
end
|
||||
it 'calls RunnersFinder with expected arguments' do
|
||||
allow(::Ci::RunnersFinder).to receive(:new).with(current_user: user, params: expected_params).once.and_return(finder)
|
||||
allow(finder).to receive(:execute).once.and_return([:execute_return_value])
|
||||
|
||||
it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner].reverse) }
|
||||
end
|
||||
|
||||
context "set to :created_at_desc" do
|
||||
let(:args) do
|
||||
{ sort: :created_at_desc }
|
||||
end
|
||||
|
||||
it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner]) }
|
||||
end
|
||||
|
||||
context "set to :created_at_asc" do
|
||||
let(:args) do
|
||||
{ sort: :created_at_asc }
|
||||
end
|
||||
|
||||
it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner].reverse) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when type is filtered' do
|
||||
let(:args) do
|
||||
{ type: runner_type.to_s }
|
||||
end
|
||||
|
||||
context 'to instance runners' do
|
||||
let(:runner_type) { :instance_type }
|
||||
|
||||
it 'returns the instance runner' do
|
||||
is_expected.to contain_exactly(instance_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to group runners' do
|
||||
let(:runner_type) { :group_type }
|
||||
|
||||
it 'returns the group runner' do
|
||||
is_expected.to contain_exactly(group_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to project runners' do
|
||||
let(:runner_type) { :project_type }
|
||||
|
||||
it 'returns the project runner' do
|
||||
is_expected.to contain_exactly(inactive_project_runner, offline_project_runner)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is filtered' do
|
||||
let(:args) do
|
||||
{ status: runner_status.to_s }
|
||||
end
|
||||
|
||||
context 'to active runners' do
|
||||
let(:runner_status) { :active }
|
||||
|
||||
it 'returns the instance and group runners' do
|
||||
is_expected.to contain_exactly(offline_project_runner, group_runner, instance_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to offline runners' do
|
||||
let(:runner_status) { :offline }
|
||||
|
||||
it 'returns the offline project runner' do
|
||||
is_expected.to contain_exactly(offline_project_runner)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tag list is filtered' do
|
||||
let(:args) do
|
||||
{ tag_list: tag_list }
|
||||
end
|
||||
|
||||
context 'with "project_runner" tag' do
|
||||
let(:tag_list) { ['project_runner'] }
|
||||
|
||||
it 'returns the project_runner runners' do
|
||||
is_expected.to contain_exactly(offline_project_runner, inactive_project_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with "project_runner" and "active_runner" tags as comma-separated string' do
|
||||
let(:tag_list) { ['project_runner,active_runner'] }
|
||||
|
||||
it 'returns the offline_project_runner runner' do
|
||||
is_expected.to contain_exactly(offline_project_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with "active_runner" and "instance_runner" tags as array' do
|
||||
let(:tag_list) { %w[instance_runner active_runner] }
|
||||
|
||||
it 'returns the offline_project_runner runner' do
|
||||
is_expected.to contain_exactly(instance_runner)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when text is filtered' do
|
||||
let(:args) do
|
||||
{ search: search_term }
|
||||
end
|
||||
|
||||
context 'to "project"' do
|
||||
let(:search_term) { 'project' }
|
||||
|
||||
it 'returns both project runners' do
|
||||
is_expected.to contain_exactly(inactive_project_runner, offline_project_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to "group"' do
|
||||
let(:search_term) { 'group' }
|
||||
|
||||
it 'returns group runner' do
|
||||
is_expected.to contain_exactly(group_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to "defghi"' do
|
||||
let(:search_term) { 'defghi' }
|
||||
|
||||
it 'returns runners containing term in token' do
|
||||
is_expected.to contain_exactly(offline_project_runner)
|
||||
end
|
||||
expect(subject.items.to_a).to eq([:execute_return_value])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,7 +42,6 @@ RSpec.describe Ci::PipelineEditorHelper do
|
|||
"ci-config-path": project.ci_config_path_or_default,
|
||||
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
|
||||
"ci-help-page-path" => help_page_path('ci/index'),
|
||||
"commit-sha" => project.commit.sha,
|
||||
"default-branch" => project.default_branch_or_main,
|
||||
"empty-state-illustration-path" => 'foo',
|
||||
"initial-branch-name" => nil,
|
||||
|
@ -69,7 +68,6 @@ RSpec.describe Ci::PipelineEditorHelper do
|
|||
"ci-config-path": project.ci_config_path_or_default,
|
||||
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
|
||||
"ci-help-page-path" => help_page_path('ci/index'),
|
||||
"commit-sha" => '',
|
||||
"default-branch" => project.default_branch_or_main,
|
||||
"empty-state-illustration-path" => 'foo',
|
||||
"initial-branch-name" => nil,
|
||||
|
@ -97,10 +95,7 @@ RSpec.describe Ci::PipelineEditorHelper do
|
|||
end
|
||||
|
||||
it 'returns correct values' do
|
||||
latest_feature_sha = project.repository.commit('feature').sha
|
||||
|
||||
expect(pipeline_editor_data['initial-branch-name']).to eq('feature')
|
||||
expect(pipeline_editor_data['commit-sha']).to eq(latest_feature_sha)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
103
spec/migrations/backfill_stage_event_hash_spec.rb
Normal file
103
spec/migrations/backfill_stage_event_hash_spec.rb
Normal file
|
@ -0,0 +1,103 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe BackfillStageEventHash, schema: 20210730103808 do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:labels) { table(:labels) }
|
||||
let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
|
||||
let(:project_stages) { table(:analytics_cycle_analytics_project_stages) }
|
||||
let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
|
||||
let(:project_value_streams) { table(:analytics_cycle_analytics_project_value_streams) }
|
||||
let(:stage_event_hashes) { table(:analytics_cycle_analytics_stage_event_hashes) }
|
||||
|
||||
let(:issue_created) { 1 }
|
||||
let(:issue_closed) { 3 }
|
||||
let(:issue_label_removed) { 9 }
|
||||
let(:unknown_stage_event) { -1 }
|
||||
|
||||
let(:namespace) { namespaces.create!(name: 'ns', path: 'ns', type: 'Group') }
|
||||
let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
|
||||
let(:group_label) { labels.create!(title: 'label', type: 'GroupLabel', group_id: namespace.id) }
|
||||
let(:group_value_stream) { group_value_streams.create!(name: 'group vs', group_id: namespace.id) }
|
||||
let(:project_value_stream) { project_value_streams.create!(name: 'project vs', project_id: project.id) }
|
||||
|
||||
let(:group_stage_1) do
|
||||
group_stages.create!(
|
||||
name: 'stage 1',
|
||||
group_id: namespace.id,
|
||||
start_event_identifier: issue_created,
|
||||
end_event_identifier: issue_closed,
|
||||
group_value_stream_id: group_value_stream.id
|
||||
)
|
||||
end
|
||||
|
||||
let(:group_stage_2) do
|
||||
group_stages.create!(
|
||||
name: 'stage 2',
|
||||
group_id: namespace.id,
|
||||
start_event_identifier: issue_created,
|
||||
end_event_identifier: issue_label_removed,
|
||||
end_event_label_id: group_label.id,
|
||||
group_value_stream_id: group_value_stream.id
|
||||
)
|
||||
end
|
||||
|
||||
let(:project_stage_1) do
|
||||
project_stages.create!(
|
||||
name: 'stage 1',
|
||||
project_id: project.id,
|
||||
start_event_identifier: issue_created,
|
||||
end_event_identifier: issue_closed,
|
||||
project_value_stream_id: project_value_stream.id
|
||||
)
|
||||
end
|
||||
|
||||
let(:invalid_group_stage) do
|
||||
group_stages.create!(
|
||||
name: 'stage 3',
|
||||
group_id: namespace.id,
|
||||
start_event_identifier: issue_created,
|
||||
end_event_identifier: unknown_stage_event,
|
||||
group_value_stream_id: group_value_stream.id
|
||||
)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
it 'populates stage_event_hash_id column' do
|
||||
group_stage_1
|
||||
group_stage_2
|
||||
project_stage_1
|
||||
|
||||
migrate!
|
||||
|
||||
group_stage_1.reload
|
||||
group_stage_2.reload
|
||||
project_stage_1.reload
|
||||
|
||||
expect(group_stage_1.stage_event_hash_id).not_to be_nil
|
||||
expect(group_stage_2.stage_event_hash_id).not_to be_nil
|
||||
expect(project_stage_1.stage_event_hash_id).not_to be_nil
|
||||
|
||||
expect(stage_event_hashes.count).to eq(2) # group_stage_1 and project_stage_1 has the same hash
|
||||
end
|
||||
|
||||
it 'runs without problem without stages' do
|
||||
expect { migrate! }.not_to raise_error
|
||||
end
|
||||
|
||||
context 'when invalid event identifier is discovered' do
|
||||
it 'removes the stage' do
|
||||
group_stage_1
|
||||
invalid_group_stage
|
||||
|
||||
expect { migrate! }.not_to change { group_stage_1 }
|
||||
|
||||
expect(group_stages.find_by_id(invalid_group_stage.id)).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_context 'runners resolver setup' do
|
||||
let_it_be(:user) { create_default(:user, :admin) }
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:subgroup) { create(:group, :public, parent: group) }
|
||||
let_it_be(:project) { create(:project, :public, group: group) }
|
||||
|
||||
let_it_be(:inactive_project_runner) do
|
||||
create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
|
||||
end
|
||||
|
||||
let_it_be(:offline_project_runner) do
|
||||
create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
|
||||
end
|
||||
|
||||
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) }
|
||||
let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: 'mnopqr', description: 'subgroup runner', contacted_at: 1.second.ago) }
|
||||
let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
|
||||
end
|
Loading…
Reference in a new issue