Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-09 15:08:13 +00:00
parent 0c1344a7c1
commit 0a353a9fa3
69 changed files with 1077 additions and 299 deletions

View File

@ -1 +1 @@
659bff3b53d7b9a6894ac9404edbc45d02b49497
190b1b5820371124001739845957980599487c80

View File

@ -1,6 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { createAlert } from '~/flash';
import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@ -49,22 +48,6 @@ export default {
},
},
},
computed: {
description() {
return this.runner?.description;
},
heading() {
if (this.description) {
return sprintf(s__('Runners|Register "%{runnerDescription}" runner'), {
runnerDescription: this.description,
});
}
return s__('Runners|Register runner');
},
ephemeralAuthenticationToken() {
return this.runner?.ephemeralAuthenticationToken;
},
},
watch: {
platform(platform) {
updateHistory({
@ -84,12 +67,10 @@ export default {
</script>
<template>
<div>
<h1 class="gl-font-size-h1">{{ heading }}</h1>
<registration-instructions
:loading="$apollo.queries.runner.loading"
:runner="runner"
:platform="platform"
:token="ephemeralAuthenticationToken"
:loading="$apollo.queries.runner.loading"
@toggleDrawer="onToggleDrawer"
/>

View File

@ -1,6 +1,7 @@
<script>
import { GlIcon, GlLink, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { s__, sprintf } from '~/locale';
import { EXECUTORS_HELP_URL, SERVICE_COMMANDS_HELP_URL } from '../../constants';
import CliCommand from './cli_command.vue';
@ -16,6 +17,11 @@ export default {
CliCommand,
},
props: {
runner: {
type: Object,
required: false,
default: null,
},
platform: {
type: String,
required: true,
@ -25,26 +31,43 @@ export default {
required: false,
default: false,
},
token: {
type: String,
required: false,
default: null,
},
},
computed: {
description() {
return this.runner?.description;
},
heading() {
if (this.description) {
return sprintf(
s__('Runners|Register "%{runnerDescription}" runner'),
{
runnerDescription: this.description,
},
false,
);
}
return s__('Runners|Register runner');
},
token() {
return this.runner?.ephemeralAuthenticationToken;
},
commandPrompt() {
return commandPrompt({ platform: this.platform });
},
registerCommand() {
return registerCommand({ platform: this.platform, registrationToken: this.token });
return registerCommand({
platform: this.platform,
registrationToken: this.token,
description: this.description,
});
},
runCommand() {
return runCommand({ platform: this.platform });
},
},
methods: {
toggleDrawer(val) {
this.$emit('toggleDrawer', val);
toggleDrawer() {
this.$emit('toggleDrawer');
},
},
EXECUTORS_HELP_URL,
@ -53,6 +76,8 @@ export default {
</script>
<template>
<div>
<h1 class="gl-font-size-h1">{{ heading }}</h1>
<p>
<gl-sprintf
:message="
@ -62,7 +87,7 @@ export default {
"
>
<template #link="{ content }">
<gl-link @click="toggleDrawer()">{{ content }}</gl-link>
<gl-link @click="toggleDrawer">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>

View File

@ -12,19 +12,36 @@ import windowsInstall from './scripts/windows/install.ps1?raw';
const OS = {
[LINUX_PLATFORM]: {
shell: 'bash',
commandPrompt: '$',
executable: 'gitlab-runner',
},
[MACOS_PLATFORM]: {
shell: 'bash',
commandPrompt: '$',
executable: 'gitlab-runner',
},
[WINDOWS_PLATFORM]: {
shell: 'powershell',
commandPrompt: '>',
executable: '.\\gitlab-runner.exe',
},
};
const escapedParam = (param, shell = 'bash') => {
let escaped;
if (shell === 'bash') {
// replace single-quotes by the sequence '\''
escaped = param.replaceAll("'", "'\\''");
} else if (shell === 'powershell') {
// replace single-quotes by the sequence ''
// https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.3
escaped = param.replaceAll("'", "''");
}
// surround with single quotes.
return `'${escaped}'`;
};
export const commandPrompt = ({ platform }) => {
return (OS[platform] || OS[DEFAULT_PLATFORM]).commandPrompt;
};
@ -33,12 +50,28 @@ export const executable = ({ platform }) => {
return (OS[platform] || OS[DEFAULT_PLATFORM]).executable;
};
export const registerCommand = ({ platform, url = gon.gitlab_url, registrationToken }) => {
return [
`${executable({ platform })} register`,
` --url ${url}`,
...(registrationToken ? [` --registration-token ${registrationToken}`] : []),
];
const shell = ({ platform }) => {
return (OS[platform] || OS[DEFAULT_PLATFORM]).shell;
};
export const registerCommand = ({
platform,
url = gon.gitlab_url,
registrationToken,
description,
}) => {
const lines = [`${executable({ platform })} register`];
if (url) {
lines.push(` --url ${url}`);
}
if (registrationToken) {
lines.push(` --registration-token ${registrationToken}`);
}
if (description) {
const escapedDescription = escapedParam(description, shell({ platform }));
lines.push(` --description ${escapedDescription}`);
}
return lines;
};
export const runCommand = ({ platform }) => {

View File

@ -22,7 +22,9 @@ export default (input, parameters, escapeParameters = true) => {
mappedParameters.forEach((key, parameterName) => {
const parameterValue = mappedParameters.get(parameterName);
const escapedParameterValue = escapeParameters ? escape(parameterValue) : parameterValue;
output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue);
// Pass the param value as a function to ignore special replacement patterns like $` and $'.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#syntax
output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), () => escapedParameterValue);
});
}

View File

@ -22,6 +22,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-ultimate-feature-removal-banner',
'.js-geo-enable-hashed-storage-callout',
'.js-geo-migrate-hashed-storage-callout',
'.js-unlimited-members-during-trial-alert',
];
const initCallouts = () => {

View File

@ -40,7 +40,7 @@ export default {
:is="component"
:aria-label="ariaLabel"
:href="href"
class="counter gl-display-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border gl-border-gray-a-08 gl-font-sm gl-hover-text-gray-900 gl-hover-text-decoration-none"
class="counter gl-display-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-font-sm gl-hover-text-gray-900 gl-hover-text-decoration-none"
>
<gl-icon aria-hidden="true" :name="icon" />
<span v-if="count" aria-hidden="true" class="gl-ml-1">{{ count }}</span>

View File

@ -1,6 +1,7 @@
<script>
import axios from '~/lib/utils/axios_utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { updateDraft, clearDraft, getDraft } from '~/lib/utils/autosave';
import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants';
import MarkdownField from './field.vue';
@ -52,10 +53,15 @@ export default {
required: false,
default: false,
},
autosaveKey: {
type: String,
required: false,
default: null,
},
},
data() {
return {
markdown: this.value || '',
markdown: this.value || (this.autosaveKey ? getDraft(this.autosaveKey) : '') || '',
editingMode: EDITING_MODE_MARKDOWN_FIELD,
autofocused: false,
};
@ -72,19 +78,27 @@ export default {
watch: {
value(val) {
this.markdown = val;
this.saveDraft();
},
},
mounted() {
this.autofocusTextarea();
this.saveDraft();
},
methods: {
updateMarkdownFromContentEditor({ markdown }) {
this.markdown = markdown;
this.$emit('input', markdown);
this.saveDraft();
},
updateMarkdownFromMarkdownField({ target }) {
this.markdown = target.value;
this.$emit('input', target.value);
this.saveDraft();
},
renderMarkdown(markdown) {
return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body);
@ -110,6 +124,11 @@ export default {
setEditorAsAutofocused() {
this.autofocused = true;
},
saveDraft() {
if (!this.autosaveKey) return;
if (this.markdown) updateDraft(this.autosaveKey, this.markdown);
else clearDraft(this.autosaveKey);
},
},
};
</script>

View File

@ -91,6 +91,7 @@
.gl-new-dropdown-custom-toggle[aria-expanded='true'] .counter {
background-color: $gray-50;
border-color: transparent;
box-shadow: none;
mix-blend-mode: multiply;
.gl-dark & {

View File

@ -135,10 +135,9 @@
}
}
// Limits the width of the user bio for readability.
.profile-user-bio {
// Limits the width of the user bio for readability.
max-width: 600px;
margin: 10px auto;
}
.user-calendar {

View File

@ -252,19 +252,6 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
.gl-gap-2 {
gap: $gl-spacing-scale-2;
}
.gl-bg-t-gray-a-08 {
background-color: $t-gray-a-08;
}
.gl-hover-bg-t-gray-a-08:hover {
background-color: $t-gray-a-08;
}
/* End gitlab-ui#1709 */
/*
@ -284,3 +271,27 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
.gl-isolate {
isolation: isolate;
}
/*
* The below style will be moved to @gitlab/ui by
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2177
*/
.gl-gap-2 {
gap: $gl-spacing-scale-2;
}
.gl-bg-t-gray-a-08 {
background-color: $t-gray-a-08;
}
.gl-hover-bg-t-gray-a-08:hover {
background-color: $t-gray-a-08;
}
.gl-inset-border-1-gray-a-08 {
box-shadow: inset 0 0 0 $gl-border-size-1 $t-gray-a-08;
}
.gl-line-height-1 {
line-height: 1;
}

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Types
module Ci
class RunnerMachineType < BaseObject
graphql_name 'CiRunnerMachine'
connection_type_class(::Types::CountableConnectionType)
authorize :read_runner_machine
alias_method :runner_machine, :object
field :architecture_name, GraphQL::Types::String, null: true,
description: 'Architecture provided by the runner machine.',
method: :architecture
field :contacted_at, Types::TimeType, null: true,
description: 'Timestamp of last contact from the runner machine.',
method: :contacted_at
field :created_at, Types::TimeType, null: true,
description: 'Timestamp of creation of the runner machine.'
field :executor_name, GraphQL::Types::String, null: true,
description: 'Executor last advertised by the runner.',
method: :executor_name
field :id, ::Types::GlobalIDType[::Ci::RunnerMachine], null: false,
description: 'ID of the runner machine.'
field :ip_address, GraphQL::Types::String, null: true,
description: 'IP address of the runner machine.'
field :platform_name, GraphQL::Types::String, null: true,
description: 'Platform provided by the runner machine.',
method: :platform
field :revision, GraphQL::Types::String, null: true, description: 'Revision of the runner.'
field :runner, RunnerType, null: true, description: 'Runner configuration for the runner machine.'
field :status,
Types::Ci::RunnerStatusEnum,
null: false,
description: 'Status of the runner machine.'
field :version, GraphQL::Types::String, null: true, description: 'Version of the runner.'
def executor_name
::Ci::Runner::EXECUTOR_TYPE_TO_NAMES[runner_machine.executor_type&.to_sym]
end
end
end
end
Types::Ci::RunnerType.prepend_mod_with('Types::Ci::RunnerType')

View File

@ -65,6 +65,9 @@ module Types
resolver: ::Resolvers::Ci::RunnerJobsResolver
field :locked, GraphQL::Types::Boolean, null: true,
description: 'Indicates the runner is locked.'
field :machines, ::Types::Ci::RunnerMachineType.connection_type, null: true,
description: 'Machines associated with the runner configuration.',
method: :runner_machines
field :maintenance_note, GraphQL::Types::String, null: true,
description: 'Runner\'s maintenance notes.'
field :maximum_timeout, GraphQL::Types::Int, null: true,

View File

@ -11,7 +11,6 @@ module Ci
include Gitlab::OptimisticLocking
include Gitlab::Utils::StrongMemoize
include AtomicInternalId
include EnumWithNil
include Ci::HasRef
include ShaAttribute
include FromUnion
@ -143,9 +142,9 @@ module Ci
# We use `Enums::Ci::Pipeline.sources` here so that EE can more easily extend
# this `Hash` with new values.
enum_with_nil source: Enums::Ci::Pipeline.sources
enum source: Enums::Ci::Pipeline.sources
enum_with_nil config_source: Enums::Ci::Pipeline.config_sources
enum config_source: Enums::Ci::Pipeline.config_sources
# We use `Enums::Ci::Pipeline.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.

View File

@ -84,8 +84,13 @@ module Ci
scope :active, -> (value = true) { where(active: value) }
scope :paused, -> { active(false) }
scope :online, -> { where('contacted_at > ?', online_contact_time_deadline) }
scope :recent, -> { where('ci_runners.created_at >= :date OR ci_runners.contacted_at >= :date', date: stale_deadline) }
scope :stale, -> { where('ci_runners.created_at < :date AND (ci_runners.contacted_at IS NULL OR ci_runners.contacted_at < :date)', date: stale_deadline) }
scope :recent, -> do
where('ci_runners.created_at >= :datetime OR ci_runners.contacted_at >= :datetime', datetime: stale_deadline)
end
scope :stale, -> do
where('ci_runners.created_at <= :datetime AND ' \
'(ci_runners.contacted_at IS NULL OR ci_runners.contacted_at <= :datetime)', datetime: stale_deadline)
end
scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
scope :never_contacted, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
@ -336,7 +341,7 @@ module Ci
def stale?
return false unless created_at
[created_at, contacted_at].compact.max < self.class.stale_deadline
[created_at, contacted_at].compact.max <= self.class.stale_deadline
end
def status(legacy_mode = nil)

View File

@ -41,6 +41,14 @@ module Ci
remove_duplicates: false).where(created_some_time_ago)
end
def self.online_contact_time_deadline
Ci::Runner.online_contact_time_deadline
end
def self.stale_deadline
STALE_TIMEOUT.ago
end
def heartbeat(values, update_contacted_at: true)
##
# We can safely ignore writes performed by a runner heartbeat. We do
@ -64,8 +72,25 @@ module Ci
end
end
def status
return :stale if stale?
return :never_contacted unless contacted_at
online? ? :online : :offline
end
private
def online?
contacted_at && contacted_at > self.class.online_contact_time_deadline
end
def stale?
return false unless created_at
[created_at, contacted_at].compact.max <= self.class.stale_deadline
end
def persist_cached_data?
# Use a random threshold to prevent beating DB updates.
contacted_at_max_age = Random.rand(UPDATE_CONTACT_COLUMN_EVERY)

View File

@ -3,9 +3,8 @@
module Ci
class RunnerVersion < Ci::ApplicationRecord
include EachBatch
include EnumWithNil
enum_with_nil status: {
enum status: {
not_processed: nil,
invalid_version: -1,
unavailable: 1,

View File

@ -4,7 +4,6 @@ module Clusters
module Platforms
class Kubernetes < ApplicationRecord
include Gitlab::Kubernetes
include EnumWithNil
include AfterCommitQueue
include ReactiveCaching
include NullifyIfBlank
@ -63,7 +62,7 @@ module Clusters
alias_attribute :ca_pem, :ca_cert
enum_with_nil authorization_type: {
enum authorization_type: {
unknown_authorization: nil,
rbac: 1,
abac: 2

View File

@ -6,7 +6,6 @@ class CommitStatus < Ci::ApplicationRecord
include Importable
include AfterCommitQueue
include Presentable
include EnumWithNil
include BulkInsertableAssociations
include TaggableQueries
@ -26,7 +25,7 @@ class CommitStatus < Ci::ApplicationRecord
enum scheduling_type: { stage: 0, dag: 1 }, _prefix: true
# We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons
enum failure_reason: Enums::Ci::CommitStatus.failure_reasons
delegate :commit, to: :pipeline
delegate :sha, :short_sha, :before_sha, to: :pipeline

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
module EnumWithNil
extend ActiveSupport::Concern
included do
def self.enum_with_nil(definitions)
# use original `enum` to auto-define all methods
enum(definitions)
# override auto-defined methods only for the
# key which uses nil value
definitions.each do |name, values|
# E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
# this overrides auto-generated method `failure_reason`
define_method(name) do
orig = super()
return orig unless orig.nil?
self.class.public_send(name.to_s.pluralize).key(nil) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
end

View File

@ -1722,11 +1722,7 @@ class User < ApplicationRecord
end
def manageable_groups(include_groups_with_developer_maintainer_access: false)
owned_and_maintainer_group_hierarchy = if Feature.enabled?(:linear_user_manageable_groups, self)
owned_or_maintainers_groups.self_and_descendants
else
Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
end
owned_and_maintainer_group_hierarchy = owned_or_maintainers_groups.self_and_descendants
if include_groups_with_developer_maintainer_access
union_sql = ::Gitlab::SQL::Union.new(

View File

@ -25,7 +25,8 @@ module Users
usage_quota_trial_alert: 14, # EE-only
preview_usage_quota_free_plan_alert: 15, # EE-only
enforcement_at_limit_alert: 16, # EE-only
web_hook_disabled: 17 # EE-only
web_hook_disabled: 17, # EE-only
unlimited_members_during_trial_alert: 18 # EE-only
}
validates :group, presence: true

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Ci
class RunnerMachinePolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:can_read_runner, scope: :subject) do
can?(:read_runner, @subject.runner)
end
rule { anonymous }.prevent_all
rule { can_read_runner }.policy do
enable :read_builds
enable :read_runner_machine
end
end
end

View File

@ -1,6 +1,9 @@
- add_page_specific_style 'page_bundles/members'
- page_title _('Group members')
= content_for :page_level_alert do
= render_if_exists 'shared/unlimited_members_during_trial_alert', group: @group.root_ancestor
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap

View File

@ -3,6 +3,10 @@
= render_if_exists 'shared/ultimate_feature_removal_banner', project: @project
= content_for :page_level_alert do
- if can_invite_members_for_project?(@project)
= render_if_exists 'shared/unlimited_members_during_trial_alert', group: @project.root_ancestor
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap

View File

@ -115,10 +115,9 @@
- if display_public_email?(@user)
= render 'middle_dot_divider', stacking: true do
= link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email'
- if @user.bio.present? && @user.confirmed? && !@user.blocked?
.gl-text-gray-900.gl-mx-5
.profile-user-bio.gl-text-left
= @user.bio
- if @user.bio.present? && @user.confirmed? && !@user.blocked?
%p.profile-user-bio.gl-mb-3
= @user.bio
- if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user)
.scrolling-tabs-container

View File

@ -1,8 +0,0 @@
---
name: linear_user_manageable_groups
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68845
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339434
milestone: '14.3'
type: development
group: group::authentication and authorization
default_enabled: false

View File

@ -244,6 +244,10 @@ p_ci_builds_metadata:
- table: projects
column: project_id
on_delete: async_delete
p_ci_runner_machine_builds:
- table: ci_runner_machines
column: runner_machine_id
on_delete: async_delete
packages_build_infos:
- table: ci_pipelines
column: pipeline_id

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class DropRunnerMachinesConstraintOnCiBuildsMetadata < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
SOURCE_TABLE_NAME = 'p_ci_builds_metadata'
TARGET_TABLE_NAME = 'ci_runner_machines'
CONSTRAINT_NAME = 'fk_rails_fae01b2700'
def up
with_lock_retries(raise_on_exhaustion: true) do
remove_foreign_key_if_exists(SOURCE_TABLE_NAME, TARGET_TABLE_NAME, name: CONSTRAINT_NAME)
end
end
def down
with_lock_retries(raise_on_exhaustion: true) do
next if check_constraint_exists?(SOURCE_TABLE_NAME, CONSTRAINT_NAME)
execute(<<~SQL)
ALTER TABLE #{SOURCE_TABLE_NAME}
ADD CONSTRAINT #{CONSTRAINT_NAME} FOREIGN KEY (runner_machine_id)
REFERENCES #{TARGET_TABLE_NAME}(id) ON DELETE SET NULL
SQL
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class EnsureCiRunnerMachinesIsEmpty < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
return unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(:gitlab_ci)
# Ensure that the ci_runner_machines table is empty to ensure that new builds
# don't try to create new join records until we add the missing FK.
execute('TRUNCATE TABLE ci_runner_machines, p_ci_runner_machine_builds')
end
def down
# no-op
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexOnRunnerMachineIdOnRunnerMachineBuilds < Gitlab::Database::Migration[2.1]
include Gitlab::Database::PartitioningMigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'index_p_ci_runner_machine_builds_on_runner_machine_id'
def up
add_concurrent_partitioned_index :p_ci_runner_machine_builds, :runner_machine_id, unique: false, name: INDEX_NAME
end
def down
remove_concurrent_partitioned_index_by_name :p_ci_runner_machine_builds, INDEX_NAME
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class TrackCiRunnerMachineRecordChanges < Gitlab::Database::Migration[2.1]
include Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers
enable_lock_retries!
def up
track_record_deletions(:ci_runner_machines)
end
def down
untrack_record_deletions(:ci_runner_machines)
end
end

View File

@ -0,0 +1 @@
ecb6f601d4f47e7c4974e097c0e87ff37f96fad93b2ab02439bfa44a7eb481cd

View File

@ -0,0 +1 @@
671fe2bcc6b45d7f312144d6c1ceb7c5e085dbc1ab1069c5a340849a08437d72

View File

@ -0,0 +1 @@
ca0e0d645cfd5f672d286e8977fc94d4c92579801cb4a781c495465cbc581a33

View File

@ -0,0 +1 @@
2b918f516a004d3b3f1b310ad9421a29a9675a7670f6a653ba73209f8e7f0f41

View File

@ -31164,6 +31164,8 @@ CREATE UNIQUE INDEX index_ops_strategies_user_lists_on_strategy_id_and_user_list
CREATE UNIQUE INDEX index_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name), id);
CREATE INDEX index_p_ci_runner_machine_builds_on_runner_machine_id ON ONLY p_ci_runner_machine_builds USING btree (runner_machine_id);
CREATE INDEX index_packages_build_infos_on_pipeline_id ON packages_build_infos USING btree (pipeline_id);
CREATE INDEX index_packages_build_infos_package_id_pipeline_id_id ON packages_build_infos USING btree (package_id, pipeline_id, id);
@ -33920,6 +33922,8 @@ CREATE TRIGGER ci_builds_loose_fk_trigger AFTER DELETE ON ci_builds REFERENCING
CREATE TRIGGER ci_pipelines_loose_fk_trigger AFTER DELETE ON ci_pipelines REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
CREATE TRIGGER ci_runner_machines_loose_fk_trigger AFTER DELETE ON ci_runner_machines REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
CREATE TRIGGER ci_runners_loose_fk_trigger AFTER DELETE ON ci_runners REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
CREATE TRIGGER clusters_loose_fk_trigger AFTER DELETE ON clusters REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
@ -36662,9 +36666,6 @@ ALTER TABLE ONLY merge_requests_closing_issues
ALTER TABLE ONLY banned_users
ADD CONSTRAINT fk_rails_fa5bb598e5 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE p_ci_builds_metadata
ADD CONSTRAINT fk_rails_fae01b2700 FOREIGN KEY (runner_machine_id) REFERENCES ci_runner_machines(id) ON DELETE SET NULL;
ALTER TABLE ONLY operations_feature_flags_issues
ADD CONSTRAINT fk_rails_fb4d2a7cb1 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;

View File

@ -7305,6 +7305,30 @@ The edge type for [`CiRunner`](#cirunner).
| <a id="cirunneredgenode"></a>`node` | [`CiRunner`](#cirunner) | The item at the end of the edge. |
| <a id="cirunneredgeweburl"></a>`webUrl` | [`String`](#string) | Web URL of the runner. The value depends on where you put this field in the query. You can use it for projects or groups. |
#### `CiRunnerMachineConnection`
The connection type for [`CiRunnerMachine`](#cirunnermachine).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnermachineconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
| <a id="cirunnermachineconnectionedges"></a>`edges` | [`[CiRunnerMachineEdge]`](#cirunnermachineedge) | A list of edges. |
| <a id="cirunnermachineconnectionnodes"></a>`nodes` | [`[CiRunnerMachine]`](#cirunnermachine) | A list of nodes. |
| <a id="cirunnermachineconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `CiRunnerMachineEdge`
The edge type for [`CiRunnerMachine`](#cirunnermachine).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnermachineedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="cirunnermachineedgenode"></a>`node` | [`CiRunnerMachine`](#cirunnermachine) | The item at the end of the edge. |
#### `CiSecureFileRegistryConnection`
The connection type for [`CiSecureFileRegistry`](#cisecurefileregistry).
@ -11809,6 +11833,7 @@ CI/CD variables for a project.
| <a id="cirunnerjobcount"></a>`jobCount` | [`Int`](#int) | Number of jobs processed by the runner (limited to 1000, plus one to indicate that more items exist). |
| <a id="cirunnerjobexecutionstatus"></a>`jobExecutionStatus` **{warning-solid}** | [`CiRunnerJobExecutionStatus`](#cirunnerjobexecutionstatus) | **Introduced** in 15.7. This feature is in Alpha. It can be changed or removed at any time. Job execution status of the runner. |
| <a id="cirunnerlocked"></a>`locked` | [`Boolean`](#boolean) | Indicates the runner is locked. |
| <a id="cirunnermachines"></a>`machines` | [`CiRunnerMachineConnection`](#cirunnermachineconnection) | Machines associated with the runner configuration. (see [Connections](#connections)) |
| <a id="cirunnermaintenancenote"></a>`maintenanceNote` | [`String`](#string) | Runner's maintenance notes. |
| <a id="cirunnermaintenancenotehtml"></a>`maintenanceNoteHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `maintenance_note`. |
| <a id="cirunnermaximumtimeout"></a>`maximumTimeout` | [`Int`](#int) | Maximum timeout (in seconds) for jobs processed by the runner. |
@ -11879,6 +11904,24 @@ Returns [`CiRunnerStatus!`](#cirunnerstatus).
| ---- | ---- | ----------- |
| <a id="cirunnerstatuslegacymode"></a>`legacyMode` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.0. Will be removed in 17.0. In GitLab 16.0 and later, the field will act as if `legacyMode` is null. |
### `CiRunnerMachine`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cirunnermachinearchitecturename"></a>`architectureName` | [`String`](#string) | Architecture provided by the runner machine. |
| <a id="cirunnermachinecontactedat"></a>`contactedAt` | [`Time`](#time) | Timestamp of last contact from the runner machine. |
| <a id="cirunnermachinecreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp of creation of the runner machine. |
| <a id="cirunnermachineexecutorname"></a>`executorName` | [`String`](#string) | Executor last advertised by the runner. |
| <a id="cirunnermachineid"></a>`id` | [`CiRunnerMachineID!`](#cirunnermachineid) | ID of the runner machine. |
| <a id="cirunnermachineipaddress"></a>`ipAddress` | [`String`](#string) | IP address of the runner machine. |
| <a id="cirunnermachineplatformname"></a>`platformName` | [`String`](#string) | Platform provided by the runner machine. |
| <a id="cirunnermachinerevision"></a>`revision` | [`String`](#string) | Revision of the runner. |
| <a id="cirunnermachinerunner"></a>`runner` | [`CiRunner`](#cirunner) | Runner configuration for the runner machine. |
| <a id="cirunnermachinestatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner machine. |
| <a id="cirunnermachineversion"></a>`version` | [`String`](#string) | Version of the runner. |
### `CiSecureFileRegistry`
Represents the Geo replication and verification state of a ci_secure_file.
@ -24574,6 +24617,12 @@ A `CiRunnerID` is a global ID. It is encoded as a string.
An example `CiRunnerID` is: `"gid://gitlab/Ci::Runner/1"`.
### `CiRunnerMachineID`
A `CiRunnerMachineID` is a global ID. It is encoded as a string.
An example `CiRunnerMachineID` is: `"gid://gitlab/Ci::RunnerMachine/1"`.
### `ClustersAgentID`
A `ClustersAgentID` is a global ID. It is encoded as a string.

View File

@ -306,7 +306,7 @@ listed in the descriptions of the relevant settings.
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_syntax_highlighting_theme` | integer | no | Default syntax highlighting theme for users who are not signed in. See [IDs of available themes](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/themes.rb#L16).
| `default_syntax_highlighting_theme` | integer | no | Default syntax highlighting theme for new users and users who are not signed in. See [IDs of available themes](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/themes.rb#L16).
| `delayed_project_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed project deletion by default in new groups. Default is `false`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), can only be enabled when `delayed_group_deletion` is true. |
| `delayed_group_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed group deletion. Default is `true`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352959) in GitLab 15.0. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), disables and locks the group-level setting for delayed protect deletion when set to `false`. |
| `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | The number of days to wait before deleting a project or group that is marked for deletion. Value must be between `1` and `90`. Defaults to `7`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), a hook on `deletion_adjourned_period` sets the period to `1` on every update, and sets both `delayed_project_deletion` and `delayed_group_deletion` to `false` if the period is `0`. |

View File

@ -395,9 +395,11 @@ scope.
| GitLab Runner | `%15.10` | [Modify register command to allow new flow with glrt- prefixed authentication tokens](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29613). |
| GitLab Runner | `%15.10` | Make the `gitlab-runner register` command happen in a single operation. |
| GitLab Rails app | `%15.10` | Define feature flag and policies for "New Runner creation workflow" for groups and projects. |
| GitLab Rails app | `%15.11` | Only update runner `contacted_at` and `status` when polled for jobs. |
| GitLab Rails app | `%15.10` | Only update runner `contacted_at` and `status` when polled for jobs. |
| GitLab Rails app | `%15.10` | Add GraphQL type to represent runner machines under `CiRunner`. |
| GitLab Rails app | `%15.10` | Implement UI to create new instance runner. |
| GitLab Rails app | `%15.11` | Update service and mutation to accept groups and projects. |
| GitLab Rails app | `%15.11` | Implement UI to create new runner. |
| GitLab Rails app | `%15.11` | Implement UI to create new group/project runners. |
| GitLab Rails app | `%15.11` | GraphQL changes to `CiRunner` type. (?) |
| GitLab Rails app | `%15.11` | UI changes to runner details view (listing of platform, architecture, IP address, etc.) (?) |
| GitLab Rails app | `%15.11` | Adapt `POST /api/v4/runners` REST endpoint to accept a request from an authorized user with a scope instead of a registration token. |

View File

@ -779,14 +779,18 @@ As much as possible, use text that follows one of these patterns:
For example:
- `For more information, see [merge requests](../../../user/project/merge_requests/index.md).`
- `To create a review app, see [review apps](../../../ci/review_apps/index.md).`
- `For more information, see [merge requests](LINK).`
- `To create a review app, see [review apps](LINK).`
You can expand on this text by using phrases like
`For more information about this feature, see...`
Do not to use alternate phrases, like `Learn more about...` or
`To read more...`.
Do not to use the following constructions:
- `Learn more about...`
- `To read more...`.
- `For more information, see the [Merge requests](LINK) page.`
- `For more information, see the [Merge requests](LINK) documentation.`
#### Descriptive text rather than `here`

View File

@ -944,6 +944,17 @@ An Owner is the highest role a user can have.
Use title case for the GitLab Package Registry.
## page
If you write a phrase like, "On the **Issues** page," ensure steps for how to get to the page are nearby. Otherwise, people might not know what the **Issues** page is.
The page name should be visible in the UI at the top of the page.
If it is not, you should be able to get the name from the breadcrumb.
The docs should match the case in the UI, and the page name should be bold. For example:
- On the **Test cases** page, ...
## permissions
Do not use [**roles**](#roles) and **permissions** interchangeably. Each user is assigned a role. Each role includes a set of permissions.

View File

@ -63,6 +63,8 @@ Dark theme only works with the **Dark** syntax highlighting theme.
## Syntax highlighting theme
> Changing the default syntax highlighting theme for new users and users who are not signed in [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/25129) in GitLab 15.10.
GitLab uses the [rouge Ruby library](http://rouge.jneen.net/ "Rouge website")
for syntax highlighting outside of any Editor context. The WebIDE (like Snippets)
uses [Monaco Editor](https://microsoft.github.io/monaco-editor/) and it's provided
@ -89,6 +91,10 @@ The default syntax theme is White, and you can choose among 5 different themes:
Introduced in GitLab 13.6, the themes [Solarized](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) and [Monokai](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) also apply to the [Web IDE](../project/web_ide/index.md) and [Snippets](../snippets.md).
You can use an API call to change the default syntax highlighting theme for new users and users
who are not signed in. For more information, see the `default_syntax_highlighting_theme`
in the [list of settings that can be accessed through API calls](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
## Diff colors
A diff compares the old/removed content with the new/added content (for example, when

View File

@ -43,11 +43,14 @@ module Gitlab
def optimize!
return unless Feature.enabled?(:optimize_batched_migrations, type: :ops)
if multiplier = batch_size_multiplier
max_batch = migration.max_batch_size || MAX_BATCH_SIZE
migration.batch_size = (migration.batch_size * multiplier).to_i.clamp(MIN_BATCH_SIZE, max_batch)
migration.save!
end
multiplier = batch_size_multiplier
return if multiplier.nil?
max_batch = migration.max_batch_size || MAX_BATCH_SIZE
min_batch = [max_batch, MIN_BATCH_SIZE].min
migration.batch_size = (migration.batch_size * multiplier).to_i.clamp(min_batch, max_batch)
migration.save!
end
private

View File

@ -4735,12 +4735,105 @@ msgstr ""
msgid "Analytics"
msgstr ""
msgid "Analytics|Add to Dashboard"
msgstr ""
msgid "Analytics|An error occurred while loading the %{visualizationTitle} visualization."
msgstr ""
msgid "Analytics|Analytics dashboards"
msgstr ""
msgid "Analytics|Browser"
msgstr ""
msgid "Analytics|Browser Family"
msgstr ""
msgid "Analytics|Choose a chart type on the right"
msgstr ""
msgid "Analytics|Choose a measurement to start"
msgstr ""
msgid "Analytics|Code"
msgstr ""
msgid "Analytics|Configure Dashboard Project"
msgstr ""
msgid "Analytics|Custom dashboards"
msgstr ""
msgid "Analytics|Dashboards are created by editing the projects dashboard files."
msgstr ""
msgid "Analytics|Data"
msgstr ""
msgid "Analytics|Data Table"
msgstr ""
msgid "Analytics|For being able to create your own dashboards please configure a special project to store your dashboards."
msgstr ""
msgid "Analytics|Host"
msgstr ""
msgid "Analytics|Language"
msgstr ""
msgid "Analytics|Line Chart"
msgstr ""
msgid "Analytics|New Analytics Visualization Title"
msgstr ""
msgid "Analytics|OS"
msgstr ""
msgid "Analytics|OS Version"
msgstr ""
msgid "Analytics|Page Language"
msgstr ""
msgid "Analytics|Page Path"
msgstr ""
msgid "Analytics|Page Title"
msgstr ""
msgid "Analytics|Pages"
msgstr ""
msgid "Analytics|Referer"
msgstr ""
msgid "Analytics|Resulting Data"
msgstr ""
msgid "Analytics|Single Statistic"
msgstr ""
msgid "Analytics|URL"
msgstr ""
msgid "Analytics|Users"
msgstr ""
msgid "Analytics|Viewport"
msgstr ""
msgid "Analytics|Visualization"
msgstr ""
msgid "Analytics|Visualization Designer"
msgstr ""
msgid "Analytics|Visualization Type"
msgstr ""
msgid "Analyze your dependencies for known vulnerabilities."
msgstr ""
@ -32639,22 +32732,19 @@ msgstr ""
msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window:"
msgstr ""
msgid "ProductAnalytics|Add to Dashboard"
msgid "ProductAnalytics|All Clicks Compared"
msgstr ""
msgid "ProductAnalytics|All clicks compared"
msgid "ProductAnalytics|All Events Compared"
msgstr ""
msgid "ProductAnalytics|All events compared"
msgid "ProductAnalytics|All Features"
msgstr ""
msgid "ProductAnalytics|All features"
msgid "ProductAnalytics|All Pages"
msgstr ""
msgid "ProductAnalytics|All pages"
msgstr ""
msgid "ProductAnalytics|All sessions compared"
msgid "ProductAnalytics|All Sessions Compared"
msgstr ""
msgid "ProductAnalytics|An error occured while loading the %{panelTitle} panel."
@ -32672,33 +32762,21 @@ msgstr ""
msgid "ProductAnalytics|Audience"
msgstr ""
msgid "ProductAnalytics|Average Per User"
msgstr ""
msgid "ProductAnalytics|Average Session Duration"
msgstr ""
msgid "ProductAnalytics|Average duration in minutes"
msgstr ""
msgid "ProductAnalytics|Average per User"
msgstr ""
msgid "ProductAnalytics|Back to dashboards"
msgstr ""
msgid "ProductAnalytics|Browser"
msgstr ""
msgid "ProductAnalytics|Browser Family"
msgstr ""
msgid "ProductAnalytics|Cancel Edit"
msgstr ""
msgid "ProductAnalytics|Choose a chart type on the right"
msgstr ""
msgid "ProductAnalytics|Choose a measurement to start"
msgstr ""
msgid "ProductAnalytics|Click Events"
msgstr ""
@ -32717,24 +32795,12 @@ msgstr ""
msgid "ProductAnalytics|Compares feature usage of all features against each other"
msgstr ""
msgid "ProductAnalytics|Compares pageviews of all pages against each other"
msgstr ""
msgid "ProductAnalytics|Configure Dashboard Project"
msgid "ProductAnalytics|Compares page views of all pages against each other"
msgstr ""
msgid "ProductAnalytics|Creating your product analytics instance..."
msgstr ""
msgid "ProductAnalytics|Custom dashboards"
msgstr ""
msgid "ProductAnalytics|Data"
msgstr ""
msgid "ProductAnalytics|Data Table"
msgstr ""
msgid "ProductAnalytics|Details on how to configure product analytics to collect data."
msgstr ""
@ -32756,13 +32822,7 @@ msgstr ""
msgid "ProductAnalytics|Events over time"
msgstr ""
msgid "ProductAnalytics|Feature Usage"
msgstr ""
msgid "ProductAnalytics|Feature usage"
msgstr ""
msgid "ProductAnalytics|For being able to create your own dashboards please configure a special project to store your dashboards."
msgid "ProductAnalytics|Feature Usages"
msgstr ""
msgid "ProductAnalytics|For the product analytics dashboard to start showing you some data, you need to add the analytics tracking code to your project."
@ -32771,13 +32831,10 @@ msgstr ""
msgid "ProductAnalytics|Go back"
msgstr ""
msgid "ProductAnalytics|Host"
msgstr ""
msgid "ProductAnalytics|How many sessions a user has"
msgstr ""
msgid "ProductAnalytics|How often sesions are repeated"
msgid "ProductAnalytics|How often sessions are repeated"
msgstr ""
msgid "ProductAnalytics|Identifies the sender of tracking events"
@ -32792,12 +32849,6 @@ msgstr ""
msgid "ProductAnalytics|Instrumentation details"
msgstr ""
msgid "ProductAnalytics|Language"
msgstr ""
msgid "ProductAnalytics|Line Chart"
msgstr ""
msgid "ProductAnalytics|Measure All tracked Events"
msgstr ""
@ -32813,43 +32864,13 @@ msgstr ""
msgid "ProductAnalytics|Measuring"
msgstr ""
msgid "ProductAnalytics|New Analytics Panel Title"
msgstr ""
msgid "ProductAnalytics|OS"
msgstr ""
msgid "ProductAnalytics|OS Version"
msgstr ""
msgid "ProductAnalytics|On what do you want to get insights?"
msgstr ""
msgid "ProductAnalytics|Page Language"
msgstr ""
msgid "ProductAnalytics|Page Path"
msgstr ""
msgid "ProductAnalytics|Page Title"
msgstr ""
msgid "ProductAnalytics|Page Views"
msgstr ""
msgid "ProductAnalytics|Pages"
msgstr ""
msgid "ProductAnalytics|Panel"
msgstr ""
msgid "ProductAnalytics|Referer"
msgstr ""
msgid "ProductAnalytics|Repeat Visit percentage"
msgstr ""
msgid "ProductAnalytics|Resulting Data"
msgid "ProductAnalytics|Repeat Visit Percentage"
msgstr ""
msgid "ProductAnalytics|SDK App ID"
@ -32867,9 +32888,6 @@ msgstr ""
msgid "ProductAnalytics|Set up product analytics"
msgstr ""
msgid "ProductAnalytics|Single Statistic"
msgstr ""
msgid "ProductAnalytics|Steps to add product analytics as a CommonJS module"
msgstr ""
@ -32894,9 +32912,6 @@ msgstr ""
msgid "ProductAnalytics|Track specific features"
msgstr ""
msgid "ProductAnalytics|URL"
msgstr ""
msgid "ProductAnalytics|Unique Users"
msgstr ""
@ -32909,15 +32924,6 @@ msgstr ""
msgid "ProductAnalytics|Users"
msgstr ""
msgid "ProductAnalytics|Viewport"
msgstr ""
msgid "ProductAnalytics|Visualization Designer"
msgstr ""
msgid "ProductAnalytics|Visualization Type"
msgstr ""
msgid "ProductAnalytics|What do you want to measure?"
msgstr ""
@ -46107,6 +46113,15 @@ msgstr ""
msgid "Unlimited"
msgstr ""
msgid "UnlimitedMembersDuringTrialAlert|During your trial, invite as many members as you like to %{group} to collaborate with you."
msgstr ""
msgid "UnlimitedMembersDuringTrialAlert|Explore paid plans"
msgstr ""
msgid "UnlimitedMembersDuringTrialAlert|Get the most out of your trial with space for more members"
msgstr ""
msgid "Unlink"
msgstr ""
@ -48700,6 +48715,11 @@ msgstr ""
msgid "When you transfer your project to a group, you can easily manage multiple projects, view usage quotas for storage, pipeline minutes, and users, and start a trial or upgrade to a paid tier."
msgstr ""
msgid "When your trial ends, you'll have a maximum of %d member on the Free tier, or you can get more by upgrading to a paid tier."
msgid_plural "When your trial ends, you'll have a maximum of %d members on the Free tier, or you can get more by upgrading to a paid tier."
msgstr[0] ""
msgstr[1] ""
msgid "When your trial ends, you'll move to the Free tier, which has a limit of %{free_user_limit} seat. %{free_user_limit} seat will remain active, and members not occupying a seat will have the %{link_start}Over limit status%{link_end} and lose access to this group."
msgid_plural "When your trial ends, you'll move to the Free tier, which has a limit of %{free_user_limit} seats. %{free_user_limit} seats will remain active, and members not occupying a seat will have the %{link_start}Over limit status%{link_end} and lose access to this group."
msgstr[0] ""
@ -51011,6 +51031,9 @@ msgstr ""
msgid "ciReport|Solution"
msgstr ""
msgid "ciReport|Something went wrong while fetching the finding. Please try again later."
msgstr ""
msgid "ciReport|Static Application Security Testing (SAST)"
msgstr ""

View File

@ -110,15 +110,12 @@ def add_definition_to_yaml(definition)
content = YAML.load_file(Rails.root.join('config/gitlab_loose_foreign_keys.yml'))
table_definitions = content[definition.from_table]
# insert new entry at random place to avoid conflicts
# insert new entry in alphabetic order
unless table_definitions
table_definitions = []
insert_idx = rand(content.count+1)
# insert at a given index in ordered hash
content = content.to_a
content.insert(insert_idx, [definition.from_table, table_definitions])
content = content.to_h
content[definition.from_table] = table_definitions
content = content.sort.to_h
end
on_delete =
@ -217,7 +214,7 @@ def add_test_to_specs(definition)
puts "Adding test to #{spec_path}..."
spec_test = <<-EOF.strip_heredoc.indent(2)
context 'loose foreign key on #{definition.from_table}.#{definition.column}' do
context 'with loose foreign key on #{definition.from_table}.#{definition.column}' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:#{definition.to_table.singularize}) }
let!(:model) { create(:#{definition.from_table.singularize}, #{definition.column.delete_suffix("_id").singularize}: parent) }

View File

@ -40,7 +40,7 @@ RSpec.describe 'Database schema', feature_category: :database do
ci_build_trace_metadata: %w[partition_id build_id],
ci_builds: %w[erased_by_id trigger_request_id partition_id],
ci_builds_runner_session: %w[partition_id build_id],
p_ci_builds_metadata: %w[partition_id build_id],
p_ci_builds_metadata: %w[partition_id build_id runner_machine_id],
ci_job_artifacts: %w[partition_id job_id],
ci_job_variables: %w[partition_id job_id],
ci_namespace_monthly_usages: %w[namespace_id],
@ -89,7 +89,7 @@ RSpec.describe 'Database schema', feature_category: :database do
oauth_access_grants: %w[resource_owner_id application_id],
oauth_access_tokens: %w[resource_owner_id application_id],
oauth_applications: %w[owner_id],
p_ci_runner_machine_builds: %w[partition_id build_id runner_machine_id],
p_ci_runner_machine_builds: %w[partition_id build_id],
product_analytics_events_experimental: %w[event_id txn_id user_id],
project_build_artifacts_size_refreshes: %w[last_job_artifact_id],
project_data_transfers: %w[project_id namespace_id],

View File

@ -66,6 +66,12 @@ FactoryBot.define do
end
end
trait :with_runner_machine do
after(:build) do |runner, evaluator|
runner.runner_machines << build(:ci_runner_machine, runner: runner)
end
end
trait :inactive do
active { false }
end

View File

@ -9,7 +9,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { s__ } from '~/locale';
import { updateHistory } from '~/lib/utils/url_utility';
import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
@ -21,7 +20,6 @@ import { runnerForRegistration } from '../mock_data';
const mockRunner = runnerForRegistration.data.runner;
const mockRunnerId = `${getIdFromGraphQLId(mockRunner.id)}`;
const mockRunnersPath = '/admin/runners';
const MOCK_TOKEN = 'MOCK_TOKEN';
Vue.use(VueApollo);
@ -51,7 +49,7 @@ describe('AdminRegisterRunnerApp', () => {
beforeEach(() => {
mockRunnerQuery = jest.fn().mockResolvedValue({
data: {
runner: { ...mockRunner, ephemeralAuthenticationToken: MOCK_TOKEN },
runner: mockRunner,
},
});
});
@ -66,15 +64,11 @@ describe('AdminRegisterRunnerApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunner.id });
});
it('shows heading', () => {
expect(wrapper.find('h1').text()).toContain(mockRunner.description);
});
it('shows registration instructions', () => {
expect(findRegistrationInstructions().props()).toEqual({
loading: false,
platform: DEFAULT_PLATFORM,
token: MOCK_TOKEN,
runner: mockRunner,
});
});
@ -114,21 +108,21 @@ describe('AdminRegisterRunnerApp', () => {
});
it('opens platform drawer', () => {
expect(findPlatformsDrawer().props('open')).toEqual(true);
expect(findPlatformsDrawer().props('open')).toBe(true);
});
it('closes platform drawer', async () => {
findRegistrationInstructions().vm.$emit('toggleDrawer');
await nextTick();
expect(findPlatformsDrawer().props('open')).toEqual(false);
expect(findPlatformsDrawer().props('open')).toBe(false);
});
it('closes platform drawer from drawer', async () => {
findPlatformsDrawer().vm.$emit('close');
await nextTick();
expect(findPlatformsDrawer().props('open')).toEqual(false);
expect(findPlatformsDrawer().props('open')).toBe(false);
});
describe('when selecting a platform', () => {
@ -151,18 +145,14 @@ describe('AdminRegisterRunnerApp', () => {
});
describe('When runner is loading', () => {
beforeEach(async () => {
beforeEach(() => {
createComponent();
});
it('shows heading', () => {
expect(wrapper.find('h1').text()).toBe(s__('Runners|Register runner'));
});
it('shows registration instructions', () => {
expect(findRegistrationInstructions().props()).toEqual({
loading: true,
token: null,
runner: null,
platform: DEFAULT_PLATFORM,
});
});

View File

@ -76,6 +76,7 @@ Array [
"gitlab-runner register",
" --url http://test.host",
" --registration-token REGISTRATION_TOKEN",
" --description 'RUNNER'",
]
`;
@ -130,6 +131,7 @@ Array [
"gitlab-runner register",
" --url http://test.host",
" --registration-token REGISTRATION_TOKEN",
" --description 'RUNNER'",
]
`;
@ -188,6 +190,7 @@ Array [
".\\\\gitlab-runner.exe register",
" --url http://test.host",
" --registration-token REGISTRATION_TOKEN",
" --description 'RUNNER'",
]
`;

View File

@ -2,6 +2,7 @@ import { GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { extendedWrapper, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { s__ } from '~/locale';
import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
@ -10,24 +11,32 @@ import {
EXECUTORS_HELP_URL,
SERVICE_COMMANDS_HELP_URL,
} from '~/ci/runner/constants';
import { runnerForRegistration } from '../../mock_data';
const REGISTRATION_TOKEN = 'REGISTRATION_TOKEN';
const DUMMY_GON = {
gitlab_url: TEST_HOST,
};
const AUTH_TOKEN = 'AUTH_TOKEN';
const mockRunner = {
...runnerForRegistration.data.runner,
ephemeralAuthenticationToken: AUTH_TOKEN,
};
describe('RegistrationInstructions', () => {
let wrapper;
let originalGon;
const findHeading = () => wrapper.find('h1');
const findStepAt = (i) => extendedWrapper(wrapper.findAll('section').at(i));
const findByText = (text, container = wrapper) => container.findByText(text);
const createComponent = (props) => {
wrapper = shallowMountExtended(RegistrationInstructions, {
propsData: {
runner: mockRunner,
platform: DEFAULT_PLATFORM,
token: REGISTRATION_TOKEN,
...props,
},
stubs: {
@ -45,29 +54,60 @@ describe('RegistrationInstructions', () => {
window.gon = originalGon;
});
beforeEach(() => {
createComponent();
describe('renders heading', () => {
it('when runner is loaded, shows heading', () => {
createComponent();
expect(findHeading().text()).toContain(mockRunner.description);
});
it('when runner is loaded, shows heading safely', () => {
const description = '<script>hacked();</script>';
createComponent({
runner: {
...mockRunner,
description,
},
});
expect(findHeading().text()).toBe('Register "<script>hacked();</script>" runner');
expect(findHeading().element.innerHTML).toBe(
'Register "&lt;script&gt;hacked();&lt;/script&gt;" runner',
);
});
it('when runner is loading, shows default heading', () => {
createComponent({
loading: true,
runner: null,
});
expect(findHeading().text()).toBe(s__('Runners|Register runner'));
});
});
it('renders legacy instructions', () => {
createComponent();
findByText('How do I install GitLab Runner?').vm.$emit('click');
expect(wrapper.emitted('toggleDrawer')).toHaveLength(1);
});
it('renders step 1', () => {
createComponent();
const step1 = findStepAt(0);
expect(step1.findComponent(CliCommand).props()).toEqual({
command: [
'gitlab-runner register',
` --url ${TEST_HOST}`,
` --registration-token ${REGISTRATION_TOKEN}`,
` --registration-token ${AUTH_TOKEN}`,
` --description '${mockRunner.description}'`,
],
prompt: '$',
});
expect(step1.find('code').text()).toBe(REGISTRATION_TOKEN);
expect(step1.findComponent(ClipboardButton).props('text')).toBe(REGISTRATION_TOKEN);
expect(step1.find('code').text()).toBe(AUTH_TOKEN);
expect(step1.findComponent(ClipboardButton).props('text')).toBe(AUTH_TOKEN);
});
it('renders step 1 in loading state', () => {
@ -83,6 +123,7 @@ describe('RegistrationInstructions', () => {
});
it('renders step 2', () => {
createComponent();
const step2 = findStepAt(1);
expect(findByText('Not sure which one to select?', step2).attributes('href')).toBe(
@ -91,6 +132,7 @@ describe('RegistrationInstructions', () => {
});
it('renders step 3', () => {
createComponent();
const step3 = findStepAt(2);
expect(step3.findComponent(CliCommand).props()).toEqual({

View File

@ -15,6 +15,7 @@ import {
} from '~/ci/runner/components/registration/utils';
const REGISTRATION_TOKEN = 'REGISTRATION_TOKEN';
const DESCRIPTION = 'RUNNER';
const DUMMY_GON = {
gitlab_url: TEST_HOST,
};
@ -37,19 +38,45 @@ describe('registration utils', () => {
it('commandPrompt is correct', () => {
expect(commandPrompt({ platform })).toMatchSnapshot();
});
it('registerCommand is correct', () => {
expect(
registerCommand({ platform, registrationToken: REGISTRATION_TOKEN }),
registerCommand({
platform,
registrationToken: REGISTRATION_TOKEN,
description: DESCRIPTION,
}),
).toMatchSnapshot();
expect(registerCommand({ platform })).toMatchSnapshot();
});
it('runCommand is correct', () => {
expect(runCommand({ platform })).toMatchSnapshot();
});
},
);
describe.each([LINUX_PLATFORM, MACOS_PLATFORM])('for "%s" platform', (platform) => {
it.each`
description | parameter
${'my runner'} | ${"'my runner'"}
${"bob's runner"} | ${"'bob'\\''s runner'"}
`('registerCommand escapes description `$description`', ({ description, parameter }) => {
expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`);
});
});
describe.each([WINDOWS_PLATFORM])('for "%s" platform', (platform) => {
it.each`
description | parameter
${'my runner'} | ${"'my runner'"}
${"bob's runner"} | ${"'bob''s runner'"}
`('registerCommand escapes description `$description`', ({ description, parameter }) => {
expect(registerCommand({ platform, description })[2]).toBe(` --description ${parameter}`);
});
});
describe('for missing platform', () => {
it('commandPrompt uses the default', () => {
const expected = commandPrompt({ platform: DEFAULT_PLATFORM });

View File

@ -84,5 +84,16 @@ describe('locale', () => {
expect(output).toBe('contains duplicated 15%');
});
});
describe('ignores special replacements in the input', () => {
it.each(['$$', '$&', '$`', `$'`])('replacement "%s" is ignored', (replacement) => {
const input = 'My odd %{replacement} is preserved';
const parameters = { replacement };
const output = sprintf(input, parameters, false);
expect(output).toBe(`My odd ${replacement} is preserved`);
});
});
});
});

View File

@ -9,10 +9,13 @@ import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { stubComponent } from 'helpers/stub_component';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
jest.mock('~/emoji');
describe('vue_shared/component/markdown/markdown_editor', () => {
useLocalStorageSpy();
let wrapper;
const value = 'test markdown';
const renderMarkdownPath = '/api/markdown';
@ -64,6 +67,8 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
afterEach(() => {
mock.restore();
localStorage.clear();
});
it('displays markdown field by default', () => {
@ -102,6 +107,42 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
describe('autosave', () => {
it('automatically saves the textarea value to local storage if autosaveKey is defined', () => {
buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: 'This is **markdown**' } });
expect(localStorage.getItem('autosave/issue/1234')).toBe('This is **markdown**');
});
it("loads value from local storage if autosaveKey is defined, and value isn't", () => {
localStorage.setItem('autosave/issue/1234', 'This is **markdown**');
buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } });
expect(findTextarea().element.value).toBe('This is **markdown**');
});
it("doesn't load value from local storage if autosaveKey is defined, and value is", () => {
localStorage.setItem('autosave/issue/1234', 'This is **markdown**');
buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
expect(findTextarea().element.value).toBe('test markdown');
});
it('does not save the textarea value to local storage if autosaveKey is not defined', () => {
buildWrapper({ propsData: { value: 'This is **markdown**' } });
expect(localStorage.setItem).not.toHaveBeenCalled();
});
it('does not save the textarea value to local storage if value is empty', () => {
buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } });
expect(localStorage.setItem).not.toHaveBeenCalled();
});
});
it('renders markdown field textarea', () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
@ -158,6 +199,16 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('input')).toEqual([[newValue]]);
});
it('autosaves the markdown value to local storage', async () => {
buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
const newValue = 'new value';
await findTextarea().setValue(newValue);
expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue);
});
describe('when autofocus is true', () => {
beforeEach(async () => {
buildWrapper({ attachTo: document.body, propsData: { autofocus: true } });
@ -219,7 +270,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
beforeEach(() => {
buildWrapper();
buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
findMarkdownField().vm.$emit('enableContentEditor');
});
@ -244,6 +295,14 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('input')).toEqual([[newValue]]);
});
it('autosaves the content editor value to local storage', async () => {
const newValue = 'new value';
await findContentEditor().vm.$emit('change', { markdown: newValue });
expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue);
});
it('bubbles up keydown event', () => {
const event = new Event('keydown');

View File

@ -1,5 +1,4 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Toggle Button', () => {
@ -16,7 +15,7 @@ describe('Toggle Button', () => {
remain,
};
const Component = Vue.extend({
const Component = {
components: {
SmartVirtualScrollList,
},
@ -26,7 +25,7 @@ describe('Toggle Button', () => {
<smart-virtual-scroll-list v-bind="$options.smartListProperties">
<li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li>
</smart-virtual-scroll-list>`,
});
};
return mount(Component).vm;
};

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['CiRunnerMachine'], feature_category: :runner_fleet do
specify { expect(described_class.graphql_name).to eq('CiRunnerMachine') }
specify { expect(described_class).to require_graphql_authorizations(:read_runner_machine) }
it 'contains attributes related to a runner machine' do
expected_fields = %w[
architecture_name contacted_at created_at executor_name id ip_address platform_name revision
runner status version
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end

View File

@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['CiRunner'], feature_category: :runner do
it 'contains attributes related to a runner' do
expected_fields = %w[
id description created_by created_at contacted_at maximum_timeout access_level active paused status
id description created_by created_at contacted_at machines maximum_timeout access_level active paused status
version short_sha revision locked run_untagged ip_address runner_type tag_list
project_count job_count admin_url edit_admin_url register_admin_url user_permissions executor_name
architecture_name platform_name maintenance_note maintenance_note_html groups projects jobs token_expires_at

View File

@ -113,5 +113,14 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do
expect { subject }.to change { migration.reload.batch_size }.to(1_000)
end
end
context 'when migration max_batch_size is less than MIN_BATCH_SIZE' do
let(:migration_params) { { max_batch_size: 900 } }
it 'does not raise an error' do
mock_efficiency(0.7)
expect { subject }.not_to raise_error
end
end
end
end

View File

@ -872,6 +872,26 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
expect(pipeline).to be_valid
end
end
context 'when source is unknown' do
subject(:pipeline) { create(:ci_empty_pipeline, :created) }
let(:attr) { :source }
let(:attr_value) { :unknown }
it_behaves_like 'having enum with nil value'
end
end
describe '#config_source' do
context 'when source is unknown' do
subject(:pipeline) { create(:ci_empty_pipeline, :created) }
let(:attr) { :config_source }
let(:attr_value) { :unknown_source }
it_behaves_like 'having enum with nil value'
end
end
describe '#block' do

View File

@ -47,4 +47,11 @@ RSpec.describe Ci::RunnerMachineBuild, model: true, feature_category: :runner_fl
it { expect(partitioning_strategy.current_partitions).to include partitioning_strategy.initial_partition }
it { expect(partitioning_strategy.active_partition).to be_present }
end
context 'loose foreign key on p_ci_runner_machine_builds.runner_machine_id' do # rubocop:disable RSpec/ContextWording
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:ci_runner_machine) }
let!(:model) { create(:ci_runner_machine_build, runner_machine: parent) }
end
end
end

View File

@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model do
it_behaves_like 'having unique enum values'
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :ci_runner_machine }
end
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:runner_version).with_foreign_key(:version) }
it { is_expected.to have_many(:runner_machine_builds) }
@ -53,6 +57,64 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
end
end
describe '.online_contact_time_deadline', :freeze_time do
subject { described_class.online_contact_time_deadline }
it { is_expected.to eq(2.hours.ago) }
end
describe '.stale_deadline', :freeze_time do
subject { described_class.stale_deadline }
it { is_expected.to eq(7.days.ago) }
end
describe '#status', :freeze_time do
let(:runner_machine) { build(:ci_runner_machine, created_at: 8.days.ago) }
subject { runner_machine.status }
context 'if never connected' do
before do
runner_machine.contacted_at = nil
end
it { is_expected.to eq(:stale) }
context 'if created recently' do
before do
runner_machine.created_at = 1.day.ago
end
it { is_expected.to eq(:never_contacted) }
end
end
context 'if contacted 1s ago' do
before do
runner_machine.contacted_at = 1.second.ago
end
it { is_expected.to eq(:online) }
end
context 'if contacted recently' do
before do
runner_machine.contacted_at = 2.hours.ago
end
it { is_expected.to eq(:offline) }
end
context 'if contacted long time ago' do
before do
runner_machine.contacted_at = 7.days.ago
end
it { is_expected.to eq(:stale) }
end
end
describe '#heartbeat', :freeze_time do
let(:runner_machine) { create(:ci_runner_machine, version: '15.0.0') }
let(:executor) { 'shell' }

View File

@ -541,9 +541,9 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
describe '.stale', :freeze_time do
subject { described_class.stale }
let!(:runner1) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago + 10.seconds) }
let!(:runner2) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago - 1.second) }
let!(:runner3) { create(:ci_runner, :instance, created_at: 3.months.ago - 1.second, contacted_at: nil) }
let!(:runner1) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago + 1.second) }
let!(:runner2) { create(:ci_runner, :instance, created_at: 4.months.ago, contacted_at: 3.months.ago) }
let!(:runner3) { create(:ci_runner, :instance, created_at: 3.months.ago, contacted_at: nil) }
let!(:runner4) { create(:ci_runner, :instance, created_at: 2.months.ago, contacted_at: nil) }
it 'returns stale runners' do
@ -551,7 +551,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
describe '#stale?', :clean_gitlab_redis_cache do
describe '#stale?', :clean_gitlab_redis_cache, :freeze_time do
let(:runner) { build(:ci_runner, :instance) }
subject { runner.stale? }
@ -570,11 +570,11 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
using RSpec::Parameterized::TableSyntax
where(:created_at, :contacted_at, :expected_stale?) do
nil | nil | false
3.months.ago - 1.second | 3.months.ago - 0.001.seconds | true
3.months.ago - 1.second | 3.months.ago + 1.hour | false
3.months.ago - 1.second | nil | true
3.months.ago + 1.hour | nil | false
nil | nil | false
3.months.ago | 3.months.ago | true
3.months.ago | (3.months - 1.hour).ago | false
3.months.ago | nil | true
(3.months - 1.hour).ago | nil | false
end
with_them do
@ -588,9 +588,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
runner.contacted_at = contacted_at
end
specify do
is_expected.to eq(expected_stale?)
end
it { is_expected.to eq(expected_stale?) }
end
context 'with cache value' do
@ -599,9 +597,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
stub_redis_runner_contacted_at(contacted_at.to_s)
end
specify do
is_expected.to eq(expected_stale?)
end
it { is_expected.to eq(expected_stale?) }
end
def stub_redis_runner_contacted_at(value)
@ -617,7 +613,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
describe '.online' do
describe '.online', :freeze_time do
subject { described_class.online }
let!(:runner1) { create(:ci_runner, :instance, contacted_at: 2.hours.ago) }
@ -626,7 +622,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
it { is_expected.to match_array([runner2]) }
end
describe '#online?', :clean_gitlab_redis_cache do
describe '#online?', :clean_gitlab_redis_cache, :freeze_time do
let(:runner) { build(:ci_runner, :instance) }
subject { runner.online? }
@ -891,8 +887,8 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
describe '#status' do
let(:runner) { build(:ci_runner, :instance, created_at: 4.months.ago) }
describe '#status', :freeze_time do
let(:runner) { build(:ci_runner, :instance, created_at: 3.months.ago) }
let(:legacy_mode) {}
subject { runner.status(legacy_mode) }
@ -948,7 +944,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted recently' do
before do
runner.contacted_at = (3.months - 1.hour).ago
runner.contacted_at = (3.months - 1.second).ago
end
it { is_expected.to eq(:offline) }
@ -956,7 +952,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted long time ago' do
before do
runner.contacted_at = (3.months + 1.second).ago
runner.contacted_at = 3.months.ago
end
context 'with legacy_mode enabled' do
@ -971,7 +967,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
describe '#deprecated_rest_status' do
describe '#deprecated_rest_status', :freeze_time do
let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) }
subject { runner.deprecated_rest_status }
@ -994,8 +990,8 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
context 'contacted long time ago' do
before do
runner.created_at = 1.year.ago
runner.contacted_at = 1.year.ago
runner.created_at = 3.months.ago
runner.contacted_at = 3.months.ago
end
it { is_expected.to eq(:stale) }

View File

@ -39,4 +39,15 @@ RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do
describe 'validation' do
it { is_expected.to validate_length_of(:version).is_at_most(2048) }
end
describe '#status' do
context 'when is not processed' do
subject(:ci_runner_version) { create(:ci_runner_version, version: 'abc124', status: :not_processed) }
let(:attr) { :status }
let(:attr_value) { :not_processed }
it_behaves_like 'having enum with nil value'
end
end
end

View File

@ -930,4 +930,13 @@ RSpec.describe Clusters::Platforms::Kubernetes do
end
end
end
describe '#authorization_type' do
subject(:kubernetes) { create(:cluster_platform_kubernetes) }
let(:attr) { :authorization_type }
let(:attr_value) { :unknown_authorization }
it_behaves_like 'having enum with nil value'
end
end

View File

@ -1039,4 +1039,13 @@ RSpec.describe CommitStatus do
end
end
end
describe '#failure_reason' do
subject(:status) { commit_status }
let(:attr) { :failure_reason }
let(:attr_value) { :unknown_failure }
it_behaves_like 'having enum with nil value'
end
end

View File

@ -2402,14 +2402,6 @@ RSpec.describe User, feature_category: :user_profile do
end
it_behaves_like 'manageable groups examples'
context 'when feature flag :linear_user_manageable_groups is disabled' do
before do
stub_feature_flags(linear_user_manageable_groups: false)
end
it_behaves_like 'manageable groups examples'
end
end
end
end

View File

@ -0,0 +1,176 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::RunnerMachinePolicy, feature_category: :runner_fleet do
let_it_be(:owner) { create(:user) }
describe 'ability :read_runner_machine' do
let_it_be(:guest) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be_with_reload(:group) { create(:group, name: 'top-level', path: 'top-level') }
let_it_be_with_reload(:subgroup) { create(:group, name: 'subgroup', path: 'subgroup', parent: group) }
let_it_be_with_reload(:project) { create(:project, group: subgroup) }
let_it_be(:instance_runner) { create(:ci_runner, :instance, :with_runner_machine) }
let_it_be(:group_runner) { create(:ci_runner, :group, :with_runner_machine, groups: [group]) }
let_it_be(:project_runner) { create(:ci_runner, :project, :with_runner_machine, projects: [project]) }
let(:runner_machine) { runner.runner_machines.first }
subject(:policy) { described_class.new(user, runner_machine) }
before_all do
group.add_guest(guest)
group.add_developer(developer)
group.add_maintainer(maintainer)
group.add_owner(owner)
end
shared_examples 'a policy allowing reading instance runner machine depending on runner sharing' do
context 'with instance runner' do
let(:runner) { instance_runner }
it { expect_allowed :read_runner_machine }
context 'with shared runners disabled on projects' do
before do
project.update!(shared_runners_enabled: false)
end
it { expect_allowed :read_runner_machine }
end
context 'with shared runners disabled for groups and projects' do
before do
group.update!(shared_runners_enabled: false)
project.update!(shared_runners_enabled: false)
end
it { expect_disallowed :read_runner_machine }
end
end
end
shared_examples 'a policy allowing reading group runner machine depending on runner sharing' do
context 'with group runner' do
let(:runner) { group_runner }
it { expect_allowed :read_runner_machine }
context 'with sharing of group runners disabled' do
before do
project.update!(group_runners_enabled: false)
end
it { expect_disallowed :read_runner_machine }
end
end
end
shared_examples 'does not allow reading runners machines on any scope' do
context 'with instance runner' do
let(:runner) { instance_runner }
it { expect_disallowed :read_runner_machine }
context 'with shared runners disabled for groups and projects' do
before do
group.update!(shared_runners_enabled: false)
project.update!(shared_runners_enabled: false)
end
it { expect_disallowed :read_runner_machine }
end
end
context 'with group runner' do
let(:runner) { group_runner }
it { expect_disallowed :read_runner_machine }
context 'with sharing of group runners disabled' do
before do
project.update!(group_runners_enabled: false)
end
it { expect_disallowed :read_runner_machine }
end
end
context 'with project runner' do
let(:runner) { project_runner }
it { expect_disallowed :read_runner_machine }
end
end
context 'without access' do
let_it_be(:user) { create(:user) }
it_behaves_like 'does not allow reading runners machines on any scope'
end
context 'with guest access' do
let(:user) { guest }
it_behaves_like 'does not allow reading runners machines on any scope'
end
context 'with developer access' do
let(:user) { developer }
it_behaves_like 'a policy allowing reading instance runner machine depending on runner sharing'
it_behaves_like 'a policy allowing reading group runner machine depending on runner sharing'
context 'with project runner' do
let(:runner) { project_runner }
it { expect_disallowed :read_runner_machine }
end
end
context 'with maintainer access' do
let(:user) { maintainer }
it_behaves_like 'a policy allowing reading instance runner machine depending on runner sharing'
it_behaves_like 'a policy allowing reading group runner machine depending on runner sharing'
context 'with project runner' do
let(:runner) { project_runner }
it { expect_allowed :read_runner_machine }
end
end
context 'with owner access' do
let(:user) { owner }
it_behaves_like 'a policy allowing reading instance runner machine depending on runner sharing'
context 'with group runner' do
let(:runner) { group_runner }
it { expect_allowed :read_runner_machine }
context 'with sharing of group runners disabled' do
before do
project.update!(group_runners_enabled: false)
end
it { expect_allowed :read_runner_machine }
end
end
context 'with project runner' do
let(:runner) { project_runner }
it { expect_allowed :read_runner_machine }
end
end
end
end

View File

@ -10,7 +10,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:group) { create(:group) }
let_it_be(:active_instance_runner) do
create(:ci_runner, :instance,
create(:ci_runner, :instance, :with_runner_machine,
description: 'Runner 1',
creator: user,
contacted_at: 2.hours.ago,
@ -58,7 +58,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
let_it_be(:project1) { create(:project) }
let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) }
let_it_be(:active_project_runner) do
create(:ci_runner, :project, :with_runner_machine, projects: [project1])
end
shared_examples 'runner details fetch' do
let(:query) do
@ -118,7 +120,12 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
'updateRunner' => true,
'deleteRunner' => true,
'assignRunner' => true
}
},
machines: a_hash_including(
"count" => runner.runner_machines.count,
"nodes" => an_instance_of(Array),
"pageInfo" => anything
)
)
expect(runner_data['tagList']).to match_array runner.tag_list
end

View File

@ -10,3 +10,13 @@ RSpec.shared_examples 'having unique enum values' do
end
end
end
RSpec.shared_examples 'having enum with nil value' do
it 'has enum with nil value' do
subject.public_send("#{attr_value}!")
expect(subject.public_send("#{attr}_for_database")).to be_nil
expect(subject.public_send("#{attr}?")).to eq(true)
expect(subject.class.public_send(attr_value)).to eq([subject])
end
end