Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-04-08 18:09:41 +00:00
parent bba263d1a3
commit c449ce987e
124 changed files with 1916 additions and 947 deletions

View File

@ -1,13 +0,0 @@
---
# Cop supports --autocorrect.
Lint/RedundantSafeNavigation:
Exclude:
- 'app/controllers/import/base_controller.rb'
- 'app/graphql/resolvers/users_resolver.rb'
- 'app/graphql/types/countable_connection_type.rb'
- 'app/helpers/search_helper.rb'
- 'ee/app/graphql/ee/types/issue_connection_type.rb'
- 'ee/app/models/search/namespace_index_assignment.rb'
- 'lib/api/projects.rb'
- 'lib/sidebars/projects/menus/infrastructure_menu.rb'
- 'spec/lib/gitlab/redis/multi_store_spec.rb'

View File

@ -332,7 +332,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/policies/incident_management/oncall_schedule_policy_spec.rb'
- 'ee/spec/policies/incident_management/oncall_shift_policy_spec.rb'
- 'ee/spec/policies/issue_policy_spec.rb'
- 'ee/spec/policies/merge_request/diff_llm_summary_policy_spec.rb'
- 'ee/spec/policies/merge_request_policy_spec.rb'
- 'ee/spec/policies/merge_requests/external_status_check_policy_spec.rb'
- 'ee/spec/policies/packages/policies/project_policy_spec.rb'
@ -672,7 +671,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/services/llm/generate_description_service_spec.rb'
- 'ee/spec/services/llm/generate_summary_service_spec.rb'
- 'ee/spec/services/llm/git_command_service_spec.rb'
- 'ee/spec/services/llm/merge_requests/summarize_diff_service_spec.rb'
- 'ee/spec/services/llm/merge_requests/summarize_review_service_spec.rb'
- 'ee/spec/services/members/activate_service_spec.rb'
- 'ee/spec/services/merge_trains/create_pipeline_service_spec.rb'

View File

@ -468,13 +468,11 @@ RSpec/NamedSubject:
- 'ee/spec/lib/gitlab/llm/templates/explain_vulnerability_spec.rb'
- 'ee/spec/lib/gitlab/llm/templates/fill_in_merge_request_template_spec.rb'
- 'ee/spec/lib/gitlab/llm/templates/generate_commit_message_spec.rb'
- 'ee/spec/lib/gitlab/llm/templates/summarize_merge_request_spec.rb'
- 'ee/spec/lib/gitlab/llm/templates/summarize_review_spec.rb'
- 'ee/spec/lib/gitlab/llm/templates/summarize_submitted_review_spec.rb'
- 'ee/spec/lib/gitlab/llm/vertex_ai/completions/analyze_ci_job_failure_spec.rb'
- 'ee/spec/lib/gitlab/llm/vertex_ai/completions/fill_in_merge_request_template_spec.rb'
- 'ee/spec/lib/gitlab/llm/vertex_ai/completions/generate_commit_message_spec.rb'
- 'ee/spec/lib/gitlab/llm/vertex_ai/completions/summarize_merge_request_spec.rb'
- 'ee/spec/lib/gitlab/llm/vertex_ai/completions/summarize_review_spec.rb'
- 'ee/spec/lib/gitlab/llm/vertex_ai/completions/summarize_submitted_review_spec.rb'
- 'ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/base_spec.rb'

View File

@ -1427,7 +1427,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/models/iteration.rb'
- 'ee/app/models/license.rb'
- 'ee/app/models/members/member_role.rb'
- 'ee/app/models/merge_request/diff_llm_summary.rb'
- 'ee/app/models/merge_request/predictions.rb'
- 'ee/app/models/merge_request/review_llm_summary.rb'
- 'ee/app/models/merge_requests/compliance_violation.rb'
@ -1441,7 +1440,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/models/search/namespace_index_assignment.rb'
- 'ee/app/models/security/training.rb'
- 'ee/app/models/vulnerabilities/finding.rb'
- 'ee/app/policies/merge_request/diff_llm_summary_policy.rb'
- 'ee/app/policies/merge_request/review_llm_summary_policy.rb'
- 'ee/app/policies/merge_request_diff_policy.rb'
- 'ee/app/policies/path_lock_policy.rb'
@ -1869,7 +1867,6 @@ Style/InlineDisableAnnotation:
- 'ee/lib/gitlab/llm/chat_storage.rb'
- 'ee/lib/gitlab/llm/open_ai/client.rb'
- 'ee/lib/gitlab/llm/templates/explain_vulnerability.rb'
- 'ee/lib/gitlab/llm/vertex_ai/completions/summarize_merge_request.rb'
- 'ee/lib/gitlab/llm/vertex_ai/completions/summarize_submitted_review.rb'
- 'ee/lib/gitlab/middleware/ip_restrictor.rb'
- 'ee/lib/gitlab/path_locks_finder.rb'

View File

@ -135,7 +135,7 @@ gem 'net-ldap', '~> 0.17.1' # rubocop:todo Gemfile/MissingFeatureCategory
# API
gem 'grape', '~> 2.0.0', feature_category: :api
gem 'grape-entity', '~> 0.10.2', feature_category: :api
gem 'grape-swagger', '~> 2.0.1', group: [:development, :test], feature_category: :api
gem 'grape-swagger', '~> 2.0.2', group: [:development, :test], feature_category: :api
gem 'grape-swagger-entity', '~> 0.5.1', group: [:development, :test], feature_category: :api
gem 'grape-path-helpers', '~> 2.0.1', feature_category: :api
gem 'rack-cors', '~> 2.0.1', require: 'rack/cors' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -270,7 +270,7 @@
{"name":"grape","version":"2.0.0","platform":"ruby","checksum":"3aeff94c17e84ccead4ff98833df691e7da0c108878cc128ca31f80c1047494a"},
{"name":"grape-entity","version":"0.10.2","platform":"ruby","checksum":"9eb584548135419d1c8ada7d21f7c174a7644e56a8b8e5bfc65d1a7a3421b571"},
{"name":"grape-path-helpers","version":"2.0.1","platform":"ruby","checksum":"ad5216e52c6e796738a9118087352ab4c962900dbad1d8f8c0f96e093c6702d7"},
{"name":"grape-swagger","version":"2.0.1","platform":"ruby","checksum":"0f90bede86dfe0f5317ea52fe9bfa93e595020e848cb46f1f8c47be04cb4c790"},
{"name":"grape-swagger","version":"2.0.2","platform":"ruby","checksum":"a7139a56ba36fab2e8465f10d668a8c73c30cf44ebe8af960f5a4e3beb200805"},
{"name":"grape-swagger-entity","version":"0.5.1","platform":"ruby","checksum":"f51e372d00ac96cf90d948f87b3f4eb287ab053976ca57ad503d442ad8605523"},
{"name":"grape_logging","version":"1.8.4","platform":"ruby","checksum":"efcc3e322dbd5d620a68f078733b7db043cf12680144cd03c982f14115c792d1"},
{"name":"graphiql-rails","version":"1.8.0","platform":"ruby","checksum":"02e2c5098be2c6c29219a0e9b2910a2cd3c494301587a3199a7c4484d8038ed1"},

View File

@ -858,7 +858,7 @@ GEM
grape (~> 2.0)
rake (> 12)
ruby2_keywords (~> 0.0.2)
grape-swagger (2.0.1)
grape-swagger (2.0.2)
grape (>= 1.7, < 3.0)
rack-test (~> 2)
grape-swagger-entity (0.5.1)
@ -1951,7 +1951,7 @@ DEPENDENCIES
grape (~> 2.0.0)
grape-entity (~> 0.10.2)
grape-path-helpers (~> 2.0.1)
grape-swagger (~> 2.0.1)
grape-swagger (~> 2.0.2)
grape-swagger-entity (~> 0.5.1)
grape_logging (~> 1.8, >= 1.8.4)
graphiql-rails (~> 1.8.0)

View File

@ -106,7 +106,7 @@ export default {
},
headerPipeline: {
query: getPipelineQuery,
// this query is already being called in pipeline_details_header.vue, which shares the same cache as this component
// this query is already being called in pipeline_header.vue, which shares the same cache as this component
// the skip here is to prevent sending double network requests on page load
skip() {
return !this.canRefetchHeaderPipeline;

View File

@ -20,7 +20,7 @@ import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.g
import { POLL_INTERVAL } from './constants';
export default {
name: 'PipelineDetailsHeader',
name: 'PipelineHeader',
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
pipelineCancel: 'pipelineCancel',
@ -292,7 +292,7 @@ export default {
</script>
<template>
<div class="gl-my-4" data-testid="pipeline-details-header">
<div class="gl-my-4" data-testid="pipeline-header">
<gl-alert
v-if="hasError"
class="gl-mb-4"

View File

@ -2,18 +2,18 @@ import VueRouter from 'vue-router';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { pipelineTabName } from './constants';
import { createPipelineDetailsHeaderApp } from './pipeline_details_header';
import { createPipelineHeaderApp } from './pipeline_header';
import { apolloProvider } from './pipeline_shared_client';
const SELECTORS = {
PIPELINE_DETAILS_HEADER: '#js-pipeline-details-header-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_TABS: '#js-pipeline-tabs',
};
export default async function initPipelineDetailsBundle() {
const headerSelector = SELECTORS.PIPELINE_DETAILS_HEADER;
const headerSelector = SELECTORS.PIPELINE_HEADER;
const headerApp = createPipelineDetailsHeaderApp;
const headerApp = createPipelineHeaderApp;
const headerEl = document.querySelector(headerSelector);

View File

@ -1,10 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import PipelineDetailsHeader from './header/pipeline_details_header.vue';
import PipelineHeader from './header/pipeline_header.vue';
Vue.use(VueApollo);
export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graphqlResourceEtag) => {
export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResourceEtag) => {
const el = document.querySelector(elSelector);
if (!el) {
@ -16,7 +16,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
// eslint-disable-next-line no-new
new Vue({
el,
name: 'PipelineDetailsHeaderApp',
name: 'PipelineHeaderApp',
apolloProvider,
provide: {
paths: {
@ -27,7 +27,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
pipelineIid,
},
render(createElement) {
return createElement(PipelineDetailsHeader);
return createElement(PipelineHeader);
},
});
};

View File

@ -3,14 +3,12 @@ import { GlButton } from '@gitlab/ui';
import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants';
import RegistrationInstructions from '../components/registration/registration_instructions.vue';
import PlatformsDrawer from '../components/registration/platforms_drawer.vue';
export default {
name: 'AdminRegisterRunnerApp',
components: {
GlButton,
RegistrationInstructions,
PlatformsDrawer,
},
props: {
runnerId: {
@ -25,7 +23,6 @@ export default {
data() {
return {
platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM,
isDrawerOpen: false,
};
},
watch: {
@ -39,9 +36,6 @@ export default {
onSelectPlatform(platform) {
this.platform = platform;
},
onToggleDrawer(val = !this.isDrawerOpen) {
this.isDrawerOpen = val;
},
},
};
</script>
@ -50,18 +44,11 @@ export default {
<registration-instructions
:runner-id="runnerId"
:platform="platform"
@toggleDrawer="onToggleDrawer"
@selectPlatform="onSelectPlatform"
>
<template #runner-list-name>{{ s__('Runners|Admin area Runners') }}</template>
</registration-instructions>
<platforms-drawer
:platform="platform"
:open="isDrawerOpen"
@selectPlatform="onSelectPlatform"
@close="onToggleDrawer(false)"
/>
<gl-button :href="runnersPath" variant="confirm">{{ s__('Runners|View runners') }}</gl-button>
</div>
</template>

View File

@ -5,20 +5,11 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import GoogleCloudFieldGroup from '~/ci/runner/components/registration/google_cloud_field_group.vue';
import GoogleCloudRegistrationInstructionsModal from '~/ci/runner/components/registration/google_cloud_registration_instructions_modal.vue';
import GoogleCloudLearnMoreLink from '~/ci/runner/components/registration/google_cloud_learn_more_link.vue';
import { createAlert } from '~/alert';
import { s__, __ } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import runnerForRegistrationQuery from '../../graphql/register/runner_for_registration.query.graphql';
import provisionGoogleCloudRunnerGroup from '../../graphql/register/provision_google_cloud_runner_group.query.graphql';
import provisionGoogleCloudRunnerProject from '../../graphql/register/provision_google_cloud_runner_project.query.graphql';
import {
I18N_FETCH_ERROR,
STATUS_ONLINE,
RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
} from '../../constants';
import { captureException } from '../../sentry_utils';
const GC_PROJECT_PATTERN = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/; // https://cloud.google.com/resource-manager/reference/rest/v1/projects
@ -101,25 +92,24 @@ export default {
HelpPopover,
},
props: {
runnerId: {
token: {
type: String,
required: true,
required: false,
default: null,
},
projectPath: {
type: String,
required: false,
default: '',
default: null,
},
groupPath: {
type: String,
required: false,
default: '',
default: null,
},
},
data() {
return {
token: '',
runner: null,
showInstructionsModal: false,
showInstructionsButtonVariant: 'default',
@ -138,36 +128,6 @@ export default {
};
},
apollo: {
runner: {
query: runnerForRegistrationQuery,
variables() {
return {
id: convertToGraphQLId(TYPENAME_CI_RUNNER, this.runnerId),
};
},
manual: true,
result({ data }) {
if (data?.runner) {
const { ephemeralAuthenticationToken, ...runner } = data.runner;
this.runner = runner;
// The token is available in the API for a limited amount of time
// preserve its original value if it is missing after polling.
this.token = ephemeralAuthenticationToken || this.token;
}
},
error(error) {
createAlert({ message: I18N_FETCH_ERROR });
captureException({ error, component: this.$options.name });
},
pollInterval() {
if (this.isRunnerOnline) {
// stop polling
return 0;
}
return RUNNER_REGISTRATION_POLLING_INTERVAL_MS;
},
},
project: {
query: provisionGoogleCloudRunnerProject,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
@ -230,9 +190,6 @@ export default {
machineType: this.machineType?.value,
};
},
isRunnerOnline() {
return this.runner?.status === STATUS_ONLINE;
},
tokenMessage() {
if (this.token) {
return s__(
@ -294,7 +251,6 @@ export default {
<template>
<div>
<div class="gl-mt-5">
<h1 class="gl-heading-1">{{ $options.i18n.heading }}</h1>
<p>
<gl-icon name="information-o" class="gl-text-blue-600" />
<gl-sprintf :message="tokenMessage">
@ -538,12 +494,5 @@ export default {
/>
<hr />
<!-- end: step two -->
<section v-if="isRunnerOnline">
<h2 class="gl-heading-2">🎉 {{ s__("Runners|You've registered a new runner!") }}</h2>
<p>
{{ s__('Runners|Your runner is online and ready to run jobs.') }}
</p>
</section>
</div>
</template>

View File

@ -1,11 +1,11 @@
<script>
import { GlIcon, GlLink, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/alert';
import { s__, sprintf } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import runnerForRegistrationQuery from '../../graphql/register/runner_for_registration.query.graphql';
import {
STATUS_ONLINE,
@ -13,10 +13,12 @@ import {
SERVICE_COMMANDS_HELP_URL,
RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
I18N_FETCH_ERROR,
I18N_REGISTRATION_SUCCESS,
GOOGLE_CLOUD_PLATFORM,
} from '../../constants';
import { captureException } from '../../sentry_utils';
import GoogleCloudRegistrationInstructions from './google_cloud_registration_instructions.vue';
import PlatformsDrawer from './platforms_drawer.vue';
import CliCommand from './cli_command.vue';
import { commandPrompt, registerCommand, runCommand } from './utils';
@ -29,7 +31,10 @@ export default {
GlSprintf,
ClipboardButton,
CliCommand,
GoogleCloudRegistrationInstructions,
PlatformsDrawer,
},
mixins: [glFeatureFlagsMixin()],
props: {
runnerId: {
type: String,
@ -39,11 +44,22 @@ export default {
type: String,
required: true,
},
projectPath: {
type: String,
required: false,
default: null,
},
groupPath: {
type: String,
required: false,
default: null,
},
},
data() {
return {
runner: null,
token: null,
isDrawerOpen: false,
};
},
apollo: {
@ -122,6 +138,11 @@ export default {
isRunnerOnline() {
return this.runner?.status === STATUS_ONLINE;
},
showGoogleCloudRegistration() {
return (
this.glFeatures?.googleCloudSupportFeatureFlag && this.platform === GOOGLE_CLOUD_PLATFORM
);
},
},
created() {
window.addEventListener('beforeunload', this.onBeforeunload);
@ -130,8 +151,11 @@ export default {
window.removeEventListener('beforeunload', this.onBeforeunload);
},
methods: {
toggleDrawer() {
this.$emit('toggleDrawer');
onSelectPlatform(event) {
this.$emit('selectPlatform', event);
},
onToggleDrawer(val = !this.isDrawerOpen) {
this.isDrawerOpen = val;
},
onBeforeunload(event) {
if (this.isRunnerOnline) {
@ -147,102 +171,122 @@ export default {
},
EXECUTORS_HELP_URL,
SERVICE_COMMANDS_HELP_URL,
I18N_REGISTRATION_SUCCESS,
};
</script>
<template>
<div class="gl-mt-5">
<h1 class="gl-heading-1">{{ heading }}</h1>
<p>
<gl-sprintf
:message="
s__(
'Runners|GitLab Runner must be installed before you can register a runner. %{linkStart}How do I install GitLab Runner?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link @click="toggleDrawer">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<section class="gl-mt-6">
<h2 class="gl-heading-2">{{ s__('Runners|Step 1') }}</h2>
<template v-if="showGoogleCloudRegistration">
<google-cloud-registration-instructions
:token="token"
:group-path="groupPath"
:project-path="projectPath"
/>
</template>
<template v-else>
<p>
{{
s__(
'Runners|Copy and paste the following command into your command line to register the runner.',
)
}}
<gl-sprintf
:message="
s__(
'Runners|GitLab Runner must be installed before you can register a runner. %{linkStart}How do I install GitLab Runner?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link @click="onToggleDrawer()">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<gl-skeleton-loader v-if="loading" />
<template v-else>
<cli-command :prompt="commandPrompt" :command="registerCommand" />
<section class="gl-mt-6">
<h2 class="gl-heading-2">{{ s__('Runners|Step 1') }}</h2>
<p>
<gl-icon name="information-o" class="gl-text-blue-600!" />
<gl-sprintf :message="tokenMessage">
<template #token>
<code data-testid="runner-token">{{ token }}</code>
<clipboard-button
:text="token"
:title="__('Copy')"
size="small"
category="tertiary"
class="gl-border-none!"
/>
{{
s__(
'Runners|Copy and paste the following command into your command line to register the runner.',
)
}}
</p>
<gl-skeleton-loader v-if="loading" />
<template v-else>
<cli-command :prompt="commandPrompt" :command="registerCommand" />
<p>
<gl-icon name="information-o" class="gl-text-blue-600!" />
<gl-sprintf :message="tokenMessage">
<template #token>
<code data-testid="runner-token">{{ token }}</code>
<clipboard-button
:text="token"
:title="__('Copy')"
size="small"
category="tertiary"
class="gl-border-none!"
/>
</template>
<template #bold="{ content }"
><span class="gl-font-weight-bold">{{ content }}</span></template
>
<template #code="{ content }"
><code>{{ content }}</code></template
>
</gl-sprintf>
</p>
</template>
</section>
<section class="gl-mt-6">
<h2 class="gl-heading-2">{{ s__('Runners|Step 2') }}</h2>
<p>
<gl-sprintf
:message="
s__(
'Runners|Choose an executor when prompted by the command line. Executors run builds in different environments. %{linkStart}Not sure which one to select?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="$options.EXECUTORS_HELP_URL" target="_blank">
{{ content }} <gl-icon name="external-link" />
</gl-link>
</template>
<template #bold="{ content }"
><span class="gl-font-weight-bold">{{ content }}</span></template
>
<template #code="{ content }"
><code>{{ content }}</code></template
>
</gl-sprintf>
</p>
</template>
</section>
<section class="gl-mt-6">
<h2 class="gl-heading-2">{{ s__('Runners|Step 2') }}</h2>
<p>
<gl-sprintf
:message="
s__(
'Runners|Choose an executor when prompted by the command line. Executors run builds in different environments. %{linkStart}Not sure which one to select?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="$options.EXECUTORS_HELP_URL" target="_blank">
{{ content }} <gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</p>
</section>
<section class="gl-mt-6">
<h2 class="gl-heading-2">{{ s__('Runners|Step 3 (optional)') }}</h2>
<p>{{ s__('Runners|Manually verify that the runner is available to pick up jobs.') }}</p>
<cli-command :prompt="commandPrompt" :command="runCommand" />
<p>
<gl-sprintf
:message="
s__(
'Runners|This may not be needed if you manage your runner as a %{linkStart}system or user service%{linkEnd}.',
)
"
>
<template #link="{ content }">
<gl-link :href="$options.SERVICE_COMMANDS_HELP_URL" target="_blank">
{{ content }} <gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</p>
</section>
</section>
<section class="gl-mt-6">
<h2 class="gl-heading-2">{{ s__('Runners|Step 3 (optional)') }}</h2>
<p>{{ s__('Runners|Manually verify that the runner is available to pick up jobs.') }}</p>
<cli-command :prompt="commandPrompt" :command="runCommand" />
<p>
<gl-sprintf
:message="
s__(
'Runners|This may not be needed if you manage your runner as a %{linkStart}system or user service%{linkEnd}.',
)
"
>
<template #link="{ content }">
<gl-link :href="$options.SERVICE_COMMANDS_HELP_URL" target="_blank">
{{ content }} <gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</p>
</section>
<platforms-drawer
:platform="platform"
:open="isDrawerOpen"
@selectPlatform="onSelectPlatform"
@close="onToggleDrawer(false)"
/>
</template>
<section v-if="isRunnerOnline" class="gl-mt-6">
<h2 class="gl-heading-2">🎉 {{ $options.I18N_REGISTRATION_SUCCESS }}</h2>
<h2 class="gl-heading-2">🎉 {{ s__("Runners|You've registered a new runner!") }}</h2>
<p>
{{ s__('Runners|Your runner is online and ready to run jobs.') }}
</p>
<p class="gl-pl-6">
<gl-sprintf :message="s__('Runners|To view the runner, go to %{runnerListName}.')">

View File

@ -121,8 +121,6 @@ export const I18N_NO_PROJECTS_FOUND = __('No projects found');
// Runner registration
export const I18N_REGISTRATION_SUCCESS = s__("Runners|You've created a new runner!");
export const RUNNER_REGISTRATION_POLLING_INTERVAL_MS = 2000;
// Styles

View File

@ -2,18 +2,14 @@
import { GlButton } from '@gitlab/ui';
import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, GOOGLE_CLOUD_PLATFORM } from '../constants';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants';
import RegistrationInstructions from '../components/registration/registration_instructions.vue';
import GoogleCloudRegistrationInstructions from '../components/registration/google_cloud_registration_instructions.vue';
import PlatformsDrawer from '../components/registration/platforms_drawer.vue';
export default {
name: 'GroupRegisterRunnerApp',
components: {
GoogleCloudRegistrationInstructions,
GlButton,
RegistrationInstructions,
PlatformsDrawer,
},
mixins: [glFeatureFlagsMixin()],
props: {
@ -33,16 +29,8 @@ export default {
data() {
return {
platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM,
isDrawerOpen: false,
};
},
computed: {
showGoogleCloudRegistration() {
return (
this.glFeatures.googleCloudSupportFeatureFlag && this.platform === GOOGLE_CLOUD_PLATFORM
);
},
},
watch: {
platform(platform) {
updateHistory({
@ -54,33 +42,19 @@ export default {
onSelectPlatform(platform) {
this.platform = platform;
},
onToggleDrawer(val = !this.isDrawerOpen) {
this.isDrawerOpen = val;
},
},
};
</script>
<template>
<div>
<template v-if="showGoogleCloudRegistration">
<google-cloud-registration-instructions :runner-id="runnerId" :group-path="groupPath" />
</template>
<template v-else>
<registration-instructions
:runner-id="runnerId"
:platform="platform"
@toggleDrawer="onToggleDrawer"
>
<template #runner-list-name>{{ s__('Runners|Group area Runners') }}</template>
</registration-instructions>
<platforms-drawer
:platform="platform"
:open="isDrawerOpen"
@selectPlatform="onSelectPlatform"
@close="onToggleDrawer(false)"
/>
</template>
<registration-instructions
:runner-id="runnerId"
:group-path="groupPath"
:platform="platform"
@selectPlatform="onSelectPlatform"
>
<template #runner-list-name>{{ s__('Runners|Group area Runners') }}</template>
</registration-instructions>
<gl-button
:href="runnersPath"

View File

@ -2,18 +2,14 @@
import { GlButton } from '@gitlab/ui';
import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, GOOGLE_CLOUD_PLATFORM } from '../constants';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants';
import RegistrationInstructions from '../components/registration/registration_instructions.vue';
import GoogleCloudRegistrationInstructions from '../components/registration/google_cloud_registration_instructions.vue';
import PlatformsDrawer from '../components/registration/platforms_drawer.vue';
export default {
name: 'ProjectRegisterRunnerApp',
components: {
GoogleCloudRegistrationInstructions,
GlButton,
RegistrationInstructions,
PlatformsDrawer,
},
mixins: [glFeatureFlagsMixin()],
props: {
@ -33,16 +29,8 @@ export default {
data() {
return {
platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM,
isDrawerOpen: false,
};
},
computed: {
showGoogleCloudRegistration() {
return (
this.glFeatures.googleCloudSupportFeatureFlag && this.platform === GOOGLE_CLOUD_PLATFORM
);
},
},
watch: {
platform(platform) {
updateHistory({
@ -54,35 +42,19 @@ export default {
onSelectPlatform(platform) {
this.platform = platform;
},
onToggleDrawer(val = !this.isDrawerOpen) {
this.isDrawerOpen = val;
},
},
};
</script>
<template>
<div>
<template v-if="showGoogleCloudRegistration">
<google-cloud-registration-instructions :runner-id="runnerId" :project-path="projectPath" />
</template>
<template v-else>
<registration-instructions
:runner-id="runnerId"
:platform="platform"
@toggleDrawer="onToggleDrawer"
>
<template #runner-list-name>{{
s__('Runners|Project CI/CD Settings Runners')
}}</template>
</registration-instructions>
<platforms-drawer
:platform="platform"
:open="isDrawerOpen"
@selectPlatform="onSelectPlatform"
@close="onToggleDrawer(false)"
/>
</template>
<registration-instructions
:runner-id="runnerId"
:project-path="projectPath"
:platform="platform"
@selectPlatform="onSelectPlatform"
>
<template #runner-list-name>{{ s__('Runners|Project CI/CD Settings Runners') }}</template>
</registration-instructions>
<gl-button
:href="runnersPath"

View File

@ -390,7 +390,7 @@ export default {
/>
<gl-form-checkbox
v-if="isReviewable && showLocalFileReviews"
v-gl-tooltip.hover.focus
v-gl-tooltip.hover.focus.left
data-testid="fileReviewCheckbox"
class="gl-mr-5 gl-mb-n3 gl-display-flex gl-align-items-center"
:title="$options.i18n.fileReviewTooltip"

View File

@ -6,9 +6,13 @@ import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { DEFAULT_PER_PAGE } from '~/api';
import { deleteProject } from '~/rest_api';
import { createAlert } from '~/alert';
import {
renderProjectDeleteSuccessToast,
deleteProjectParams,
formatProjects,
} from 'ee_else_ce/organizations/shared/utils';
import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
import projectsQuery from '../graphql/queries/projects.query.graphql';
import { formatProjects } from '../utils';
import NewProjectButton from './new_project_button.vue';
export default {
@ -174,8 +178,9 @@ export default {
try {
this.setProjectIsDeleting(nodeIndex, true);
await deleteProject(project.id);
await deleteProject(project.id, deleteProjectParams(project));
this.$apollo.queries.projects.refetch();
renderProjectDeleteSuccessToast(project);
} catch (error) {
createAlert({ message: this.$options.i18n.deleteErrorMessage, error, captureError: true });
} finally {

View File

@ -1,4 +1,6 @@
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import toast from '~/vue_shared/plugins/global_toast';
import { sprintf, __ } from '~/locale';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR } from './constants';
@ -92,3 +94,16 @@ export const onPageChange = ({
return routeQuery;
};
export const renderProjectDeleteSuccessToast = (project) => {
toast(
sprintf(__("Project '%{name}' is being deleted."), {
name: project.name,
}),
);
};
export const deleteProjectParams = () => {
// Overridden in EE
return {};
};

View File

@ -54,6 +54,10 @@ export default {
DeleteModal,
ListActions,
ProjectListItemInactiveBadge,
ProjectListItemDelayedDeletionModalFooter: () =>
import(
'ee_component/vue_shared/components/projects_list/project_list_item_delayed_deletion_modal_footer.vue'
),
},
directives: {
GlTooltip: GlTooltipDirective,
@ -402,6 +406,10 @@ export default {
:forks-count="forksCount"
:stars-count="starCount"
@primary="$emit('delete', project)"
/>
>
<template #modal-footer
><project-list-item-delayed-deletion-modal-footer :project="project"
/></template>
</delete-modal>
</li>
</template>

View File

@ -15,7 +15,7 @@ class Import::BaseController < ApplicationController
incompatible_repos: serialized_incompatible_repos }
end
format.html do
if params[:namespace_id]&.present?
if params[:namespace_id].present?
@namespace = Namespace.find_by_id(params[:namespace_id])
render_404 unless current_user.can?(:import_projects, @namespace)

View File

@ -48,7 +48,7 @@ module Resolvers
return super if args.values.compact.blank?
if args[:usernames]&.present? && args[:ids]&.present?
if args[:usernames].present? && args[:ids].present?
raise Gitlab::Graphql::Errors::ArgumentError, 'Provide either a list of usernames or ids'
end

View File

@ -14,7 +14,7 @@ module Types
relation = relation.reorder(nil) if relation.respond_to?(:reorder)
# rubocop: enable CodeReuse/ActiveRecord
if relation.try(:group_values)&.present?
if relation.try(:group_values).present?
relation.size.keys.size
else
relation.size

View File

@ -21,7 +21,6 @@ module Types
field :url, GraphQL::Types::String,
null: false,
method: :file,
description: 'Link to file of the emoji.'
field :external, GraphQL::Types::Boolean,

View File

@ -25,7 +25,7 @@ module Projects
}
end
def js_pipeline_details_header_data(project, pipeline)
def js_pipeline_header_data(project, pipeline)
{
full_path: project.full_path,
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),

View File

@ -204,11 +204,11 @@ module SearchHelper
end
def search_has_group?
search_group&.present? && search_group&.persisted?
search_group.present? && search_group&.persisted?
end
def search_has_project?
@project&.present? && @project&.persisted?
@project.present? && @project&.persisted?
end
def header_search_context
@ -228,7 +228,7 @@ module SearchHelper
end
hash[:scope] = search_scope if search_has_project? || search_has_group?
hash[:for_snippets] = @snippet&.present? || @snippets&.any?
hash[:for_snippets] = @snippet.present? || @snippets&.any?
end
end

View File

@ -67,7 +67,7 @@ class AwardEmoji < ApplicationRecord
return if TanukiEmoji.find_by_alpha_code(name)
Groups::CustomEmojiFinder.new(resource_parent, { include_ancestor_groups: true }).execute
.by_name(name)&.select(:url)&.first&.url
.by_name(name)&.select(:file)&.first&.url
end
def expire_cache

View File

@ -58,6 +58,10 @@ module Ci
end
end
def waiting_processables
processables.waiting_for_resource
end
def current_processable
Ci::Processable.find_by('(id, partition_id) IN (?)', resources.select('build_id, partition_id'))
end

View File

@ -2,12 +2,6 @@
module Ci
# This model represents metadata for a running build.
# Despite the generic RunningBuild name, in this first iteration it applies only to shared runners
# (see Ci::RunningBuild.upsert_shared_runner_build!).
# The decision to insert all of the running builds here was deferred to avoid the pressure on the database as
# at this time that was not necessary.
# We can reconsider the decision to limit this only to shared runners when there is more evidence that inserting all
# of the running builds there is worth the additional pressure.
class RunningBuild < Ci::ApplicationRecord
include Ci::Partitionable
@ -22,11 +16,15 @@ module Ci
enum runner_type: ::Ci::Runner.runner_types
def self.upsert_shared_runner_build!(build)
unless build.shared_runner_build?
def self.upsert_build!(build)
unless add_ci_running_build?(build)
raise ArgumentError, 'build has not been picked by a shared runner'
end
if build.runner.nil?
raise ArgumentError, 'build has not been picked by a runner'
end
entry = self.new(
build: build,
project: build.project,
@ -38,5 +36,11 @@ module Ci
self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
end
private_class_method def self.add_ci_running_build?(build)
return true if Feature.enabled?(:add_all_ci_running_builds, Project.actor_from_id(build.project_id))
build.shared_runner_build?
end
end
end

View File

@ -45,8 +45,6 @@ class CustomEmoji < ApplicationRecord
.order(order)
end
alias_attribute :url, :file # this might need a change in https://gitlab.com/gitlab-org/gitlab/-/issues/230467
scope :for_resource, -> (resource) do
return none if resource.nil?
return none unless resource.is_a?(Group)
@ -54,6 +52,10 @@ class CustomEmoji < ApplicationRecord
resource.custom_emoji
end
def url
Gitlab::AssetProxy.proxy_url(file)
end
private
def valid_emoji_name

View File

@ -2211,10 +2211,29 @@ class MergeRequest < ApplicationRecord
merge_request_diff.get_patch_id_sha
end
def auto_merge_available_when_pipeline_succeeds?
pipeline = diff_head_pipeline
return unless pipeline
if auto_merge_when_incomplete_pipeline_succeeds_enabled?
!pipeline.complete?
else
pipeline.active?
end
end
private
attr_accessor :skip_fetch_ref
def auto_merge_when_incomplete_pipeline_succeeds_enabled?
Feature.enabled?(
:auto_merge_when_incomplete_pipeline_succeeds,
Project.actor_from_id(target_project_id),
type: :gitlab_com_derisk
)
end
def merge_base_pipelines
return ::Ci::Pipeline.none unless diff_head_pipeline&.target_sha

View File

@ -45,7 +45,7 @@ module AutoMerge
end
def check_availability(merge_request)
merge_request.diff_head_pipeline&.active?
merge_request.auto_merge_available_when_pipeline_succeeds?
end
def notify(merge_request)

View File

@ -3,12 +3,37 @@
module Ci
module ResourceGroups
class AssignResourceFromResourceGroupService < ::BaseService
# rubocop: disable CodeReuse/ActiveRecord
RESPAWN_WAIT_TIME = 1.minute
def execute(resource_group)
release_resource_from_stale_jobs(resource_group)
free_resources = resource_group.resources.free.count
if free_resources == 0
if resource_group.waiting_processables.any?
# if the resource group is still 'tied up' in other processables,
# and there are more upcoming processables
# kick off the worker again for the current resource group
respawn_assign_resource_worker(resource_group)
end
return
end
enqueue_upcoming_processables(free_resources, resource_group)
end
private
def respawn_assign_resource_worker(resource_group)
return if Feature.disabled?(:respawn_assign_resource_worker, project, type: :gitlab_com_derisk)
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker.perform_in(RESPAWN_WAIT_TIME, resource_group.id)
end
# rubocop: disable CodeReuse/ActiveRecord
def enqueue_upcoming_processables(free_resources, resource_group)
resource_group.upcoming_processables.take(free_resources).each do |upcoming|
Gitlab::OptimisticLocking.retry_lock(upcoming, name: 'enqueue_waiting_for_resource') do |processable|
processable.enqueue_waiting_for_resource
@ -17,8 +42,6 @@ module Ci
end
# rubocop: enable CodeReuse/ActiveRecord
private
def release_resource_from_stale_jobs(resource_group)
resource_group.resources.stale_processables.find_each do |processable|
resource_group.release_resource_from(processable)

View File

@ -50,18 +50,19 @@ module Ci
end
##
# Add shared runner build tracking entry (used for queuing).
# Add runner build tracking entry (used for queuing and for runner fleet dashboard).
#
def track(build, transition)
return unless build.shared_runner_build?
return if build.runner.nil?
return unless add_ci_running_build?(build)
raise InvalidQueueTransition unless transition.to == 'running'
transition.within_transaction do
result = ::Ci::RunningBuild.upsert_shared_runner_build!(build)
result = ::Ci::RunningBuild.upsert_build!(build)
unless result.empty?
metrics.increment_queue_operation(:shared_runner_build_new)
metrics.increment_queue_operation(:shared_runner_build_new) if build.shared_runner_build?
result.rows.dig(0, 0)
end
@ -69,11 +70,11 @@ module Ci
end
##
# Remove a runtime build tracking entry for a shared runner build (used for
# queuing).
# Remove a runtime build tracking entry for a runner build (used for queuing and for runner fleet dashboard).
#
def untrack(build, transition)
return unless build.shared_runner_build?
return if build.runner.nil?
return unless remove_ci_running_build?(build)
raise InvalidQueueTransition unless transition.from == 'running'
@ -81,7 +82,7 @@ module Ci
removed = build.all_runtime_metadata.delete_all
if removed > 0
metrics.increment_queue_operation(:shared_runner_build_done)
metrics.increment_queue_operation(:shared_runner_build_done) if build.shared_runner_build?
build.id
end
@ -109,5 +110,17 @@ module Ci
runner.pick_build!(build)
end
end
def add_ci_running_build?(build)
return true if Feature.enabled?(:add_all_ci_running_builds, Project.actor_from_id(build.project_id))
build.shared_runner_build?
end
def remove_ci_running_build?(build)
return true if Feature.enabled?(:remove_all_ci_running_builds, Project.actor_from_id(build.project_id))
build.shared_runner_build?
end
end
end

View File

@ -61,7 +61,7 @@ module MergeRequests
end
def ci_check_failed_check
if merge_request.diff_head_pipeline&.active?
if merge_request.auto_merge_available_when_pipeline_succeeds?
:ci_still_running
else
check_ci_results.payload.fetch(:identifier)

View File

@ -10,7 +10,7 @@
- add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
#js-pipeline-details-header-vue{ data: js_pipeline_details_header_data(@project, @pipeline) }
#js-pipeline-header-vue{ data: js_pipeline_header_data(@project, @pipeline) }
= render_if_exists 'projects/pipelines/cc_validation_required_alert', pipeline: @pipeline

View File

@ -1,9 +1,9 @@
---
name: workhorse_google_client
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/9457
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96891
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372596
milestone: '15.6'
group: group::package registry
name: add_all_ci_running_builds
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437846
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147943
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/452166
milestone: '16.11'
group: group::runner
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,9 @@
---
name: auto_merge_when_incomplete_pipeline_succeeds
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/439443
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148210
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/454162
milestone: '16.11'
group: group::pipeline execution
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,9 @@
---
name: remove_all_ci_running_builds
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437846
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147943
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/452166
milestone: '16.11'
group: group::runner
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,9 @@
---
name: respawn_assign_resource_worker
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/436988
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147313
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/450793
milestone: '16.11'
group: group::environments
type: gitlab_com_derisk
default_enabled: false

View File

@ -717,6 +717,8 @@
- 1
- - security_refresh_project_policies
- 1
- - security_scan_execution_policies_create_pipeline
- 1
- - security_scan_execution_policies_rule_schedule
- 1
- - security_scan_result_policies_add_approvers_to_rules

View File

@ -0,0 +1,8 @@
---
migration_job_name: UpdateSbomOccurrencesComponentNameBasedOnPep503
description: Updates sbom_occurrences.component_name in accordance with PEP503
feature_category: software_composition_analysis
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146776
milestone: '16.11'
queued_migration_version: 20240306120522
finalize_after: '2024-03-24'

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class QueueUpdateSbomOccurrencesComponentNameBasedOnPep503 < Gitlab::Database::Migration[2.2]
milestone '16.11'
restrict_gitlab_migration gitlab_schema: :gitlab_main
MIGRATION = "UpdateSbomOccurrencesComponentNameBasedOnPep503"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 2000
SUB_BATCH_SIZE = 200
def up
queue_batched_background_migration(
MIGRATION,
:sbom_occurrences,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :sbom_occurrences, :id, [])
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class RemoveIdxMergeRequestsOnTargetProjectIdAndLockedState < Gitlab::Database::Migration[2.2]
milestone '16.11'
INDEX_NAME = 'idx_merge_requests_on_target_project_id_and_locked_state'
COLUMN_NAME = :target_project_id
# TODO: Index to be destroyed synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/454457
def up
prepare_async_index_removal :merge_requests, COLUMN_NAME, name: INDEX_NAME
end
def down
unprepare_async_index :merge_requests, COLUMN_NAME, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
2645479cd4ce377b4183c1fade37050a2b143d9697f341c690483a11a202bde4

View File

@ -0,0 +1 @@
c24c3c941355f6f75f16bb73544581a83db9e996cffcb7f40d44914f42944751

View File

@ -103,7 +103,7 @@ To change the worker timeout to 600 seconds:
## Disable Puma clustered mode in memory-constrained environments
WARNING:
This feature is an [Experiment](../../policy/experiment-beta-support.md#experiment) and subject to change without notice. The feature
This feature is an [Experiment](../../policy/experiment-beta-support.md#experiment) and subject to change without notice. This feature
is not ready for production use. If you want to use this feature, you should test
outside of production first. See the [known issues](#puma-single-mode-known-issues)
for additional details.

View File

@ -49,7 +49,7 @@ In GitLab 16.1 and earlier, you should **not** use direct transfer with [schedul
WARNING:
This feature is in [Beta](../../policy/experiment-beta-support.md#beta) and subject to change without notice.
The feature is not ready for production use.
This feature is not ready for production use.
Migration of groups and projects by direct transfer is disabled by default.
To enable migration of groups and projects by direct transfer:

View File

@ -12359,29 +12359,6 @@ The edge type for [`MergeRequestDiff`](#mergerequestdiff).
| <a id="mergerequestdiffedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="mergerequestdiffedgenode"></a>`node` | [`MergeRequestDiff`](#mergerequestdiff) | The item at the end of the edge. |
#### `MergeRequestDiffLlmSummaryConnection`
The connection type for [`MergeRequestDiffLlmSummary`](#mergerequestdiffllmsummary).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestdiffllmsummaryconnectionedges"></a>`edges` | [`[MergeRequestDiffLlmSummaryEdge]`](#mergerequestdiffllmsummaryedge) | A list of edges. |
| <a id="mergerequestdiffllmsummaryconnectionnodes"></a>`nodes` | [`[MergeRequestDiffLlmSummary]`](#mergerequestdiffllmsummary) | A list of nodes. |
| <a id="mergerequestdiffllmsummaryconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `MergeRequestDiffLlmSummaryEdge`
The edge type for [`MergeRequestDiffLlmSummary`](#mergerequestdiffllmsummary).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestdiffllmsummaryedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="mergerequestdiffllmsummaryedgenode"></a>`node` | [`MergeRequestDiffLlmSummary`](#mergerequestdiffllmsummary) | The item at the end of the edge. |
#### `MergeRequestDiffRegistryConnection`
The connection type for [`MergeRequestDiffRegistry`](#mergerequestdiffregistry).
@ -22664,7 +22641,6 @@ Defines which user roles, users, or groups can merge into a protected branch.
| <a id="mergerequestdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="mergerequestdetailedmergestatus"></a>`detailedMergeStatus` | [`DetailedMergeStatus`](#detailedmergestatus) | Detailed merge status of the merge request. |
| <a id="mergerequestdiffheadsha"></a>`diffHeadSha` | [`String`](#string) | Diff head SHA of the merge request. |
| <a id="mergerequestdiffllmsummaries"></a>`diffLlmSummaries` **{warning-solid}** | [`MergeRequestDiffLlmSummaryConnection`](#mergerequestdiffllmsummaryconnection) | **Introduced** in GitLab 16.1. **Status**: Experiment. Diff summaries generated by AI. |
| <a id="mergerequestdiffrefs"></a>`diffRefs` | [`DiffRefs`](#diffrefs) | References of the base SHA, the head SHA, and the start SHA for this merge request. |
| <a id="mergerequestdiffstatssummary"></a>`diffStatsSummary` | [`DiffStatsSummary`](#diffstatssummary) | Summary of which files were changed in this merge request. |
| <a id="mergerequestdiscussionlocked"></a>`discussionLocked` | [`Boolean!`](#boolean) | Indicates if comments on the merge request are locked to members only. |
@ -23460,25 +23436,9 @@ A diff version of a merge request.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestdiffcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the diff was created. |
| <a id="mergerequestdiffdiffllmsummary"></a>`diffLlmSummary` | [`MergeRequestDiffLlmSummary`](#mergerequestdiffllmsummary) | Diff summary generated by AI. |
| <a id="mergerequestdiffreviewllmsummaries"></a>`reviewLlmSummaries` | [`MergeRequestReviewLlmSummaryConnection`](#mergerequestreviewllmsummaryconnection) | Review summaries generated by AI. (see [Connections](#connections)) |
| <a id="mergerequestdiffupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the diff was updated. |
### `MergeRequestDiffLlmSummary`
A diff summary generated by AI.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestdiffllmsummarycontent"></a>`content` | [`String!`](#string) | Content of the diff summary. |
| <a id="mergerequestdiffllmsummarycreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the diff summary was created. |
| <a id="mergerequestdiffllmsummarymergerequestdiffid"></a>`mergeRequestDiffId` | [`ID!`](#id) | ID of the Merge Request diff associated with the diff summary. |
| <a id="mergerequestdiffllmsummaryprovider"></a>`provider` | [`String!`](#string) | AI provider that generated the summary. |
| <a id="mergerequestdiffllmsummaryupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the diff summary was updated. |
| <a id="mergerequestdiffllmsummaryuser"></a>`user` | [`UserCore`](#usercore) | User associated with the diff summary. |
### `MergeRequestDiffRegistry`
Represents the Geo sync and verification state of a Merge Request diff.

View File

@ -1597,6 +1597,7 @@ GET /groups/:id/hooks/:hook_id
"deployment_events": true,
"releases_events": true,
"subgroup_events": true,
"member_events": true,
"enable_ssl_verification": true,
"repository_update_events": false,
"alert_status": "executable",
@ -1634,6 +1635,7 @@ POST /groups/:id/hooks
| `deployment_events` | boolean | no | Trigger hook on deployment events |
| `releases_events` | boolean | no | Trigger hook on release events |
| `subgroup_events` | boolean | no | Trigger hook on subgroup events |
| `member_events` | boolean | no | Trigger hook on member events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; not returned in the response |
| `resource_access_token_events` | boolean | no | Trigger hook on project access token expiry events. |
@ -1666,6 +1668,7 @@ PUT /groups/:id/hooks/:hook_id
| `deployment_events` | boolean | no | Trigger hook on deployment events. |
| `releases_events` | boolean | no | Trigger hook on release events. |
| `subgroup_events` | boolean | no | Trigger hook on subgroup events. |
| `member_events` | boolean | no | Trigger hook on member events. |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook. |
| `service_access_tokens_expiration_enforced` | boolean | no | Require service account access tokens to have an expiration date. |
| `token` | string | no | Secret token to validate received payloads. Not returned in the response. When you change the webhook URL, the secret token is reset and not retained. |

View File

@ -906,7 +906,7 @@ DETAILS:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425066) in GitLab 16.9 as a [Beta](../policy/experiment-beta-support.md) feature [with a flag](../administration/feature_flags.md) named `google_cloud_support_feature_flag`. Disabled by default.
FLAG:
On GitLab.com, this feature is not available. The feature is not ready for production use.
On GitLab.com, this feature is not available. This feature is not ready for production use.
### Set up Google Artifact Registry

View File

@ -839,7 +839,7 @@ Use `detailed_merge_status` instead of `merge_status` to account for all potenti
- `not_approved`: Approval is required before merge.
- `not_open`: The merge request must be open before merge.
- `jira_association_missing`: The title or description must reference a Jira issue.
- `needs_rebase`: The merge request must be rebased.
- `need_rebase`: The merge request must be rebased.
- `conflict`: There are conflicts between the source and target branches.
- `requested_changes`: The merge request has reviewers who have requested changes.

View File

@ -16,7 +16,7 @@ On self-managed GitLab, by default this feature is not available. To make it ava
an administrator can [enable the feature flags](../../../administration/feature_flags.md)
named `activity_pub` and `activity_pub_project`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
This feature requires two feature flags:

View File

@ -16,7 +16,7 @@ On self-managed GitLab, by default this feature is not available. To make it ava
an administrator can [enable the feature flags](../../../administration/feature_flags.md)
named `activity_pub` and `activity_pub_project`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
This feature requires two feature flags:

View File

@ -16,7 +16,7 @@ On self-managed GitLab, by default this feature is not available. To make it ava
an administrator can [enable the feature flags](../../administration/feature_flags.md)
named `activity_pub` and `activity_pub_project`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
Usage of ActivityPub in GitLab is governed by the
[GitLab Testing Agreement](https://handbook.gitlab.com/handbook/legal/testing-agreement/).

View File

@ -11,7 +11,7 @@ vulnerabilities commonly identified in the GitLab codebase. They are intended
to help developers identify potential security vulnerabilities early, with the
goal of reducing the number of vulnerabilities released over time.
**Contributing**
## Contributing
If you would like to contribute to one of the existing documents, or add
guidelines for a new vulnerability type, open an MR! Try to
@ -35,7 +35,7 @@ A common vulnerability when permission checks are missing is called [IDOR](https
### When to Consider
Each time you implement a new feature/endpoint, whether it is at UI, API or GraphQL level.
Each time you implement a new feature or endpoint at the UI, API, or GraphQL level.
### Mitigations
@ -47,7 +47,7 @@ Each time you implement a new feature/endpoint, whether it is at UI, API or Grap
- Do not forget **abuse cases**: write specs that **make sure certain things can't happen**
- A lot of specs are making sure things do happen and coverage percentage doesn't take into account permissions as same piece of code is used.
- Make assertions that certain actors cannot perform actions
- Naming convention to ease auditability: to be defined, for example, a subfolder containing those specific permission tests or a `#permissions` block
- Naming convention to ease auditability: to be defined, for example, a subfolder containing those specific permission tests, or a `#permissions` block
Be careful to **also test [visibility levels](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/doc/development/permissions.md#feature-specific-permissions)** and not only project access rights.
@ -260,7 +260,7 @@ The preferred SSRF mitigations within GitLab are:
The [`Gitlab::HTTP`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/http.rb) wrapper library has grown to include mitigations for all of the GitLab-known SSRF vectors. It is also configured to respect the
`Outbound requests` options that allow instance administrators to block all internal connections, or limit the networks to which connections can be made.
The `Gitlab::HTTP` wrapper library deletages the requests to the [`gitlab-http`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-http) gem.
The `Gitlab::HTTP` wrapper library delegates the requests to the [`gitlab-http`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-http) gem.
In some cases, it has been possible to configure `Gitlab::HTTP` as the HTTP
connection library for 3rd-party gems. This is preferable over re-implementing
@ -387,7 +387,7 @@ Note that denylists should be avoided, as it is near impossible to block all [va
#### Output encoding
Once you've [determined when and where](#setting-expectations) the user submitted data will be output, it's important to encode it based on the appropriate context. For example:
After you've [determined when and where](#setting-expectations) the user submitted data will be output, it's important to encode it based on the appropriate context. For example:
- Content placed inside HTML elements need to be [HTML entity encoded](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-1---html-escape-before-inserting-untrusted-data-into-html-element-content).
- Content placed into a JSON response needs to be [JSON encoded](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-31---html-escape-json-values-in-an-html-context-and-read-the-data-with-jsonparse).
@ -484,7 +484,7 @@ Traversal can occur when a path includes directories. A typical malicious exampl
### Impact
Path Traversal attacks can lead to multiple critical and high severity issues, like arbitrary file read, remote code execution or information disclosure.
Path Traversal attacks can lead to multiple critical and high severity issues, like arbitrary file read, remote code execution, or information disclosure.
### When to consider
@ -1253,7 +1253,7 @@ GitLab-specific example can be found in [this issue](https://gitlab.com/gitlab-o
**Example 2:** you have a feature which schedules jobs. When the user schedules the job, they have permission to do so. But imagine if, between the time they schedule the job and the time it is run, their permissions are restricted. Unless you re-check permissions at time of use, you could inadvertently allow unauthorized activity.
**Example 3:** you need to fetch a remote file, and perform a `HEAD` request to get and validate the content length and content type. When you subsequently make a `GET` request, though, the file delivered is a different size or different file type. (This is stretching the definition of TOCTOU, but things _have_ changed between time of check and time of use).
**Example 3:** you need to fetch a remote file, and perform a `HEAD` request to get and validate the content length and content type. When you subsequently make a `GET` request, the file delivered is a different size or different file type. (This is stretching the definition of TOCTOU, but things _have_ changed between time of check and time of use).
**Example 4:** you allow users to upvote a comment if they haven't already. The server is multi-threaded, and you aren't using transactions or an applicable database index. By repeatedly selecting upvote in quick succession a malicious user is able to add multiple upvotes: the requests arrive at the same time, the checks run in parallel and confirm that no upvote exists yet, and so each upvote is written to the database.
@ -1508,6 +1508,13 @@ Logging helps track events for debugging. Logging also allows the application to
- An audit trail for log edits must be available.
- To avoid data loss, logs must be saved on different storage.
### Related topics
- [Log system in GitLab](../administration/logs/index.md)
- [Audit event development guidelines](../development/audit_event_guide/index.md))
- [Security logging overview](https://handbook.gitlab.com/handbook/security/security-operations/security-logging/)
- [OWASP logging cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)
## URL Spoofing
We want to protect our users from bad actors who might try to use GitLab
@ -1540,13 +1547,37 @@ end
Also see this [real-life usage](https://gitlab.com/gitlab-org/gitlab/-/blob/bdba5446903ff634fb12ba695b2de99b6d6881b5/app/helpers/application_helper.rb#L378) as an example.
## Email and notifications
Ensure that only intended recipients get emails and notifications. Even if your
code is secure when it merges, it's better practice to use the defense-in-depth
"single recipient" check just before sending the email. This prevents a vulnerability
if otherwise-vulnerable code is committed at a later date. For example:
### Example: Ruby
```ruby
# Insecure if email is user-controlled
def insecure_email(email)
mail(to: email, subject: 'Password reset email')
end
# A single recipient, just as a developer expects
insecure_email("person@example.com")
# Multiple emails sent when an array is passed
insecure_email(["person@example.com", "attacker@evil.com"])
# Multiple emails sent even when a single string is passed
insecure_email("person@example.com, attacker@evil.com")
```
### Prevention and defense
- Use `Gitlab::Email::SingleRecipientValidator` when adding new emails intended for a single recipient
- Strongly type your code by calling `.to_s` on values, or check its class with `value.kind_of?(String)`
## Who to contact if you have questions
For general guidance, contact the [Application Security](https://handbook.gitlab.com/handbook/security/product-security/application-security/) team.
## Related topics
- [Log system in GitLab](../administration/logs/index.md)
- [Audit event development guidelines](../development/audit_event_guide/index.md))
- [Security logging overview](https://handbook.gitlab.com/handbook/security/security-operations/security-logging/)
- [OWASP logging cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)
For general guidance, contact the
[Application Security](https://handbook.gitlab.com/handbook/security/product-security/application-security/) team.

View File

@ -29,6 +29,19 @@ All workers should include `ApplicationWorker` instead of `Sidekiq::Worker`,
which adds some convenience methods and automatically sets the queue based on
the [routing rules](../../administration/sidekiq/processing_specific_job_classes.md#routing-rules).
## Sharding
All calls to Sidekiq APIs must account for sharding. To achieve this,
utilize the Sidekiq API within the `Sidekiq::Client.via` block to guarantee the correct `Sidekiq.redis` pool is utilized.
Obtain the suitable Redis pool by invoking the `Gitlab::SidekiqSharding::Router.get_shard_instance` method.
```ruby
pool_name, pool = Gitlab::SidekiqSharding::Router.get_shard_instance(worker_class.sidekiq_options['store'])
Sidekiq::Client.via(pool) do
...
end
```
## Retries
Sidekiq defaults to using [25 retries](https://github.com/mperham/sidekiq/wiki/Error-Handling#automatic-job-retry),

View File

@ -17,7 +17,7 @@ DETAILS:
FLAG:
On self-managed GitLab, this feature is not available.
On GitLab.com, this feature is available. On GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
Many teams receive alerts and collaborate in real time during incidents in Slack.
Use the GitLab for Slack app to:

View File

@ -15,7 +15,7 @@ DETAILS:
FLAG:
This feature is only available on GitLab.com. On self-managed GitLab and GitLab Dedicated, by default this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
GitLab supports centralized application and infrastructure logs collection, storage, and analysis.
GiLab Logging provides insight about the operational health of monitored systems.

View File

@ -15,7 +15,7 @@ DETAILS:
FLAG:
This feature is only available on GitLab.com. On self-managed GitLab and GitLab Dedicated, by default this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
Metrics provide insight about the operational health of monitored systems.
Use metrics to learn more about your systems and applications in a given time range.

View File

@ -17,7 +17,7 @@ DETAILS:
FLAG:
On GitLab.com, by default this feature is not available.
To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `observability_tracing`.
The feature is not ready for production use.
This feature is not ready for production use.
With distributed tracing, you can troubleshoot application performance issues by inspecting how a request moves through different services and systems, the timing of each operation, and any errors or logs as they occur. Tracing is particularly useful in the context of microservice applications, which group multiple independent services collaborating to fulfill user requests.

View File

@ -209,7 +209,7 @@ DETAILS:
> - Push notification support [introduced](https://gitlab.com/gitlab-org/gitlab-shell/-/issues/506) in GitLab 15.3.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `two_factor_for_cli`. On GitLab.com and GitLab Dedicated, this feature is not available. The feature is not ready for production use. This feature flag also affects [session duration for Git Operations when 2FA is enabled](../administration/settings/account_and_limit_settings.md#customize-session-duration-for-git-operations-when-2fa-is-enabled).
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `two_factor_for_cli`. On GitLab.com and GitLab Dedicated, this feature is not available. This feature is not ready for production use. This feature flag also affects [session duration for Git Operations when 2FA is enabled](../administration/settings/account_and_limit_settings.md#customize-session-duration-for-git-operations-when-2fa-is-enabled).
You can enforce 2FA for [Git over SSH operations](../development/gitlab_shell/features.md#git-operations). However, you should use
[ED25519_SK](../user/ssh.md#ed25519_sk-ssh-keys) or [ECDSA_SK](../user/ssh.md#ecdsa_sk-ssh-keys) SSH keys instead. 2FA is enforced for Git operations only, and internal commands such as [`personal_access_token`](../development/gitlab_shell/features.md#personal-access-token) are excluded.

View File

@ -43,9 +43,9 @@ These integrations have to do with using GitLab to build application workloads a
AWS Services that are supported directly by a CodeStar Connection in an AWS account:
- **Amazon CodeWhisperer Customization Capability** ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) [can connect to a GitLab repository](https://aws.amazon.com/blogs/aws/new-customization-capability-in-amazon-codewhisperer-generates-even-better-suggestions-preview/). `[AWS Built]`
- **AWS Service Catalog** directly inherits CodeStar Connections, there is not any specific documentation about GitLab because it just uses any GitLab CodeStar Connection that has been created in the account. ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) `[AWS Built]`
- **AWS Proton** directly inherits CodeStar Connections, there is not any specific documentation about GitLab since it just uses any GitLab CodeStar Connection that has been created in the account. ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) `[AWS Built]`
- **AWS CodeBuild** - [for GitLab.com, self-managed and dedicated - click documentation tabs here](https://docs.aws.amazon.com/codebuild/latest/userguide/create-project-console.html#create-project-console-source). ([03/26/2023](https://aws.amazon.com/about-aws/whats-new/2024/03/aws-codebuild-gitlab-gitlab-self-managed/)) `[AWS Built]`
Documentation and References:
@ -58,7 +58,6 @@ Documentation and References:
AWS Services that are supported by an AWS CodePipeline integration:
- **AWS CodeBuild Integration** - through CodePipeline support. ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) `[AWS Built]`
- **Amazon SageMaker MLOps Projects** are created via CodePipeline ([as noted here](https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-projects-walkthrough-3rdgit.html#sagemaker-proejcts-walkthrough-connect-3rdgit)), there is not any specific documentation about GitLab since it just uses any GitLab CodeStar Connection that has been created in the account. ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) `[AWS Built]`
Documentation and References:

View File

@ -68,7 +68,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per group, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `epic_color_highlight`.
On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
On GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
When you create or edit an epic, you can select its color.
An epic's color is shown in [roadmaps](../roadmap/index.md), and [epic boards](epic_boards.md).
@ -259,7 +259,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available.
To make it available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `or_issuable_queries`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
When this feature is enabled, you can use the OR operator (**is one of: `||`**)
when you [filter the list of epics](#filter-the-list-of-epics) by:

View File

@ -19,7 +19,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project, an administrator can [enable the featured flag](../administration/feature_flags.md) named `okrs_mvc`.
On GitLab.com, by default this feature is not available, but can be configured by GitLab.com administrators.
On GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
[Objectives and key results](https://en.wikipedia.org/wiki/OKR) (OKRs) are a framework for setting
and tracking goals that are aligned with your organization's overall strategy and vision.
@ -354,7 +354,7 @@ To reorder them, drag them around.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `okr_checkin_reminders`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
Schedule check-in reminders to remind your team to provide status updates on the key results you care
about.

View File

@ -240,7 +240,7 @@ DETAILS:
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per user, an administrator can
[enable the feature flag](../../../administration/feature_flags.md) named `forti_token_cloud`. On GitLab.com and GitLab Dedicated, this
feature is not available. The feature is not ready for production use.
feature is not available. This feature is not ready for production use.
You can use FortiToken Cloud as a one-time password (OTP) provider in GitLab. Users must:

View File

@ -16,7 +16,7 @@ DETAILS:
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
an administrator can [enable the feature flag](../../administration/feature_flags.md) named `achievements`.
The feature is not ready for production use.
This feature is not ready for production use.
Achievements are a way to reward users for their activity on GitLab.
As a namespace maintainer or owner, you can create custom achievements for specific contributions, which you can award to or revoke from users based on your criteria.

View File

@ -679,7 +679,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an
administrator to [enable the feature flag](../../administration/feature_flags.md) named `board_multi_select`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
You can select multiple issue cards, then drag the group to another position within the list, or to
another list. This makes it faster to reorder many issues at once.

View File

@ -139,7 +139,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance,
an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `move_issue_children`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
When this feature is enabled, when you move an issue to another project, all its child tasks are also
moved to the target project and remain associated as child tasks on the moved issue.

View File

@ -15,7 +15,7 @@ DETAILS:
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115741) in GitLab 15.11.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com and GitLab Dedicated, this feature is available. The feature is not ready for production use.
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com and GitLab Dedicated, this feature is available. This feature is not ready for production use.
This tutorial shows you how to:

View File

@ -16,7 +16,7 @@ DETAILS:
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115741) in GitLab 15.11.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com and GitLab Dedicated, this feature is available. The feature is not ready for production use.
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com and GitLab Dedicated, this feature is available. This feature is not ready for production use.
You can use remote development to write and compile code hosted on GitLab.
With remote development, you can:

View File

@ -17,7 +17,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available.
To make it available, an administrator can [enable the feature flags](../../administration/feature_flags.md) named `index_code_with_zoekt` and `search_code_with_zoekt`.
On GitLab.com, this feature is available. On GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
WARNING:
This feature is in [Beta](../../policy/experiment-beta-support.md#beta) and subject to change without notice.
@ -37,7 +37,7 @@ FLAG:
On self-managed GitLab, by default this feature is available.
To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `zoekt_search_api`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
By default, the Zoekt search API is disabled on GitLab.com to avoid breaking changes.
@ -51,7 +51,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available.
To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `zoekt_cross_namespace_search`.
On GitLab.com and GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
Use this feature to search code across the entire GitLab instance.

View File

@ -124,7 +124,7 @@ FLAG:
On self-managed GitLab, by default the rich text feature is not available. To make it available per group, ask an
administrator
to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc`. On GitLab.com, this feature
is not available. The feature is not ready for production use.
is not available. This feature is not ready for production use.
Use a rich text editor to edit a task's description.

View File

@ -80,7 +80,7 @@ FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per user,
an administrator can [enable the feature flag](../administration/feature_flags.md) named `multiple_todos`.
On GitLab.com, this feature is available. On GitLab Dedicated, this feature is not available.
The feature is not ready for production use.
This feature is not ready for production use.
When you enable this feature:

View File

@ -48,12 +48,11 @@ module Keeps
file = File.expand_path("../#{filename}", __dir__)
full_file_content = File.read(file)
issue_url = flaky_issue['web_url']
file_lines = full_file_content.lines
return unless file_lines[line_number - 1].match?(EXAMPLE_LINE_REGEX)
file_lines[line_number - 1].sub!(EXAMPLE_LINE_REGEX, "\\1, quarantine: '#{issue_url}' do")
file_lines[line_number - 1].sub!(EXAMPLE_LINE_REGEX, "\\1, quarantine: '#{flaky_issue['web_url']}' do")
File.write(file, file_lines.join)
construct_change(filename, line_number, description, flaky_issue)
@ -91,7 +90,7 @@ module Keeps
- accept the merge request and schedule to improve the test
- close the merge request in favor of another merge request to delete the test
Related to #{issue_url}.
Related to #{flaky_issue['web_url']}.
MARKDOWN
group_label = flaky_issue['labels'].grep(/group::/).first

View File

@ -228,7 +228,7 @@ module API
end
def add_import_params(params)
params[:import_type] = 'git' if params[:import_url]&.present?
params[:import_type] = 'git' if params[:import_url].present?
params
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class UpdateSbomOccurrencesComponentNameBasedOnPep503 < BatchedMigrationJob
operation_name :update_occurrence_component_name_based_on_pep_503
feature_category :software_composition_analysis
def perform
each_sub_batch do |sub_batch|
update_occurrence_component_name(sub_batch)
end
end
private
def normalized_name(name)
connection.quote(name.gsub(Sbom::PackageUrl::Normalizer::PYPI_REGEX, '-'))
end
def update_occurrence_component_name(batch)
occurrences = batch
.joins("INNER JOIN sbom_components ON sbom_occurrences.component_id = sbom_components.id")
.where("sbom_components.purl_type = 8 AND sbom_occurrences.component_name LIKE '%.%'")
return if occurrences.blank?
values_list = occurrences.map do |occurrence|
"(#{occurrence.id}, #{normalized_name(occurrence.component_name)})"
end.join(", ")
sql = <<~SQL
WITH new_values (id, component_name) AS (
VALUES
#{values_list}
)
UPDATE sbom_occurrences
SET component_name = new_values.component_name
FROM new_values
WHERE sbom_occurrences.id = new_values.id
SQL
connection.execute(sql)
end
end
end
end

View File

@ -32,7 +32,7 @@ module Gitlab
# This is why for sanity check, we still want to make sure that there is no matching
# job artifact record in the database before we delete the object.
paths_with_job_artifact_records(objects.keys).each do |non_orphan_path|
log_skipping_object(non_orphan_path)
log_skipping_no_artifact_record(non_orphan_path)
objects.delete(non_orphan_path)
end
@ -48,15 +48,17 @@ module Gitlab
def each_fog_file
entries.each do |entry|
yield build_fog_file(entry)
end
end
# NOTE: If the object store is configured to use bucket prefix, the GenerateList task
# would have included the bucket_prefix in paths in the orphans list CSV.
path_with_bucket_prefix, _ = entry.split(',')
fog_file = artifacts_directory.files.get(path_with_bucket_prefix)
def build_fog_file(line)
# NOTE: If the object store is configured to use bucket prefix, the GenerateList task would have included the
# bucket_prefix in paths in the orphans list CSV.
path_with_bucket_prefix, size = line.split(',')
artifacts_directory.files.new(key: path_with_bucket_prefix, content_length: size)
if fog_file
yield fog_file
else
log_skipping_non_existent_object(path_with_bucket_prefix)
end
end
end
def path_without_bucket_prefix(path)
@ -79,9 +81,13 @@ module Gitlab
::Ci::JobArtifact.where(file_final_path: paths).pluck(:file_final_path) # rubocop:disable CodeReuse/ActiveRecord -- intentionally used pluck directly to keep it simple.
end
def log_skipping_object(path)
def log_skipping_no_artifact_record(path)
logger.info("Found job artifact record for object #{path}, skipping.")
end
def log_skipping_non_existent_object(path)
logger.info("No object found for #{path}, skipping.")
end
end
end
end

View File

@ -133,7 +133,8 @@ module Gitlab
end
def add_deleted_object_to_list(fog_file)
deleted_list_file.puts([fog_file.key, fog_file.content_length].join(','))
# We log the object's generation (GCP-only attribute) because we need this for the GCP rollback task if ever
deleted_list_file.puts([fog_file.key, fog_file.content_length, fog_file.try(:generation)].compact.join(','))
end
def log_info(msg)

View File

@ -0,0 +1,142 @@
# frozen_string_literal: true
module Gitlab
module Cleanup
module OrphanJobArtifactFinalObjects
class RollbackDeletedObjects
include StorageHelpers
UnsupportedProviderError = Class.new(StandardError)
GOOGLE_PROVIDER = 'google'
DEFAULT_DELETED_LIST_FILENAME = [
ProcessList::DELETED_LIST_FILENAME_PREFIX,
GenerateList::DEFAULT_FILENAME
].join.freeze
CURSOR_TRACKER_REDIS_KEY = 'orphan-job-artifact-objects-cleanup-rollback-cursor-tracker'
def initialize(filename: nil, force_restart: false, logger: Gitlab::AppLogger)
@force_restart = force_restart
@logger = logger
@filename = filename || DEFAULT_DELETED_LIST_FILENAME
end
def run!
ensure_supported_provider!
log_info("Processing #{filename}...")
initialize_file
each_fog_file do |fog_file|
rollback(fog_file)
end
log_info("Done. Rolled back deleted objects listed in #{filename}.")
ensure
file&.close
end
private
attr_reader :file, :filename, :force_restart, :logger
def ensure_supported_provider!
return if configuration.connection.provider.downcase == GOOGLE_PROVIDER
raise UnsupportedProviderError, 'Rollback mechanism only supports Google object store provider'
end
def initialize_file
@file = File.open(filename, 'r')
end
def each_fog_file
cursor_position = resume_from_last_cursor_position.to_i
file.pos = cursor_position
file.each do |line|
yield build_fog_file(line)
save_current_cursor_position(file.pos)
end
clear_last_cursor_position
end
def build_fog_file(line)
# NOTE: If the object store is configured to use bucket prefix, the ProcessList task would have included the
# bucket_prefix in paths in the deleted objects list CSV.
path_with_bucket_prefix, _, generation = line.split(',')
new_fog_file(path_with_bucket_prefix, generation.strip)
end
def new_fog_file(key, generation)
artifacts_directory.files.new(key: key, generation: generation)
end
def rollback(fog_file)
fog_file.copy(
fog_file.directory.key,
fog_file.key,
source_generation: fog_file.generation,
if_generation_match: 0 # Makes the request fail if there is aleady a live version
)
log_rolled_back_object(fog_file)
rescue Google::Apis::ClientError => error
raise error unless error.message.include?('conditionNotMet')
log_info("There is already a live version for object #{fog_file.key}, skipping.")
end
def resume_from_last_cursor_position
if force_restart
log_info("Force restarted. Will not resume from last known cursor position.")
nil
else
get_last_cursor_position
end
end
def get_last_cursor_position
Gitlab::Redis::SharedState.with do |redis|
position = redis.get(CURSOR_TRACKER_REDIS_KEY)
if position
log_info("Resuming from last cursor position: #{position}")
else
log_info("No last cursor position found, starting from beginning.")
end
position
end
end
def save_current_cursor_position(position)
Gitlab::Redis::SharedState.with do |redis|
# Set TTL to 1 week (86400 * 7 seconds)
redis.set(CURSOR_TRACKER_REDIS_KEY, position, ex: 604_800)
log_info("Saved current cursor position: #{position}")
end
end
def clear_last_cursor_position
Gitlab::Redis::SharedState.with do |redis|
redis.del(CURSOR_TRACKER_REDIS_KEY)
end
end
def log_rolled_back_object(fog_file)
log_info("Rolled back deleted object #{fog_file.key} to generation #{fog_file.generation}")
end
def log_info(msg)
logger.info(msg)
end
end
end
end
end

View File

@ -171,7 +171,10 @@ module Gitlab
::Ci::Build.transaction do
build = ::Ci::Build.new(importing: true, **build_attrs).tap(&:save!)
::Ci::RunningBuild.upsert_shared_runner_build!(build) if build.running? && build.shared_runner_build?
if build.running? &&
(Feature.enabled?(:add_all_ci_running_builds, build.project) || build.shared_runner_build?)
::Ci::RunningBuild.upsert_build!(build)
end
end
end

View File

@ -68,7 +68,7 @@ module ObjectStorage
workhorse_aws_hash
elsif config.azure?
workhorse_azure_hash
elsif Feature.enabled?(:workhorse_google_client, Feature.current_request) && config.google?
elsif config.google?
workhorse_google_hash
else
{}

View File

@ -123,7 +123,7 @@ module Sidebars
def google_oauth2_configured?
config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
config&.present? && config.app_id.present? && config.app_secret.present?
config.present? && config.app_id.present? && config.app_secret.present?
end
end
end

View File

@ -103,6 +103,22 @@ Usage: rake "gitlab:cleanup:list_orphan_job_artifact_final_objects[provider]")
processor.run!
end
desc 'GitLab | Cleanup | Rollback deleted final orphan job artifact objects (GCP only)'
task rollback_deleted_orphan_job_artifact_final_objects: :gitlab_environment do
warn_user_is_not_gitlab
force_restart = ENV['FORCE_RESTART'].present?
filename = ENV['FILENAME']
processor = Gitlab::Cleanup::OrphanJobArtifactFinalObjects::RollbackDeletedObjects.new(
force_restart: force_restart,
filename: filename,
logger: logger
)
processor.run!
end
desc 'GitLab | Cleanup | Clean orphan LFS file references'
task orphan_lfs_file_references: :gitlab_environment do
warn_user_is_not_gitlab

View File

@ -17222,6 +17222,9 @@ msgstr ""
msgid "Dependencies|There was a problem fetching vulnerabilities."
msgstr ""
msgid "Dependencies|There was an error fetching the components for this group. Please try again later."
msgstr ""
msgid "Dependencies|There was an error fetching the projects for this group. Please try again later."
msgstr ""
@ -39402,6 +39405,12 @@ msgstr ""
msgid "Project & Group can not be assigned at the same time"
msgstr ""
msgid "Project '%{name}' is being deleted."
msgstr ""
msgid "Project '%{name}' will be deleted on %{date}."
msgstr ""
msgid "Project '%{project_name}' is being imported."
msgstr ""
@ -44422,9 +44431,6 @@ msgstr ""
msgid "Runners|You may lose access to the runner token if you leave this page."
msgstr ""
msgid "Runners|You've created a new runner!"
msgstr ""
msgid "Runners|You've registered a new runner!"
msgstr ""
@ -52524,6 +52530,9 @@ msgstr ""
msgid "This project can be restored until %{date}."
msgstr ""
msgid "This project can be restored until %{date}. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "This project cannot be %{visibilityLevel} because the visibility of %{openShowLink}%{name}%{closeShowLink} is %{visibility}. To make this project %{visibilityLevel}, you must first %{openEditLink}change the visibility%{closeEditLink} of the parent group."
msgstr ""

View File

@ -7,8 +7,8 @@ module QA
class Show < QA::Page::Base
include Component::CiIcon
view 'app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue' do
element 'pipeline-details-header', required: true
view 'app/assets/javascripts/ci/pipeline_details/header/pipeline_header.vue' do
element 'pipeline-header', required: true
end
view 'app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue' do
@ -36,7 +36,7 @@ module QA
end
def running?(wait: 0)
within_element('pipeline-details-header') do
within_element('pipeline-header') do
page.has_content?('running', wait: wait)
end
end

View File

@ -3,8 +3,11 @@
module RuboCop
module Cop
class SidekiqApiUsage < RuboCop::Cop::Base
MSG = 'Refrain from directly using Sidekiq APIs.' \
'Only permitted in migrations, administrations and Sidekiq middlewares.'
MSG = 'Refrain from directly using Sidekiq APIs. ' \
'Only permitted in migrations, administrations and Sidekiq middlewares. ' \
'When disabling the cop, ensure that Sidekiq APIs are wrapped with ' \
'Sidekiq::Client.via(..) { ... } block to remain shard aware. ' \
'See doc/development/sidekiq/index.md#sharding for more information.'
ALLOWED_WORKER_METHODS = [
:skipping_transaction_check,
@ -12,6 +15,8 @@ module RuboCop
:raise_exception_for_being_inside_a_transaction?
].freeze
ALLOWED_CLIENT_METHODS = [:via].freeze
def_node_matcher :using_sidekiq_api?, <<~PATTERN
(send (const (const nil? :Sidekiq) $_ ) $... )
PATTERN
@ -23,6 +28,9 @@ module RuboCop
# allow methods defined in config/initializers/forbid_sidekiq_in_transactions.rb
next if klass == :Worker && ALLOWED_WORKER_METHODS.include?(methods_called[0])
# allow Sidekiq::Client.via calls
next if klass == :Client && ALLOWED_CLIENT_METHODS.include?(methods_called[0])
add_offense(node, message: MSG)
end
end

View File

@ -310,7 +310,7 @@ FactoryBot.define do
runner factory: :ci_runner
after(:create) do |build|
::Ci::RunningBuild.upsert_shared_runner_build!(build)
::Ci::RunningBuild.upsert_build!(build)
end
end

View File

@ -1,10 +0,0 @@
# frozen_string_literal: true
FactoryBot.define do
factory :merge_request_diff_llm_summary, class: 'MergeRequest::DiffLlmSummary' do
association :user, factory: :user
association :merge_request_diff, factory: :merge_request_diff
provider { 0 }
content { FFaker::Lorem.sentence }
end
end

View File

@ -41,7 +41,7 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
end
it 'contains commit short id' do
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_content pipeline.sha[0..7]
end
end

View File

@ -89,7 +89,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the pipeline information' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_content("For #{pipeline.ref}")
expect(page).to have_content("#{pipeline.statuses.count} jobs")
expect(page).to have_link(pipeline.ref,
@ -100,7 +100,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'displays pipeline name instead of commit title' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_content(pipeline.name)
expect(page).to have_content(project.commit.short_id)
expect(page).not_to have_selector('[data-testid="pipeline-commit-title"]')
@ -115,7 +115,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'displays commit title' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_content(project.commit.title)
expect(page).not_to have_selector('[data-testid="pipeline-name"]')
end
@ -140,7 +140,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows time ago' do
visit project_pipeline_path(project, finished_pipeline)
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_selector('[data-testid="pipeline-finished-time-ago"]')
end
end
@ -150,7 +150,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'does not show time ago' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).not_to have_selector('[data-testid="pipeline-finished-time-ago"]')
end
end
@ -237,7 +237,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
wait_for_requests
expect(page).not_to have_content('Retry job')
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
@ -291,7 +291,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
wait_for_requests
expect(page).not_to have_content('Retry job')
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
@ -325,7 +325,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
wait_for_requests
expect(page).not_to have_content('Play job')
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
@ -566,7 +566,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows running status in pipeline header', :sidekiq_might_not_need_inline do
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
end
@ -639,7 +639,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'does not render render raw HTML to the pipeline ref' do
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).not_to have_content('<span class="ref-name"')
end
end
@ -665,7 +665,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the pipeline information' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_content("#{pipeline.statuses.count} jobs")
expect(page).to have_content("Related merge request !#{merge_request.iid} " \
"to merge #{merge_request.source_branch}")
@ -684,7 +684,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'does not link to the source branch commit path' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).not_to have_link(merge_request.source_branch)
expect(page).to have_content(merge_request.source_branch)
end
@ -699,7 +699,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows the pipeline information', :sidekiq_might_not_need_inline do
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_content("#{pipeline.statuses.count} jobs")
expect(page).to have_content("Related merge request !#{merge_request.iid} " \
"to merge #{merge_request.source_branch}")
@ -736,7 +736,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows the pipeline information' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_content("#{pipeline.statuses.count} jobs")
expect(page).to have_content("Related merge request !#{merge_request.iid} " \
"to merge #{merge_request.source_branch} " \
@ -758,7 +758,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'does not link to the target branch commit path' do
visit_pipeline
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).not_to have_link(merge_request.target_branch)
expect(page).to have_content(merge_request.target_branch)
end
@ -773,7 +773,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'shows the pipeline information', :sidekiq_might_not_need_inline do
within_testid 'pipeline-details-header' do
within_testid 'pipeline-header' do
expect(page).to have_content("#{pipeline.statuses.count} jobs")
expect(page).to have_content("Related merge request !#{merge_request.iid} " \
"to merge #{merge_request.source_branch} " \
@ -904,6 +904,12 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
context 'when build requires resource', :sidekiq_inline do
before do
allow_next_instance_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService) do |resource_service|
allow(resource_service).to receive(:respawn_assign_resource_worker)
end
end
let_it_be(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@ -932,7 +938,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows deploy job as created' do
subject
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Pending')
end
@ -957,7 +963,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows deploy job as pending' do
subject
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
@ -986,7 +992,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows deploy job as waiting for resource' do
subject
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Waiting')
end
@ -1006,7 +1012,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows deploy job as pending' do
subject
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Running')
end
@ -1034,7 +1040,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'shows deploy job as waiting for resource' do
subject
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector('[data-testid="ci-icon"]', text: 'Waiting')
end
@ -1303,7 +1309,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'contains badge that indicates it is the latest build' do
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_content 'latest'
end
end
@ -1326,7 +1332,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'contains badge that indicates errors' do
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_content 'yaml invalid'
end
end
@ -1334,7 +1340,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'contains badge with tooltip which contains error' do
expect(pipeline).to have_yaml_errors
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector(
%(span[title="#{pipeline.yaml_errors}"]))
end
@ -1347,7 +1353,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
it 'contains badge with tooltip which contains failure reason' do
expect(pipeline.failure_reason?).to eq true
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_selector(
%(span[title="#{pipeline.present.failure_reason}"]))
end
@ -1365,7 +1371,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'contains badge that indicates being stuck' do
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_content 'stuck'
end
end
@ -1391,7 +1397,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'contains badge that indicates using auto devops' do
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_content 'Auto DevOps'
end
end
@ -1427,7 +1433,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
it 'contains badge that indicates detached merge request pipeline' do
within_testid('pipeline-details-header') do
within_testid('pipeline-header') do
expect(page).to have_content 'merge request'
end
end

View File

@ -5,7 +5,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PipelineDetailsHeader from '~/ci/pipeline_details/header/pipeline_details_header.vue';
import PipelineHeader from '~/ci/pipeline_details/header/pipeline_header.vue';
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import cancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql';
@ -28,7 +28,7 @@ import {
Vue.use(VueApollo);
describe('Pipeline details header', () => {
describe('Pipeline header', () => {
let wrapper;
const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess);
@ -93,7 +93,7 @@ describe('Pipeline details header', () => {
};
const createComponent = (handlers = defaultHandlers) => {
wrapper = shallowMountExtended(PipelineDetailsHeader, {
wrapper = shallowMountExtended(PipelineHeader, {
provide: {
...defaultProvideOptions,
},

View File

@ -9,7 +9,6 @@ import { updateHistory } from '~/lib/utils/url_utility';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
import AdminRegisterRunnerApp from '~/ci/runner/admin_register_runner/admin_register_runner_app.vue';
import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
import { runnerForRegistration } from '../mock_data';
const mockRunnerId = runnerForRegistration.data.runner.id;
@ -24,7 +23,6 @@ describe('AdminRegisterRunnerApp', () => {
let wrapper;
const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
const findBtn = () => wrapper.findComponent(GlButton);
const createComponent = () => {
@ -46,13 +44,8 @@ describe('AdminRegisterRunnerApp', () => {
expect(findRegistrationInstructions().props()).toEqual({
platform: DEFAULT_PLATFORM,
runnerId: mockRunnerId,
});
});
it('configures platform drawer', () => {
expect(findPlatformsDrawer().props()).toEqual({
open: false,
platform: DEFAULT_PLATFORM,
groupPath: null,
projectPath: null,
});
});
@ -63,7 +56,7 @@ describe('AdminRegisterRunnerApp', () => {
});
});
describe('When another platform has been selected', () => {
describe('When a platform is selected in the creation', () => {
beforeEach(() => {
setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
@ -75,48 +68,23 @@ describe('AdminRegisterRunnerApp', () => {
});
});
describe('When opening install instructions', () => {
describe('When a platform is selected in the instructions', () => {
beforeEach(async () => {
createComponent();
findRegistrationInstructions().vm.$emit('toggleDrawer');
findRegistrationInstructions().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
await nextTick();
});
it('opens platform drawer', () => {
expect(findPlatformsDrawer().props('open')).toBe(true);
it('updates the url', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
});
});
it('closes platform drawer', async () => {
findRegistrationInstructions().vm.$emit('toggleDrawer');
await nextTick();
expect(findPlatformsDrawer().props('open')).toBe(false);
});
it('closes platform drawer from drawer', async () => {
findPlatformsDrawer().vm.$emit('close');
await nextTick();
expect(findPlatformsDrawer().props('open')).toBe(false);
});
describe('when selecting a platform', () => {
beforeEach(async () => {
findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
await nextTick();
});
it('updates the url', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
});
});
it('updates the registration instructions', () => {
expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
});
it('updates the registration instructions', () => {
expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
});
});
});

View File

@ -2,18 +2,14 @@ import { GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import GoogleCloudRegistrationInstructions from '~/ci/runner/components/registration/google_cloud_registration_instructions.vue';
import GoogleCloudRegistrationInstructionsModal from '~/ci/runner/components/registration/google_cloud_registration_instructions_modal.vue';
import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
import provisionGoogleCloudRunnerQueryProject from '~/ci/runner/graphql/register/provision_google_cloud_runner_project.query.graphql';
import provisionGoogleCloudRunnerQueryGroup from '~/ci/runner/graphql/register/provision_google_cloud_runner_group.query.graphql';
import { STATUS_ONLINE } from '~/ci/runner/constants';
import {
runnerForRegistration,
mockAuthenticationToken,
projectRunnerCloudProvisioningSteps,
groupRunnerCloudProvisioningSteps,
@ -21,31 +17,6 @@ import {
Vue.use(VueApollo);
const mockRunnerResponse = {
data: {
runner: {
...runnerForRegistration.data.runner,
ephemeralAuthenticationToken: mockAuthenticationToken,
},
},
};
const mockRunnerWithoutTokenResponse = {
data: {
runner: {
...runnerForRegistration.data.runner,
ephemeralAuthenticationToken: null,
},
},
};
const mockRunnerOnlineResponse = {
data: {
runner: {
...runnerForRegistration.data.runner,
status: STATUS_ONLINE,
},
},
};
const mockProjectRunnerCloudSteps = {
data: {
project: {
@ -62,7 +33,8 @@ const mockGroupRunnerCloudSteps = {
},
};
const mockRunnerId = `${getIdFromGraphQLId(runnerForRegistration.data.runner.id)}`;
const mockGroupPath = 'test/group';
const mockProjectPath = 'test/project';
describe('GoogleCloudRegistrationInstructions', () => {
let wrapper;
@ -100,44 +72,43 @@ describe('GoogleCloudRegistrationInstructions', () => {
return waitForPromises();
};
const runnerWithTokenResolver = jest.fn().mockResolvedValue(mockRunnerResponse);
const runnerWithoutTokenResolver = jest.fn().mockResolvedValue(mockRunnerWithoutTokenResponse);
const runnerOnlineResolver = jest.fn().mockResolvedValue(mockRunnerOnlineResponse);
const projectInstructionsResolver = jest.fn().mockResolvedValue(mockProjectRunnerCloudSteps);
const groupInstructionsResolver = jest.fn().mockResolvedValue(mockGroupRunnerCloudSteps);
const errorResolver = jest
.fn()
.mockRejectedValue(new Error('GraphQL error: One or more validations have failed'));
const error = new Error('GraphQL error: One or more validations have failed');
const errorResolver = jest.fn().mockRejectedValue(error);
const defaultHandlers = [[runnerForRegistrationQuery, runnerWithTokenResolver]];
const defaultProps = {
runnerId: mockRunnerId,
projectPath: 'test/project',
};
const createComponent = (handlers = defaultHandlers, props = defaultProps) => {
const createComponent = ({ props = {}, handlers = [] } = {}) => {
wrapper = mountExtended(GoogleCloudRegistrationInstructions, {
apolloProvider: createMockApollo(handlers),
propsData: {
token: mockAuthenticationToken,
...props,
},
attachTo: document.body,
});
};
it('displays form inputs', () => {
it('displays runner token', async () => {
createComponent();
expect(findProjectIdInput().exists()).toBe(true);
expect(findRegionInput().exists()).toBe(true);
expect(findZoneInput().exists()).toBe(true);
expect(findMachineTypeInput().exists()).toBe(true);
await waitForPromises();
expect(findToken().exists()).toBe(true);
expect(findToken().text()).toBe(mockAuthenticationToken);
expect(findClipboardButton().exists()).toBe(true);
expect(findClipboardButton().props('text')).toBe(mockAuthenticationToken);
});
it('machine type input has a default value', () => {
createComponent();
it('does not display runner token', async () => {
createComponent({
props: { token: null },
});
expect(findMachineTypeInput().find('input').element.value).toEqual('n2d-standard-2');
await waitForPromises();
expect(findToken().exists()).toBe(false);
expect(findClipboardButton().exists()).toBe(false);
});
it('contains external docs links', () => {
@ -154,30 +125,19 @@ describe('GoogleCloudRegistrationInstructions', () => {
);
});
it('calls runner for registration query', () => {
it('displays form inputs', () => {
createComponent();
expect(runnerWithTokenResolver).toHaveBeenCalled();
expect(findProjectIdInput().exists()).toBe(true);
expect(findRegionInput().exists()).toBe(true);
expect(findZoneInput().exists()).toBe(true);
expect(findMachineTypeInput().exists()).toBe(true);
});
it('displays runner token', async () => {
it('machine type input has a default value', () => {
createComponent();
await waitForPromises();
expect(findToken().exists()).toBe(true);
expect(findToken().text()).toBe(mockAuthenticationToken);
expect(findClipboardButton().exists()).toBe(true);
expect(findClipboardButton().props('text')).toBe(mockAuthenticationToken);
});
it('does not display runner token', async () => {
createComponent([[runnerForRegistrationQuery, runnerWithoutTokenResolver]]);
await waitForPromises();
expect(findToken().exists()).toBe(false);
expect(findClipboardButton().exists()).toBe(false);
expect(findMachineTypeInput().find('input').element.value).toEqual('n2d-standard-2');
});
it('Shows an alert when the form has empty fields', async () => {
@ -194,60 +154,84 @@ describe('GoogleCloudRegistrationInstructions', () => {
);
});
it('Hides an alert when the form is valid', async () => {
createComponent([[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver]]);
describe('when fetching instructions for a project runner', () => {
beforeEach(async () => {
createComponent({
props: { projectPath: mockProjectPath },
handlers: [[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver]],
});
await fillInGoogleForm();
await fillInGoogleForm();
});
expect(findAlert().exists()).toBe(false);
});
it('Hides an alert when the form is valid', () => {
expect(findAlert().exists()).toBe(false);
});
it('Shows a modal with the correspondent scripts for a project', async () => {
createComponent([[provisionGoogleCloudRunnerQueryProject, projectInstructionsResolver]]);
it('Shows a modal with the correspondent scripts for a project', () => {
expect(projectInstructionsResolver).toHaveBeenCalled();
expect(groupInstructionsResolver).not.toHaveBeenCalled();
await fillInGoogleForm();
expect(projectInstructionsResolver).toHaveBeenCalled();
expect(groupInstructionsResolver).not.toHaveBeenCalled();
expect(findGoogleCloudInstructionsModal().props()).toEqual({
visible: true,
applyTerraformScript: 'mock project apply terraform script',
setupBashScript: 'mock project setup bash script',
setupTerraformFile: 'mock project setup terraform file',
expect(findGoogleCloudInstructionsModal().props()).toEqual({
visible: true,
applyTerraformScript: 'mock project apply terraform script',
setupBashScript: 'mock project setup bash script',
setupTerraformFile: 'mock project setup terraform file',
});
});
});
it('Shows a modal with the correspondent scripts for a group', async () => {
createComponent([[provisionGoogleCloudRunnerQueryGroup, groupInstructionsResolver]], {
runnerId: mockRunnerId,
groupPath: 'groups/test',
describe('when fetching instructions for a group runner', () => {
beforeEach(async () => {
createComponent({
props: { groupPath: mockGroupPath },
handlers: [[provisionGoogleCloudRunnerQueryGroup, groupInstructionsResolver]],
});
await fillInGoogleForm();
});
await fillInGoogleForm();
it('Hides an alert when the form is valid', () => {
expect(findAlert().exists()).toBe(false);
});
expect(groupInstructionsResolver).toHaveBeenCalled();
expect(projectInstructionsResolver).not.toHaveBeenCalled();
it('Shows a modal with the correspondent scripts for a group', () => {
expect(groupInstructionsResolver).toHaveBeenCalled();
expect(projectInstructionsResolver).not.toHaveBeenCalled();
expect(findGoogleCloudInstructionsModal().props()).toEqual({
visible: true,
applyTerraformScript: 'mock group apply terraform script',
setupBashScript: 'mock group setup bash script',
setupTerraformFile: 'mock group setup terraform file',
expect(findGoogleCloudInstructionsModal().props()).toEqual({
visible: true,
applyTerraformScript: 'mock group apply terraform script',
setupBashScript: 'mock group setup bash script',
setupTerraformFile: 'mock group setup terraform file',
});
});
});
it('Shows feedback when runner is online', async () => {
createComponent([[runnerForRegistrationQuery, runnerOnlineResolver]]);
describe('when fetching instructions fails', () => {
beforeEach(async () => {
createComponent({
props: { projectPath: mockProjectPath },
handlers: [[provisionGoogleCloudRunnerQueryProject, errorResolver]],
});
await waitForPromises();
expect(runnerOnlineResolver).toHaveBeenCalledTimes(1);
expect(runnerOnlineResolver).toHaveBeenCalledWith({
id: expect.stringContaining(mockRunnerId),
await fillInGoogleForm();
});
expect(wrapper.text()).toContain('Your runner is online');
it('Does not display a modal with text when validation errors occur', () => {
expect(errorResolver).toHaveBeenCalled();
expect(findAlert().text()).toContain(
'To view the setup instructions, make sure all form fields are completed and correct.',
);
expect(findGoogleCloudInstructionsModal().props()).toEqual({
visible: true,
applyTerraformScript: null,
setupBashScript: null,
setupTerraformFile: null,
});
});
});
describe('Field validation', () => {
@ -342,24 +326,5 @@ describe('GoogleCloudRegistrationInstructions', () => {
expectValidation(findMachineTypeInput(), { ariaInvalid, feedback });
});
});
it('Does not display a modal with text when validation errors occur', async () => {
createComponent([[provisionGoogleCloudRunnerQueryProject, errorResolver]]);
await fillInGoogleForm();
expect(errorResolver).toHaveBeenCalled();
expect(findAlert().text()).toContain(
'To view the setup instructions, make sure all form fields are completed and correct.',
);
expect(findGoogleCloudInstructionsModal().props()).toEqual({
visible: true,
applyTerraformScript: null,
setupBashScript: null,
setupTerraformFile: null,
});
});
});
});

View File

@ -10,9 +10,12 @@ import { extendedWrapper, shallowMountExtended } from 'helpers/vue_test_utils_he
import { TEST_HOST } from 'helpers/test_constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
import GoogleCloudRegistrationInstructions from '~/ci/runner/components/registration/google_cloud_registration_instructions.vue';
import {
DEFAULT_PLATFORM,
EXECUTORS_HELP_URL,
@ -20,7 +23,8 @@ import {
STATUS_NEVER_CONTACTED,
STATUS_ONLINE,
RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
I18N_REGISTRATION_SUCCESS,
WINDOWS_PLATFORM,
GOOGLE_CLOUD_PLATFORM,
} from '~/ci/runner/constants';
import { runnerForRegistration, mockAuthenticationToken } from '../../mock_data';
@ -43,6 +47,7 @@ describe('RegistrationInstructions', () => {
const findHeading = () => wrapper.find('h1');
const findStepAt = (i) => extendedWrapper(wrapper.findAll('section').at(i));
const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
const findByText = (text, container = wrapper) => container.findByText(text);
const waitForPolling = async () => {
@ -70,7 +75,7 @@ describe('RegistrationInstructions', () => {
});
};
const createComponent = (props) => {
const createComponent = ({ props = {}, ...options } = {}) => {
wrapper = shallowMountExtended(RegistrationInstructions, {
apolloProvider: createMockApollo([[runnerForRegistrationQuery, mockRunnerQuery]]),
propsData: {
@ -81,6 +86,7 @@ describe('RegistrationInstructions', () => {
stubs: {
GlSprintf,
},
...options,
});
};
@ -129,12 +135,26 @@ describe('RegistrationInstructions', () => {
});
});
it('renders legacy instructions', () => {
createComponent();
describe('platform installation drawer instructions', () => {
it('opens and closes the drawer', async () => {
createComponent();
findByText('How do I install GitLab Runner?').vm.$emit('click');
expect(findPlatformsDrawer().props('open')).toBe(false);
expect(wrapper.emitted('toggleDrawer')).toHaveLength(1);
await findByText('How do I install GitLab Runner?').vm.$emit('click');
expect(findPlatformsDrawer().props('open')).toBe(true);
await findPlatformsDrawer().vm.$emit('close');
expect(findPlatformsDrawer().props('open')).toBe(false);
});
it('selects a new platform from the drawer', () => {
createComponent();
findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
expect(wrapper.emitted('selectPlatform')).toEqual([[WINDOWS_PLATFORM]]);
});
});
describe('step 1', () => {
@ -276,7 +296,7 @@ describe('RegistrationInstructions', () => {
});
it('does not show success message', () => {
expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS);
expect(wrapper.text()).not.toContain('🎉');
});
describe('when the page is closing', () => {
@ -305,7 +325,7 @@ describe('RegistrationInstructions', () => {
it('shows success message', () => {
expect(wrapper.text()).toContain('🎉');
expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS);
expect(wrapper.text()).toContain("You've registered a new runner!");
});
describe('when the page is closing', () => {
@ -323,4 +343,81 @@ describe('RegistrationInstructions', () => {
});
});
});
describe('when using Google Cloud registration method', () => {
const findGoogleCloudRegistrationInstructions = () =>
wrapper.findComponent(GoogleCloudRegistrationInstructions);
it('passes a group path to the google instructions', async () => {
createComponent({
props: {
platform: GOOGLE_CLOUD_PLATFORM,
groupPath: 'mock/group/path',
},
provide: {
glFeatures: { googleCloudSupportFeatureFlag: true },
},
});
await waitForPromises();
expect(findGoogleCloudRegistrationInstructions().props()).toEqual({
token: mockAuthenticationToken,
groupPath: 'mock/group/path',
projectPath: null,
});
});
it('passes a project path to the google instructions', async () => {
createComponent({
props: {
platform: GOOGLE_CLOUD_PLATFORM,
projectPath: 'mock/project/path',
},
provide: {
glFeatures: { googleCloudSupportFeatureFlag: true },
},
});
await waitForPromises();
expect(findGoogleCloudRegistrationInstructions().props()).toEqual({
token: mockAuthenticationToken,
projectPath: 'mock/project/path',
groupPath: null,
});
});
it('does not show google instructions when disabled', async () => {
createComponent({
props: {
platform: GOOGLE_CLOUD_PLATFORM,
projectPath: 'mock/project/path',
},
provide: {
glFeatures: { googleCloudSupportFeatureFlag: false },
},
});
await waitForPromises();
expect(findGoogleCloudRegistrationInstructions().exists()).toBe(false);
});
it('does not show google instructions when on another platform', async () => {
createComponent({
props: {
platform: WINDOWS_PLATFORM,
projectPath: 'mock/project/path',
},
provide: {
glFeatures: { googleCloudSupportFeatureFlag: true },
},
});
await waitForPromises();
expect(findGoogleCloudRegistrationInstructions().exists()).toBe(false);
});
});
});

View File

@ -8,16 +8,9 @@ import { mockTracking } from 'helpers/tracking_helper';
import { InternalEvents } from '~/tracking';
import { updateHistory } from '~/lib/utils/url_utility';
import {
PARAM_KEY_PLATFORM,
DEFAULT_PLATFORM,
WINDOWS_PLATFORM,
GOOGLE_CLOUD_PLATFORM,
} from '~/ci/runner/constants';
import GoogleCloudRegistrationInstructions from '~/ci/runner/components/registration/google_cloud_registration_instructions.vue';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
import GroupRegisterRunnerApp from '~/ci/runner/group_register_runner/group_register_runner_app.vue';
import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
import { runnerForRegistration } from '../mock_data';
const mockRunnerId = runnerForRegistration.data.runner.id;
@ -33,10 +26,7 @@ describe('GroupRegisterRunnerApp', () => {
let wrapper;
let trackingSpy;
const findCloudRegistrationInstructions = () =>
wrapper.findComponent(GoogleCloudRegistrationInstructions);
const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
const findBtn = () => wrapper.findComponent(GlButton);
const createComponent = (googleCloudSupportFeatureFlag = false) => {
@ -65,13 +55,8 @@ describe('GroupRegisterRunnerApp', () => {
expect(findRegistrationInstructions().props()).toEqual({
platform: DEFAULT_PLATFORM,
runnerId: mockRunnerId,
});
});
it('configures platform drawer', () => {
expect(findPlatformsDrawer().props()).toEqual({
open: false,
platform: DEFAULT_PLATFORM,
groupPath: mockGroupPath,
projectPath: null,
});
});
@ -101,7 +86,7 @@ describe('GroupRegisterRunnerApp', () => {
});
});
describe('When another platform has been selected', () => {
describe('When a platform is selected in the creation', () => {
beforeEach(() => {
setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
@ -113,70 +98,23 @@ describe('GroupRegisterRunnerApp', () => {
});
});
describe('When opening install instructions', () => {
beforeEach(() => {
describe('When a platform is selected in the instructions', () => {
beforeEach(async () => {
createComponent();
findRegistrationInstructions().vm.$emit('toggleDrawer');
});
it('opens platform drawer', () => {
expect(findPlatformsDrawer().props('open')).toBe(true);
});
it('closes platform drawer', async () => {
findRegistrationInstructions().vm.$emit('toggleDrawer');
findRegistrationInstructions().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
await nextTick();
expect(findPlatformsDrawer().props('open')).toBe(false);
});
it('closes platform drawer from drawer', async () => {
findPlatformsDrawer().vm.$emit('close');
await nextTick();
expect(findPlatformsDrawer().props('open')).toBe(false);
});
describe('when selecting a platform', () => {
beforeEach(() => {
findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
});
it('updates the url', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
});
});
it('updates the registration instructions', () => {
expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
});
});
});
describe('Google cloud', () => {
describe('flag enabled', () => {
beforeEach(() => {
setWindowLocation(`?${PARAM_KEY_PLATFORM}=${GOOGLE_CLOUD_PLATFORM}`);
createComponent(true);
});
it('shows google cloud registration instructions', () => {
expect(findCloudRegistrationInstructions().exists()).toBe(true);
expect(findRegistrationInstructions().exists()).toBe(false);
it('updates the url', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
});
});
describe('flag disabled', () => {
it('does not show google cloud registration instructions', () => {
createComponent();
expect(findCloudRegistrationInstructions().exists()).toBe(false);
expect(findRegistrationInstructions().exists()).toBe(true);
});
it('updates the registration instructions', () => {
expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
});
});
});

View File

@ -8,16 +8,9 @@ import { mockTracking } from 'helpers/tracking_helper';
import { InternalEvents } from '~/tracking';
import { updateHistory } from '~/lib/utils/url_utility';
import {
PARAM_KEY_PLATFORM,
DEFAULT_PLATFORM,
WINDOWS_PLATFORM,
GOOGLE_CLOUD_PLATFORM,
} from '~/ci/runner/constants';
import GoogleCloudRegistrationInstructions from '~/ci/runner/components/registration/google_cloud_registration_instructions.vue';
import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
import ProjectRegisterRunnerApp from '~/ci/runner/project_register_runner/project_register_runner_app.vue';
import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
import { runnerForRegistration } from '../mock_data';
const mockRunnerId = runnerForRegistration.data.runner.id;
@ -33,10 +26,7 @@ describe('ProjectRegisterRunnerApp', () => {
let wrapper;
let trackingSpy;
const findCloudRegistrationInstructions = () =>
wrapper.findComponent(GoogleCloudRegistrationInstructions);
const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
const findBtn = () => wrapper.findComponent(GlButton);
const createComponent = (googleCloudSupportFeatureFlag = false) => {
@ -65,13 +55,8 @@ describe('ProjectRegisterRunnerApp', () => {
expect(findRegistrationInstructions().props()).toEqual({
platform: DEFAULT_PLATFORM,
runnerId: mockRunnerId,
});
});
it('configures platform drawer', () => {
expect(findPlatformsDrawer().props()).toEqual({
open: false,
platform: DEFAULT_PLATFORM,
projectPath: mockProjectPath,
groupPath: null,
});
});
@ -101,7 +86,7 @@ describe('ProjectRegisterRunnerApp', () => {
});
});
describe('When another platform has been selected', () => {
describe('When a platform is selected in the creation', () => {
beforeEach(() => {
setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
@ -113,70 +98,23 @@ describe('ProjectRegisterRunnerApp', () => {
});
});
describe('When opening install instructions', () => {
beforeEach(() => {
describe('When a platform is selected in the instructions', () => {
beforeEach(async () => {
createComponent();
findRegistrationInstructions().vm.$emit('toggleDrawer');
});
it('opens platform drawer', () => {
expect(findPlatformsDrawer().props('open')).toBe(true);
});
it('closes platform drawer', async () => {
findRegistrationInstructions().vm.$emit('toggleDrawer');
findRegistrationInstructions().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
await nextTick();
expect(findPlatformsDrawer().props('open')).toBe(false);
});
it('closes platform drawer from drawer', async () => {
findPlatformsDrawer().vm.$emit('close');
await nextTick();
expect(findPlatformsDrawer().props('open')).toBe(false);
});
describe('when selecting a platform', () => {
beforeEach(() => {
findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
});
it('updates the url', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
});
});
it('updates the registration instructions', () => {
expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
});
});
});
describe('Google cloud', () => {
describe('flag enabled', () => {
beforeEach(() => {
setWindowLocation(`?${PARAM_KEY_PLATFORM}=${GOOGLE_CLOUD_PLATFORM}`);
createComponent(true);
});
it('shows google cloud registration instructions', () => {
expect(findCloudRegistrationInstructions().exists()).toBe(true);
expect(findRegistrationInstructions().exists()).toBe(false);
it('updates the url', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
});
});
describe('flag disabled', () => {
it('does not show google cloud registration instructions', () => {
createComponent();
expect(findCloudRegistrationInstructions().exists()).toBe(false);
expect(findRegistrationInstructions().exists()).toBe(true);
});
it('updates the registration instructions', () => {
expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
});
});
});

View File

@ -5,7 +5,11 @@ import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql';
import { formatProjects } from '~/organizations/shared/utils';
import {
renderProjectDeleteSuccessToast,
deleteProjectParams,
formatProjects,
} from 'ee_else_ce/organizations/shared/utils';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { createAlert } from '~/alert';
@ -24,6 +28,16 @@ import {
jest.mock('~/alert');
jest.mock('~/api/projects_api');
const MOCK_DELETE_PARAMS = {
testParam: true,
};
jest.mock('ee_else_ce/organizations/shared/utils', () => ({
...jest.requireActual('ee_else_ce/organizations/shared/utils'),
renderProjectDeleteSuccessToast: jest.fn(),
deleteProjectParams: jest.fn(() => MOCK_DELETE_PARAMS),
}));
Vue.use(VueApollo);
describe('ProjectsView', () => {
@ -346,7 +360,8 @@ describe('ProjectsView', () => {
it('calls deleteProject, properly sets loading state, and refetches list when promise resolves', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
expect(deleteProject).toHaveBeenCalledWith(MOCK_PROJECT.id);
expect(deleteProjectParams).toHaveBeenCalledWith(MOCK_PROJECT);
expect(deleteProject).toHaveBeenCalledWith(MOCK_PROJECT.id, MOCK_DELETE_PARAMS);
expect(
findProjectsListProjectById(MOCK_PROJECT.id).actionLoadingStates[ACTION_DELETE],
).toBe(true);
@ -360,6 +375,13 @@ describe('ProjectsView', () => {
expect(successHandler).toHaveBeenCalledTimes(2);
});
it('does call renderProjectDeleteSuccessToast', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
await waitForPromises();
expect(renderProjectDeleteSuccessToast).toHaveBeenCalledWith(MOCK_PROJECT);
});
it('does not call createAlert', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
await waitForPromises();
@ -382,7 +404,8 @@ describe('ProjectsView', () => {
it('calls deleteProject, properly sets loading state, and shows error alert', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
expect(deleteProject).toHaveBeenCalledWith(MOCK_PROJECT.id);
expect(deleteProjectParams).toHaveBeenCalledWith(MOCK_PROJECT);
expect(deleteProject).toHaveBeenCalledWith(MOCK_PROJECT.id, MOCK_DELETE_PARAMS);
expect(
findProjectsListProjectById(MOCK_PROJECT.id).actionLoadingStates[ACTION_DELETE],
).toBe(true);
@ -401,6 +424,13 @@ describe('ProjectsView', () => {
captureError: true,
});
});
it('does not call renderProjectDeleteSuccessToast', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
await waitForPromises();
expect(renderProjectDeleteSuccessToast).not.toHaveBeenCalled();
});
});
});
});

View File

@ -1,8 +1,17 @@
import { formatProjects, formatGroups, onPageChange } from '~/organizations/shared/utils';
import {
formatProjects,
formatGroups,
onPageChange,
deleteProjectParams,
renderProjectDeleteSuccessToast,
} from '~/organizations/shared/utils';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import toast from '~/vue_shared/plugins/global_toast';
import { organizationProjects, organizationGroups } from '~/organizations/mock_data';
jest.mock('~/vue_shared/plugins/global_toast');
describe('formatProjects', () => {
it('correctly formats the projects', () => {
const [firstMockProject] = organizationProjects;
@ -122,3 +131,19 @@ describe('onPageChange', () => {
});
});
});
describe('renderProjectDeleteSuccessToast', () => {
const [MOCK_PROJECT] = formatProjects(organizationProjects);
it('calls toast correctly', () => {
renderProjectDeleteSuccessToast(MOCK_PROJECT);
expect(toast).toHaveBeenCalledWith(`Project '${MOCK_PROJECT.name}' is being deleted.`);
});
});
describe('deleteProjectParams', () => {
it('returns {} always', () => {
expect(deleteProjectParams()).toStrictEqual({});
});
});

Some files were not shown because too many files have changed in this diff Show More