Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bba263d1a3
commit
c449ce987e
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}.')">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
2645479cd4ce377b4183c1fade37050a2b143d9697f341c690483a11a202bde4
|
||||
|
|
@ -0,0 +1 @@
|
|||
c24c3c941355f6f75f16bb73544581a83db9e996cffcb7f40d44914f42944751
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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/).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue