Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-14 15:09:57 +00:00
parent 0b194c4854
commit b689f37135
99 changed files with 1411 additions and 555 deletions

View File

@ -25,7 +25,7 @@ review-build-cng:
extends:
- .default-retry
- .review:rules:review-build-cng
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13
stage: review-prepare
before_script:
- source ./scripts/utils.sh

View File

@ -52,7 +52,7 @@ no_ee_check:
verify-tests-yml:
extends:
- .setup:rules:verify-tests-yml
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13
stage: test
needs: []
script:

View File

@ -280,6 +280,22 @@ GitlabSecurity/PublicSend:
- 'ee/lib/**/*.rake'
- 'ee/spec/**/*'
Database/MultipleDatabases:
Enabled: true
Include:
- 'app/**/*.rb'
- 'ee/app/**/*.rb'
- 'lib/**/*.rb'
- 'ee/lib/**/*.rb'
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Exclude:
- 'ee/db/**/*.rb'
- 'spec/migrations/**/*.rb'
- 'lib/gitlab/background_migration/**/*.rb'
- 'spec/lib/gitlab/background_migration/**/*.rb'
- 'spec/lib/gitlab/database/**/*.rb'
Gitlab/DuplicateSpecLocation:
Enabled: true

View File

@ -2201,8 +2201,8 @@ Cop/UserAdmin:
- 'ee/lib/ee/gitlab/git_access.rb'
- 'lib/api/award_emoji.rb'
- 'lib/api/ci/runners.rb'
- 'lib/api/entities/runner_details.rb'
- 'lib/api/entities/user_safe.rb'
- 'lib/api/entities/ci/runner_details.rb'
- 'lib/api/entities/ci/user_safe.rb'
- 'lib/api/groups.rb'
- 'lib/api/helpers.rb'
- 'lib/api/personal_access_tokens.rb'
@ -2465,3 +2465,170 @@ Style/RegexpLiteralMixedPreserve:
- 'lib/gitlab/regex.rb'
- 'lib/gitlab/utils.rb'
- 'lib/product_analytics/tracker.rb'
- 'qa/qa/page/project/settings/advanced.rb'
- 'qa/spec/service/docker_run/gitlab_runner_spec.rb'
- 'rubocop/cop/gitlab/duplicate_spec_location.rb'
- 'spec/features/clusters/cluster_health_dashboard_spec.rb'
- 'spec/features/markdown/metrics_spec.rb'
- 'spec/features/search/user_searches_for_code_spec.rb'
- 'spec/features/snippets/embedded_snippet_spec.rb'
- 'spec/helpers/diff_helper_spec.rb'
- 'spec/helpers/releases_helper_spec.rb'
- 'spec/lib/gitlab/ci/reports/test_case_spec.rb'
- 'spec/lib/gitlab/consul/internal_spec.rb'
- 'spec/lib/gitlab/import_export/shared_spec.rb'
- 'spec/lib/gitlab/utils/usage_data_spec.rb'
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
- 'spec/requests/api/projects_spec.rb'
- 'spec/services/jira/requests/projects/list_service_spec.rb'
- 'spec/support/capybara.rb'
- 'spec/support/helpers/grafana_api_helpers.rb'
- 'spec/support/helpers/query_recorder.rb'
- 'spec/support/helpers/require_migration.rb'
- 'spec/views/layouts/_head.html.haml_spec.rb'
Database/MultipleDatabases:
Exclude:
- 'app/mailers/previews/notify_preview.rb'
- 'app/models/application_setting.rb'
- 'app/models/internal_id.rb'
- 'app/services/auto_merge/base_service.rb'
- 'app/services/ci/delete_unit_tests_service.rb'
- 'app/services/ci/unlock_artifacts_service.rb'
- 'app/services/deployments/update_environment_service.rb'
- 'app/services/design_management/copy_design_collection/copy_service.rb'
- 'app/services/feature_flags/create_service.rb'
- 'app/services/feature_flags/destroy_service.rb'
- 'app/services/feature_flags/update_service.rb'
- 'app/services/issuable/clone/base_service.rb'
- 'app/services/issuable/common_system_notes_service.rb'
- 'app/services/issuable/destroy_label_links_service.rb'
- 'app/services/packages/create_dependency_service.rb'
- 'app/services/packages/go/create_package_service.rb'
- 'app/services/packages/npm/create_package_service.rb'
- 'app/services/packages/terraform_module/create_package_service.rb'
- 'app/services/projects/cleanup_service.rb'
- 'app/services/projects/fetch_statistics_increment_service.rb'
- 'app/services/releases/update_service.rb'
- 'app/services/todos/destroy/destroyed_issuable_service.rb'
- 'ee/app/models/dora/daily_metrics.rb'
- 'ee/app/services/analytics/devops_adoption/enabled_namespaces/bulk_delete_service.rb'
- 'ee/app/services/approval_rules/finalize_service.rb'
- 'ee/app/services/approval_rules/project_rule_destroy_service.rb'
- 'ee/app/services/app_sec/dast/site_profiles/create_service.rb'
- 'ee/app/services/app_sec/dast/site_profiles/update_service.rb'
- 'ee/app/services/ci/minutes/update_build_minutes_service.rb'
- 'ee/app/services/ee/issuable/common_system_notes_service.rb'
- 'ee/app/services/group_saml/group_managed_accounts/transfer_membership_service.rb'
- 'ee/app/services/group_saml/sign_up_service.rb'
- 'ee/app/services/iterations/roll_over_issues_service.rb'
- 'ee/app/services/security/store_scan_service.rb'
- 'ee/app/services/timebox_report_service.rb'
- 'ee/app/services/vulnerability_feedback/create_service.rb'
- 'ee/lib/ee/gitlab/checks/push_rule_check.rb'
- 'ee/lib/ee/gitlab/database.rb'
- 'ee/lib/gitlab/geo/database_tasks.rb'
- 'ee/lib/gitlab/geo/geo_tasks.rb'
- 'ee/lib/gitlab/geo/health_check.rb'
- 'ee/lib/gitlab/geo/log_cursor/daemon.rb'
- 'ee/lib/pseudonymizer/dumper.rb'
- 'ee/lib/pseudonymizer/pager.rb'
- 'ee/lib/system_check/geo/geo_database_configured_check.rb'
- 'ee/spec/lib/pseudonymizer/dumper_spec.rb'
- 'ee/spec/models/pg_replication_slot_spec.rb'
- 'ee/spec/services/ee/merge_requests/update_service_spec.rb'
- 'lib/backup/database.rb'
- 'lib/after_commit_queue.rb'
- 'lib/api/rubygem_packages.rb'
- 'lib/backup/manager.rb'
- 'lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb'
- 'lib/gitlab/chaos.rb'
- 'lib/gitlab/current_settings.rb'
- 'lib/gitlab/database/batch_count.rb'
- 'lib/gitlab/database/batch_counter.rb'
- 'lib/gitlab/database/count/reltuples_count_strategy.rb'
- 'lib/gitlab/database/count/tablesample_count_strategy.rb'
- 'lib/gitlab/database/grant.rb'
- 'lib/gitlab/database/load_balancing/load_balancer.rb'
- 'lib/gitlab/database/load_balancing.rb'
- 'lib/gitlab/database/load_balancing/sticking.rb'
- 'lib/gitlab/database/migrations/observers/migration_observer.rb'
- 'lib/gitlab/database/migrations/observers/query_log.rb'
- 'lib/gitlab/database/multi_threaded_migration.rb'
- 'lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb'
- 'lib/gitlab/database/partitioning/monthly_strategy.rb'
- 'lib/gitlab/database/partitioning/partition_manager.rb'
- 'lib/gitlab/database/partitioning/partition_creator.rb'
- 'lib/gitlab/database/partitioning/replace_table.rb'
- 'lib/gitlab/database/partitioning/time_partition.rb'
- 'lib/gitlab/database/postgres_hll/batch_distinct_counter.rb'
- 'lib/gitlab/database/postgresql_adapter/dump_schema_versions_mixin.rb'
- 'lib/gitlab/database/postgresql_database_tasks/load_schema_versions_mixin.rb'
- 'lib/gitlab/database.rb'
- 'lib/gitlab/database/reindexing/concurrent_reindex.rb'
- 'lib/gitlab/database/reindexing/reindex_concurrently.rb'
- 'lib/gitlab/database/schema_cache_with_renamed_table.rb'
- 'lib/gitlab/database/schema_migrations/context.rb'
- 'lib/gitlab/database/schema_version_files.rb'
- 'lib/gitlab/database/similarity_score.rb'
- 'lib/gitlab/database/unidirectional_copy_trigger.rb'
- 'lib/gitlab/database/with_lock_retries.rb'
- 'lib/gitlab/gitlab_import/importer.rb'
- 'lib/gitlab/health_checks/db_check.rb'
- 'lib/gitlab/import_export/base/relation_factory.rb'
- 'lib/gitlab/import_export/relation_tree_restorer.rb'
- 'lib/gitlab/legacy_github_import/importer.rb'
- 'lib/gitlab/metrics/samplers/database_sampler.rb'
- 'lib/gitlab/optimistic_locking.rb'
- 'lib/gitlab/otp_key_rotator.rb'
- 'lib/gitlab/profiler.rb'
- 'lib/gitlab/seeder.rb'
- 'lib/gitlab/sherlock/query.rb'
- 'lib/gitlab/sql/glob.rb'
- 'lib/gitlab/sql/set_operator.rb'
- 'lib/system_check/orphans/repository_check.rb'
- 'spec/db/schema_spec.rb'
- 'spec/features/admin/dashboard_spec.rb'
- 'spec/initializers/database_config_spec.rb'
- 'spec/initializers/lograge_spec.rb'
- 'spec/lib/backup/manager_spec.rb'
- 'spec/lib/gitlab/current_settings_spec.rb'
- 'spec/lib/gitlab/database_spec.rb'
- 'spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb'
- 'spec/lib/gitlab/import_export/project/tree_saver_spec.rb'
- 'spec/lib/gitlab/metrics/subscribers/active_record_spec.rb'
- 'spec/lib/gitlab/pagination/keyset/order_spec.rb'
- 'spec/lib/gitlab/profiler_spec.rb'
- 'spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb'
- 'spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb'
- 'spec/lib/gitlab/sql/cte_spec.rb'
- 'spec/lib/gitlab/sql/glob_spec.rb'
- 'spec/lib/gitlab/sql/recursive_cte_spec.rb'
- 'spec/lib/gitlab/usage_data_metrics_spec.rb'
- 'spec/lib/gitlab/usage_data_queries_spec.rb'
- 'spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/constraints_spec.rb'
- 'spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb'
- 'spec/lib/gitlab/utils/usage_data_spec.rb'
- 'spec/models/application_setting_spec.rb'
- 'spec/models/concerns/case_sensitivity_spec.rb'
- 'spec/models/concerns/sortable_spec.rb'
- 'spec/models/concerns/where_composite_spec.rb'
- 'spec/models/experiment_spec.rb'
- 'spec/models/internal_id_spec.rb'
- 'spec/models/project_feature_usage_spec.rb'
- 'spec/models/users_statistics_spec.rb'
- 'spec/requests/api/statistics_spec.rb'
- 'spec/services/users/activity_service_spec.rb'
- 'spec/support/caching.rb'
- 'spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb'
- 'spec/support/helpers/database_connection_helpers.rb'
- 'spec/support/helpers/database/database_helpers.rb'
- 'spec/support/helpers/database/table_schema_helpers.rb'
- 'spec/support/helpers/migrations_helpers.rb'
- 'spec/support/helpers/query_recorder.rb'
- 'spec/support/helpers/usage_data_helpers.rb'
- 'spec/tasks/gitlab/backup_rake_spec.rb'
- 'spec/tasks/gitlab/db_rake_spec.rb'
- 'spec/workers/analytics/usage_trends/counter_job_worker_spec.rb'
- 'spec/workers/users/create_statistics_worker_spec.rb'

View File

@ -62,6 +62,7 @@ export default {
projects: [],
selectedProjects: this.defaultProjects || [],
searchTerm: '',
isDirty: false,
};
},
computed: {
@ -124,6 +125,24 @@ export default {
this.setSelectedProjects(project, !isSelected);
this.$emit('selected', this.selectedProjects);
},
onMultiSelectClick({ project, isSelected }) {
this.setSelectedProjects(project, !isSelected);
this.isDirty = true;
},
onSelected(ev) {
if (this.multiSelect) {
this.onMultiSelectClick(ev);
} else {
this.onClick(ev);
}
},
onHide() {
if (this.multiSelect && this.isDirty) {
this.$emit('selected', this.selectedProjects);
}
this.searchTerm = '';
this.isDirty = false;
},
fetchData() {
this.loading = true;
@ -158,12 +177,12 @@ export default {
},
};
</script>
<template>
<gl-dropdown
ref="projectsDropdown"
class="dropdown dropdown-projects"
toggle-class="gl-shadow-none"
@hide="onHide"
>
<template #button-content>
<div class="gl-display-flex gl-flex-grow-1">
@ -181,15 +200,18 @@ export default {
</div>
<gl-icon class="gl-ml-2" name="chevron-down" />
</template>
<gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
<gl-search-box-by-type v-model.trim="searchTerm" />
<template #header>
<gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
<gl-search-box-by-type v-model.trim="searchTerm" />
</template>
<gl-dropdown-item
v-for="project in availableProjects"
:key="project.id"
:is-check-item="true"
:is-checked="isProjectSelected(project.id)"
@click.prevent="onClick({ project, isSelected: isProjectSelected(project.id) })"
@click.native.capture.stop="
onSelected({ project, isSelected: isProjectSelected(project.id) })
"
>
<div class="gl-display-flex">
<gl-avatar
@ -203,7 +225,9 @@ export default {
/>
<div>
<div data-testid="project-name">{{ project.name }}</div>
<div class="gl-text-gray-500" data-testid="project-full-path">{{ project.fullPath }}</div>
<div class="gl-text-gray-500" data-testid="project-full-path">
{{ project.fullPath }}
</div>
</div>
</div>
</gl-dropdown-item>

View File

@ -35,13 +35,23 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, boardId, fullBoardId, fullPath, boardConfig, issuableType } = data;
const {
allowSubEpics,
boardConfig,
boardId,
boardType,
disabled,
fullBoardId,
fullPath,
issuableType,
} = data;
state.allowSubEpics = allowSubEpics;
state.boardConfig = boardConfig;
state.boardId = boardId;
state.fullBoardId = fullBoardId;
state.fullPath = fullPath;
state.boardType = boardType;
state.disabled = disabled;
state.boardConfig = boardConfig;
state.fullBoardId = fullBoardId;
state.fullPath = fullPath;
state.issuableType = issuableType;
},

View File

@ -47,12 +47,6 @@ export default {
},
},
computed: {
variant() {
if (this.disabled) {
return 'default';
}
return 'danger';
},
title() {
if (this.isProtectedBranch && this.disabled) {
return s__('Branches|Only a project maintainer or owner can delete a protected branch');
@ -83,7 +77,7 @@ export default {
class="js-delete-branch-button"
data-qa-selector="delete_branch_button"
:disabled="disabled"
:variant="variant"
variant="default"
:title="title"
:aria-label="title"
@click="openModal"

View File

@ -0,0 +1,110 @@
<script>
import {
GlDropdown,
GlDropdownForm,
GlButton,
GlFormInputGroup,
GlDropdownDivider,
GlDropdownItem,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { acceptedMimes } from '../extensions/image';
import { getImageAlt } from '../services/utils';
export default {
components: {
GlDropdown,
GlDropdownForm,
GlFormInputGroup,
GlDropdownDivider,
GlDropdownItem,
GlButton,
},
directives: {
GlTooltip,
},
props: {
tiptapEditor: {
type: TiptapEditor,
required: true,
},
},
data() {
return {
imgSrc: '',
};
},
methods: {
resetFields() {
this.imgSrc = '';
this.$refs.fileSelector.value = '';
},
insertImage() {
this.tiptapEditor
.chain()
.focus()
.setImage({
src: this.imgSrc,
canonicalSrc: this.imgSrc,
alt: getImageAlt(this.imgSrc),
})
.run();
this.resetFields();
this.emitExecute();
},
emitExecute(source = 'url') {
this.$emit('execute', { contentType: 'image', value: source });
},
openFileUpload() {
this.$refs.fileSelector.click();
},
onFileSelect(e) {
this.tiptapEditor
.chain()
.focus()
.uploadImage({
file: e.target.files[0],
})
.run();
this.resetFields();
this.emitExecute('upload');
},
},
acceptedMimes,
};
</script>
<template>
<gl-dropdown
v-gl-tooltip
:aria-label="__('Insert image')"
:title="__('Insert image')"
size="small"
category="tertiary"
icon="media"
@hidden="resetFields()"
>
<gl-dropdown-form class="gl-px-3!">
<gl-form-input-group v-model="imgSrc" :placeholder="__('Image URL')">
<template #append>
<gl-button variant="confirm" @click="insertImage">{{ __('Insert') }}</gl-button>
</template>
</gl-form-input-group>
</gl-dropdown-form>
<gl-dropdown-divider />
<gl-dropdown-item @click="openFileUpload">
{{ __('Upload image') }}
</gl-dropdown-item>
<input
ref="fileSelector"
type="file"
name="content_editor_image"
:accept="$options.acceptedMimes"
class="gl-display-none"
@change="onFileSelect"
/>
</gl-dropdown>
</template>

View File

@ -43,7 +43,7 @@ export default {
},
mounted() {
this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
const { 'data-canonical-src': canonicalSrc, href } = editor.getAttributes(linkContentType);
const { canonicalSrc, href } = editor.getAttributes(linkContentType);
this.linkHref = canonicalSrc || href;
});
@ -56,7 +56,7 @@ export default {
.unsetLink()
.setLink({
href: this.linkHref,
'data-canonical-src': this.linkHref,
canonicalSrc: this.linkHref,
})
.run();

View File

@ -4,6 +4,7 @@ import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '
import { ContentEditor } from '../services/content_editor';
import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue';
import ToolbarLinkButton from './toolbar_link_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
@ -18,6 +19,7 @@ export default {
ToolbarTextStyleDropdown,
ToolbarLinkButton,
ToolbarTableButton,
ToolbarImageButton,
Divider,
},
mixins: [trackingMixin],
@ -89,6 +91,12 @@ export default {
@execute="trackToolbarControlExecution"
/>
<divider />
<toolbar-image-button
ref="imageButton"
data-testid="image"
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="blockquote"
content-type="blockquote"
@ -140,3 +148,8 @@ export default {
/>
</div>
</template>
<style>
.gl-spinner-container {
text-align: left;
}
</style>

View File

@ -38,11 +38,11 @@ export const tiptapExtension = Link.extend({
};
},
},
'data-canonical-src': {
canonicalSrc: {
default: null,
parseHTML: (element) => {
return {
href: element.dataset.canonicalSrc,
canonicalSrc: element.dataset.canonicalSrc,
};
},
},
@ -57,7 +57,7 @@ export const serializer = {
return '[';
},
close(state, mark) {
const href = mark.attrs['data-canonical-src'] || mark.attrs.href;
const href = mark.attrs.canonicalSrc || mark.attrs.href;
return `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
},
};

View File

@ -0,0 +1,40 @@
<script>
import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlSearchBoxByType,
},
inheritAttrs: false,
props: {
namespaces: {
type: Array,
required: true,
},
},
data() {
return { searchTerm: '' };
},
computed: {
filteredNamespaces() {
return this.namespaces.filter((ns) =>
ns.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
},
};
</script>
<template>
<gl-dropdown
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1"
data-qa-selector="target_namespace_selector_dropdown"
v-bind="$attrs"
>
<template #header>
<gl-search-box-by-type v-model.trim="searchTerm" />
</template>
<slot :namespaces="filteredNamespaces"></slot>
</gl-dropdown>
</template>

View File

@ -1,7 +1,6 @@
<script>
import {
GlButton,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
@ -11,6 +10,7 @@ import {
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql';
@ -22,8 +22,8 @@ const DEBOUNCE_INTERVAL = 300;
export default {
components: {
ImportStatus,
ImportGroupDropdown,
GlButton,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
@ -83,6 +83,10 @@ export default {
},
computed: {
availableNamespaceNames() {
return this.availableNamespaces.map((ns) => ns.full_path);
},
importTarget() {
return this.group.import_target;
},
@ -153,9 +157,11 @@ export default {
disabled: isAlreadyImported,
}"
>
<gl-dropdown
<import-group-dropdown
#default="{ namespaces }"
:text="importTarget.target_namespace"
:disabled="isAlreadyImported"
:namespaces="availableNamespaceNames"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="import-entities-namespace-dropdown gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
@ -163,22 +169,22 @@ export default {
<gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
s__('BulkImport|No parent')
}}</gl-dropdown-item>
<template v-if="availableNamespaces.length">
<template v-if="namespaces.length">
<gl-dropdown-divider />
<gl-dropdown-section-header>
{{ s__('BulkImport|Existing groups') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="ns in availableNamespaces"
:key="ns.full_path"
v-for="ns in namespaces"
:key="ns"
data-qa-selector="target_group_dropdown_item"
:data-qa-group-name="ns.full_path"
@click="$emit('update-target-namespace', ns.full_path)"
:data-qa-group-name="ns"
@click="$emit('update-target-namespace', ns)"
>
{{ ns.full_path }}
{{ ns }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</import-group-dropdown>
<div
class="import-entities-target-select-separator gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
>

View File

@ -47,18 +47,7 @@ export default {
},
availableNamespaces() {
const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({
id: fullPath,
text: fullPath,
}));
return [
{ text: __('Groups'), children: serializedNamespaces },
{
text: __('Users'),
children: [{ id: this.defaultTargetNamespace, text: this.defaultTargetNamespace }],
},
];
return this.namespaces.map(({ fullPath }) => fullPath);
},
importAllButtonText() {
@ -179,6 +168,7 @@ export default {
:key="repo.importSource.providerLink"
:repo="repo"
:available-namespaces="availableNamespaces"
:user-namespace="defaultTargetNamespace"
/>
</template>
</tbody>

View File

@ -1,8 +1,17 @@
<script>
import { GlIcon, GlBadge, GlFormInput, GlButton, GlLink } from '@gitlab/ui';
import {
GlIcon,
GlBadge,
GlFormInput,
GlButton,
GlLink,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
} from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
@ -10,10 +19,13 @@ import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
export default {
name: 'ProviderRepoTableRow',
components: {
Select2Select,
ImportGroupDropdown,
ImportStatus,
GlFormInput,
GlButton,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
GlIcon,
GlBadge,
GlLink,
@ -23,6 +35,10 @@ export default {
type: Object,
required: true,
},
userNamespace: {
type: String,
required: true,
},
availableNamespaces: {
type: Array,
required: true,
@ -61,22 +77,6 @@ export default {
return this.ciCdOnly ? __('Connect') : __('Import');
},
select2Options() {
return {
data: this.availableNamespaces,
containerCssClass: 'import-namespace-select qa-project-namespace-select gl-w-auto',
};
},
targetNamespaceSelect: {
get() {
return this.importTarget.targetNamespace;
},
set(value) {
this.updateImportTarget({ targetNamespace: value });
},
},
newNameInput: {
get() {
return this.importTarget.newName;
@ -118,7 +118,29 @@ export default {
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted">
<div class="import-entities-target-select gl-display-flex gl-align-items-stretch gl-w-full">
<select2-select v-model="targetNamespaceSelect" :options="select2Options" />
<import-group-dropdown
#default="{ namespaces }"
:text="importTarget.targetNamespace"
:namespaces="availableNamespaces"
>
<template v-if="namespaces.length">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="ns in namespaces"
:key="ns"
data-qa-selector="target_group_dropdown_item"
:data-qa-group-name="ns"
@click="updateImportTarget({ targetNamespace: ns })"
>
{{ ns }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="updateImportTarget({ targetNamespace: ns })">{{
userNamespace
}}</gl-dropdown-item>
</import-group-dropdown>
<div
class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
>

View File

@ -38,7 +38,7 @@ export function initStoreFromElement(element) {
export function initPropsFromElement(element) {
return {
providerTitle: element.dataset.providerTitle,
providerTitle: element.dataset.provider,
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
};

View File

@ -1,18 +1,27 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { GlAlert, GlLink, GlSprintf, GlIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
export default {
i18n: {
viewOnlyMessage: s__('Pipelines|Merged YAML is view only'),
unavailableDefaultTitle: s__('Pipelines|Merged YAML unavailable'),
unavailableDefaultText: s__(
'Pipelines|The merged YAML view is only available for the default branch. %{linkStart}Learn more.%{linkEnd}',
),
},
components: {
SourceEditor,
GlAlert,
GlIcon,
GlLink,
GlSprintf,
},
inject: ['ciConfigPath'],
inject: ['ciConfigPath', 'defaultBranch'],
props: {
ciConfigData: {
type: Object,
@ -24,6 +33,15 @@ export default {
failureType: null,
};
},
// This is not the best practice, don't copy me (@samdbeckham)
// This is a temporary workaround to unblock a release.
// See this comment for more information on this approach
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65972#note_626095648
apollo: {
currentBranch: {
query: getCurrentBranch,
},
},
computed: {
fileGlobalId() {
return `${this.ciConfigPath}-${uniqueId()}`;
@ -31,24 +49,44 @@ export default {
mergedYaml() {
return this.ciConfigData.mergedYaml;
},
isOnDefaultBranch() {
return this.currentBranch === this.defaultBranch;
},
expandedConfigHelpPath() {
return helpPagePath('ci/pipeline_editor/index', { anchor: 'view-expanded-configuration' });
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-align-items-center">
<gl-icon :size="16" name="lock" class="gl-text-gray-500 gl-mr-3" />
{{ $options.i18n.viewOnlyMessage }}
</div>
<div class="gl-mt-3 gl-border-solid gl-border-gray-100 gl-border-1">
<source-editor
ref="editor"
:value="mergedYaml"
:file-name="ciConfigPath"
:file-global-id="fileGlobalId"
:editor-options="{ readOnly: true }"
v-on="$listeners"
/>
<div v-if="isOnDefaultBranch">
<div class="gl-display-flex gl-align-items-center">
<gl-icon :size="16" name="lock" class="gl-text-gray-500 gl-mr-3" />
{{ $options.i18n.viewOnlyMessage }}
</div>
<div class="gl-mt-3 gl-border-solid gl-border-gray-100 gl-border-1">
<source-editor
ref="editor"
:value="mergedYaml"
:file-name="ciConfigPath"
:file-global-id="fileGlobalId"
:editor-options="{ readOnly: true }"
v-on="$listeners"
/>
</div>
</div>
<gl-alert
v-else
variant="info"
:dismissible="false"
:title="$options.i18n.unavailableDefaultTitle"
>
<gl-sprintf :message="$options.i18n.unavailableDefaultText">
<template #link="{ content }">
<gl-link :href="expandedConfigHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</div>
</template>

View File

@ -1,6 +1,7 @@
<script>
import filesQuery from 'shared_queries/repository/files.query.graphql';
import createFlash from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '../../locale';
import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../constants';
import getRefMixin from '../mixins/get_ref';
@ -14,7 +15,7 @@ export default {
FileTable,
FilePreview,
},
mixins: [getRefMixin],
mixins: [getRefMixin, glFeatureFlagMixin()],
apollo: {
projectPath: {
query: projectPathQuery,
@ -52,7 +53,9 @@ export default {
pageSize() {
// we want to exponentially increase the page size to reduce the load on the frontend
const exponentialSize = (TREE_PAGE_SIZE / TREE_INITIAL_FETCH_COUNT) * (this.fetchCounter + 1);
return exponentialSize < TREE_PAGE_SIZE ? exponentialSize : TREE_PAGE_SIZE;
return exponentialSize < TREE_PAGE_SIZE && this.glFeatures.increasePageSizeExponentially
? exponentialSize
: TREE_PAGE_SIZE;
},
totalEntries() {
return Object.values(this.entries).flat().length;

View File

@ -1,48 +0,0 @@
<script>
import $ from 'jquery';
import 'select2';
import { loadCSSFile } from '~/lib/utils/css_utils';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Select2Select',
props: {
options: {
type: Object,
required: false,
default: () => ({}),
},
value: {
type: String,
required: false,
default: '',
},
},
watch: {
value() {
$(this.$refs.dropdownInput).val(this.value).trigger('change');
},
},
mounted() {
loadCSSFile(gon.select2_css_path)
.then(() => {
$(this.$refs.dropdownInput)
.val(this.value)
.select2(this.options)
.on('change', (event) => this.$emit('input', event.target.value));
})
.catch(() => {});
},
beforeDestroy() {
$(this.$refs.dropdownInput).select2('destroy');
},
};
</script>
<template>
<input ref="dropdownInput" type="hidden" />
</template>

View File

@ -54,6 +54,8 @@
white-space: pre;
word-wrap: normal;
border-left: $border-style;
text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; /* stylelint-disable-line property-no-vendor-prefix */
}
code {
@ -65,7 +67,7 @@
}
.line-numbers {
padding: 10px;
padding: 10px 10px 10px 0;
text-align: right;
float: left;
@ -86,18 +88,24 @@
}
}
.file-actions {
flex-shrink: 0;
}
.file-title-flex-parent {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
background-color: $gray-light;
border: $border-style;
border-bottom: 0;
padding: $gl-padding-top $gl-padding;
padding: $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
.file-header-content {
max-width: 75%;
.file-title-name {
font-weight: $gl-font-weight-bold;
}
@ -105,6 +113,7 @@
.gitlab-embedded-snippets-title {
text-decoration: none;
color: $gl-text-color;
word-break: break-word;
&:hover {
text-decoration: underline;

View File

@ -37,6 +37,7 @@ class ProjectsController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
end
layout :determine_layout

View File

@ -2,10 +2,10 @@
module Integrations
class Datadog < Integration
DEFAULT_SITE = 'datadoghq.com'
URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'
URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'
URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/"
DEFAULT_DOMAIN = 'datadoghq.com'
URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_domain}/api/v2/webhook'
URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_domain}/account/settings#api'
URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/"
SUPPORTED_EVENTS = %w[
pipeline job
@ -26,7 +26,7 @@ module Integrations
def initialize_properties
super
self.datadog_site ||= DEFAULT_SITE
self.datadog_site ||= DEFAULT_DOMAIN
end
def self.supported_events
@ -62,7 +62,7 @@ module Integrations
{
type: 'text',
name: 'datadog_site',
placeholder: DEFAULT_SITE,
placeholder: DEFAULT_DOMAIN,
help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
required: false
},
@ -105,18 +105,21 @@ module Integrations
end
def hook_url
url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site)
url = api_url.presence || sprintf(URL_TEMPLATE, datadog_domain: datadog_domain)
url = URI.parse(url)
url.path = File.join(url.path || '/', api_key)
query = { service: datadog_service.presence, env: datadog_env.presence }.compact
url.query = query.to_query unless query.empty?
query = {
"dd-api-key" => api_key,
service: datadog_service.presence,
env: datadog_env.presence
}.compact
url.query = query.to_query
url.to_s
end
def api_keys_url
return URL_API_KEYS_DOCS unless datadog_site.presence
sprintf(URL_TEMPLATE_API_KEYS, datadog_site: datadog_site)
sprintf(URL_TEMPLATE_API_KEYS, datadog_domain: datadog_domain)
end
def execute(data)
@ -137,5 +140,14 @@ module Integrations
{ success: true, result: result[:message] }
end
private
def datadog_domain
# Transparently ignore "app" prefix from datadog_site as the official docs table in
# https://docs.datadoghq.com/getting_started/site/ is confusing for internal URLs.
# US3 needs to keep a prefix but other datacenters cannot have the listed "app" prefix
datadog_site.delete_prefix("app.")
end
end
end

View File

@ -209,7 +209,7 @@ module Ci
# We need to use the presenter here because Gitaly calls in the presenter
# may fail, and we need to ensure the response has been generated.
presented_build = ::Ci::BuildRunnerPresenter.new(build) # rubocop:disable CodeReuse/Presenter
build_json = ::API::Entities::JobRequest::Response.new(presented_build).to_json
build_json = ::API::Entities::Ci::JobRequest::Response.new(presented_build).to_json
Result.new(build, build_json, true)
end

View File

@ -20,20 +20,15 @@ module ServicePing
def execute
return unless ServicePing::PermitDataCategoriesService.new.product_intelligence_enabled?
usage_data = Gitlab::UsageData.data(force_refresh: true)
begin
usage_data = BuildPayloadService.new.execute
raw_usage_data, response = submit_usage_data_payload(usage_data)
rescue StandardError
return unless Gitlab::CurrentSettings.usage_ping_enabled?
raise SubmissionError, 'Usage data is blank' if usage_data.blank?
raw_usage_data = save_raw_usage_data(usage_data)
response = Gitlab::HTTP.post(
url,
body: usage_data.to_json,
allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
)
raise SubmissionError, "Unsuccessful response code: #{response.code}" unless response.success?
usage_data = Gitlab::UsageData.data(force_refresh: true)
raw_usage_data, response = submit_usage_data_payload(usage_data)
end
version_usage_data_id = response.dig('conv_index', 'usage_data_id') || response.dig('dev_ops_score', 'usage_data_id')
@ -48,6 +43,27 @@ module ServicePing
private
def submit_payload(usage_data)
Gitlab::HTTP.post(
url,
body: usage_data.to_json,
allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
)
end
def submit_usage_data_payload(usage_data)
raise SubmissionError, 'Usage data is blank' if usage_data.blank?
raw_usage_data = save_raw_usage_data(usage_data)
response = submit_payload(usage_data)
raise SubmissionError, "Unsuccessful response code: #{response.code}" unless response.success?
[raw_usage_data, response]
end
def save_raw_usage_data(usage_data)
RawUsageData.safe_find_or_create_by(recorded_at: usage_data[:recorded_at]) do |record|
record.payload = usage_data

View File

@ -74,7 +74,7 @@
= f.text_field :build_timeout_human_readable, class: 'form-control gl-form-input'
%p.form-text.text-muted
= html_escape(_('Jobs fail if they run longer than the timeout time. Input value is in seconds by default. Human readable input is also accepted, for example %{code_open}1 hour%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'timeout'), target: '_blank'
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'set-a-limit-for-how-long-jobs-can-run'), target: '_blank'
- if can?(current_user, :update_max_artifacts_size, @project)
.form-group
@ -94,7 +94,7 @@
.input-group-text /
%p.form-text.text-muted
= html_escape(_('The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable.')) % { regex: '<code>\(\d+.\d+%\)</code>'.html_safe }
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'add-test-coverage-results-to-a-merge-request'), target: '_blank'
= f.submit _('Save changes'), class: "btn gl-button btn-confirm", data: { qa_selector: 'save_general_pipelines_changes_button' }

View File

@ -1,8 +0,0 @@
---
name: escalation_policies_mvc
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60524
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329347
milestone: '13.12'
type: development
group: group::monitor
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: increase_page_size_exponentially
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66174
rollout_issue_url:
milestone: '14.1'
type: development
group: group::source code
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: multiple_oncall_schedules
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59829
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328474
milestone: '13.11'
type: development
group: group::monitor
default_enabled: false

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326189
milestone: '13.11'
type: development
group: group::pipeline authoring
default_enabled: false
default_enabled: true

View File

@ -1,7 +1,7 @@
---
name: security_configuration_redesign_ee
introduced_by_url:
rollout_issue_url:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65171
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336077
milestone: '14.1'
type: development
group: group::analyzer frontend

View File

@ -1,5 +1,4 @@
# frozen_string_literal: true
# rubocop:disable Style/SignalException
DATA_WAREHOUSE_LABELS = [
"Data Warehouse::Impact Check",
@ -15,11 +14,6 @@ Notification to the Data Team about changes to the db/structure.sql file, add la
MSG
db_schema_updated = !git.modified_files.grep(%r{\Adb/structure\.sql}).empty?
no_data_warehouse_labels = (gitlab.mr_labels & DATA_WAREHOUSE_LABELS).empty?
if db_schema_updated && no_data_warehouse_labels
markdown(CHANGED_SCHEMA_MESSAGE)
end
markdown(CHANGED_SCHEMA_MESSAGE) if db_schema_updated && no_data_warehouse_labels

View File

@ -21,7 +21,7 @@ these definitions yet.
| Term | Definition | Scope | Discouraged synonyms |
|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|-------------------------------------------------|
| Node | An individual server that runs GitLab either with a specific role or as a whole (e.g. a Rails application node). In a cloud context this can be a specific machine type. | GitLab | instance, server |
| Node | An individual server that runs GitLab either with a specific role or as a whole (for example a Rails application node). In a cloud context this can be a specific machine type. | GitLab | instance, server |
| Site | One or a collection of nodes running a single GitLab application. A site can be single-node or multi-node. | GitLab | deployment, installation instance |
| Single-node site | A specific configuration of GitLab that uses exactly one node. | GitLab | single-server, single-instance
| Multi-node site | A specific configuration of GitLab that uses more than one node. | GitLab | multi-server, multi-instance, high availability |
@ -31,7 +31,7 @@ these definitions yet.
| Reference architecture(s) | A [specified configuration of GitLab for a number of users](../reference_architectures/index.md), possibly including multiple nodes and multiple sites. | GitLab | |
| Promoting | Changing the role of a site from secondary to primary. | Geo-specific | |
| Demoting | Changing the role of a site from primary to secondary. | Geo-specific | |
| Failover | The entire process that shifts users from a primary Site to a secondary site. This includes promoting a secondary, but contains other parts as well e.g. scheduling maintenance. | Geo-specific | |
| Failover | The entire process that shifts users from a primary Site to a secondary site. This includes promoting a secondary, but contains other parts as well. For example, scheduling maintenance. | Geo-specific | |
## Examples

View File

@ -15,7 +15,7 @@ the `.gitlab-ci.yml` file in the root of your repository. To access the editor,
From the pipeline editor page you can:
- Select the branch to work from. [Introduced in GitLab 13.12](https://gitlab.com/gitlab-org/gitlab/-/issues/326189), disabled by default.
- Select the branch to work from.
- [Validate](#validate-ci-configuration) your configuration syntax while editing the file.
- Do a deeper [lint](#lint-ci-configuration) of your configuration, that verifies it with any configuration
added with the [`include`](../yaml/index.md#include) keyword.
@ -85,6 +85,9 @@ where:
[extended configuration merged into the job](../yaml/index.md#merge-details).
- YAML anchors are [replaced with the linked configuration](../yaml/index.md#anchors).
NOTE:
You can only see the expanded view when editing the [default branch](../../user/project/repository/branches/default.md).
## Commit changes to CI configuration
The commit form appears at the bottom of each tab in the editor so you can commit

View File

@ -104,10 +104,10 @@ To customize the path:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
1. In the **CI/CD configuration file** field, enter the file name, and if:
- The file is not in the root directory, include the path.
- The file is in a different project, include the group and project name.
- The file is on an external site, enter the full URL.
1. In the **CI/CD configuration file** field, enter the filename. If the file:
- Is not in the root directory, include the path.
- Is in a different project, include the group and project name.
- Is on an external site, enter the full URL.
1. Select **Save changes**.
### Custom CI/CD configuration file examples
@ -122,7 +122,7 @@ If the CI/CD configuration file is on an external site, the URL must end with `.
- `http://example.com/generate/ci/config.yml`
If the CI/CD configuration file is in a different project in GitLab, the path must be relative
If the CI/CD configuration file is in a different project, the path must be relative
to the root directory in the other project. Include the group and project name at the end:
- `.gitlab-ci.yml@mygroup/another-project`
@ -173,31 +173,32 @@ In GitLab 12.0 and later, newly created projects automatically have a default
This value can be overridden by the [`GIT_DEPTH` variable](../large_repositories/index.md#shallow-cloning)
in the `.gitlab-ci.yml` file.
## Timeout
## Set a limit for how long jobs can run
Timeout defines the maximum amount of time in minutes that a job is able run.
This is configurable under your project's **Settings > CI/CD > General pipelines settings**.
The default value is 60 minutes. Decrease the time limit if you want to impose
a hard limit on your jobs' running time or increase it otherwise. In any case,
if the job surpasses the threshold, it is marked as failed.
You can define how long a job can run before it times out.
### Timeout overriding for runners
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
1. In the **Timeout** field, enter the number of minutes, or a human-readable value like `2 hours`.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17221) in GitLab 10.7.
Jobs that exceed the timeout are marked as failed.
Project defined timeout (either specific timeout set by user or the default
60 minutes timeout) may be [overridden for runners](../runners/configure_runners.md#set-maximum-job-timeout-for-a-runner).
You can override this value [for individual runners](../runners/configure_runners.md#set-maximum-job-timeout-for-a-runner).
## Test coverage parsing
## Add test coverage results to a merge request
If you use test coverage in your code, GitLab can capture its output in the
job log using a regular expression.
If you use test coverage in your code, you can use a regular expression to
find coverage results in the job log. You can then include these results
in the merge request in GitLab.
In your project, go to **Settings > CI/CD** and expand the **General pipelines**
section. Enter the regular expression in the **Test coverage parsing** field.
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
1. In the **Test coverage parsing** field, enter a regular expression.
Leave blank to disable this feature.
Leave blank if you want to disable it or enter a Ruby regular expression. You
can use <https://rubular.com> to test your regex. The regex returns the **last**
You can use <https://rubular.com> to test your regex. The regex returns the **last**
match found in the output.
If the pipeline succeeds, the coverage is shown in the merge request widget and
@ -208,6 +209,10 @@ averaged.
![Build status coverage](img/pipelines_test_coverage_build.png)
### Test coverage examples
Use this regex for commonly used test tools.
<!-- vale gitlab.Spelling = NO -->
- Simplecov (Ruby). Example: `\(\d+.\d+\%\) covered`.
@ -221,21 +226,25 @@ averaged.
- `mix test --cover` (Elixir). Example: `\d+.\d+\%\s+\|\s+Total`.
- JaCoCo (Java/Kotlin). Example: `Total.*?([0-9]{1,3})%`.
- `go test -cover` (Go). Example: `coverage: \d+.\d+% of statements`.
- .Net (OpenCover). Example: `(Visited Points).*\((.*)\)`.
- .Net (`dotnet test` line coverage). Example: `Total\s*\|\s*(\d+(?:\.\d+)?)`.
- .NET (OpenCover). Example: `(Visited Points).*\((.*)\)`.
- .NET (`dotnet test` line coverage). Example: `Total\s*\|\s*(\d+(?:\.\d+)?)`.
<!-- vale gitlab.Spelling = YES -->
### Code coverage history
### View code coverage history
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) the ability to download a `.csv` in GitLab 12.10.
> - [Graph introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1.
To see the evolution of your project code coverage over time,
you can view a graph or download a CSV file with this data. From your project:
you can view a graph or download a CSV file with this data.
1. Go to **Project Analytics > Repository** to see the historic data for each job listed in the dropdown above the graph.
1. If you want a CSV file of that data, click **Download raw data (`.csv`)**
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Analytics > Repository**.
The historic data for each job is listed in the dropdown above the graph.
To view a CSV file of the data, select **Download raw data (`.csv`)**.
![Code coverage graph of a project over time](img/code_coverage_graph_v13_1.png)
@ -261,7 +270,7 @@ Follow these steps to enable the `Coverage-Check` MR approval rule:
![Coverage-Check approval rule](img/coverage_check_approval_rule_14_1.png)
### Removing color codes
### Remove color codes from code coverage
Some test coverage tools output with ANSI color codes that aren't
parsed correctly by the regular expression. This causes coverage
@ -279,13 +288,18 @@ lein cloverage | perl -pe 's/\e\[?.*?[\@-~]//g'
## Pipeline badges
In the pipelines settings page you can find pipeline status and test coverage
badges for your project. The latest successful pipeline is used to read
the pipeline status and test coverage values.
Pipeline badges indicate the pipeline status and a test coverage value
for your project. These badges are determined by the latest successful pipeline.
Visit the pipelines settings page in your project to see the exact link to
your badges. You can also see ways to embed the badge image in your HTML or Markdown
pages.
### View the code for the pipeline status and coverage reports badges
You can view the exact link for your badges. Then you can embed the badge in your HTML
or Markdown pages.
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
1. In the **Pipeline status** or **Coverage report** sections, view the URLs for the images.
![Pipelines badges](img/pipelines_settings_badges.png)
@ -301,7 +315,7 @@ Depending on the status of your pipeline, a badge can have the following values:
- `canceled`
- `unknown`
You can access a pipeline status badge image using the following link:
You can access a pipeline status badge image by using the following link:
```plaintext
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/pipeline.svg
@ -309,7 +323,7 @@ https://gitlab.example.com/<namespace>/<project>/badges/<branch>/pipeline.svg
#### Display only non-skipped status
If you want the pipeline status badge to only display the last non-skipped status, you can use the `?ignore_skipped=true` query parameter:
To make the pipeline status badge display only the last non-skipped status, use the `?ignore_skipped=true` query parameter:
```plaintext
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/pipeline.svg?ignore_skipped=true
@ -317,20 +331,20 @@ https://gitlab.example.com/<namespace>/<project>/badges/<branch>/pipeline.svg?ig
### Test coverage report badge
GitLab makes it possible to define the regular expression for the [coverage report](#test-coverage-parsing),
You can define the regular expression for the [coverage report](#add-test-coverage-results-to-a-merge-request)
that each job log is matched against. This means that each job in the
pipeline can have the test coverage percentage value defined.
The test coverage badge can be accessed using following link:
To access the test coverage badge, use the following link:
```plaintext
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/coverage.svg
```
If you would like to get the coverage report from a specific job, you can add
To get the coverage report from a specific job, add
the `job=coverage_job_name` parameter to the URL. For example, the following
Markdown code embeds the test coverage report badge of the `coverage` job
into your `README.md`:
in your `README.md`:
```markdown
![coverage](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage)

View File

@ -19,14 +19,14 @@ Read [clearing the cache](../caching/index.md#clearing-the-cache).
## Set maximum job timeout for a runner
For each runner, you can specify a *maximum job timeout*. This timeout,
if smaller than the [project defined timeout](../pipelines/settings.md#timeout), takes precedence.
if smaller than the [project defined timeout](../pipelines/settings.md#set-a-limit-for-how-long-jobs-can-run), takes precedence.
This feature can be used to prevent your shared runner from being overwhelmed
by a project that has jobs with a long timeout (for example, one week).
When not configured, runners do not override the project timeout.
On GitLab.com, you cannot override the job timeout for shared runners and must use the [project defined timeout](../pipelines/settings.md#timeout).
On GitLab.com, you cannot override the job timeout for shared runners and must use the [project defined timeout](../pipelines/settings.md#set-a-limit-for-how-long-jobs-can-run).
To set the maximum job timeout:

View File

@ -3418,7 +3418,7 @@ test:
```
The job-level timeout can exceed the
[project-level timeout](../pipelines/settings.md#timeout) but can't
[project-level timeout](../pipelines/settings.md#set-a-limit-for-how-long-jobs-can-run) but can't
exceed the runner-specific timeout.
### `parallel`

View File

@ -605,7 +605,7 @@ A merge request may benefit from being considered a customer critical priority b
Properties of customer critical merge requests:
- The [Senior Director of Development](https://about.gitlab.com/job-families/engineering/engineering-management/#senior-director-engineering) ([@clefelhocz1](https://gitlab.com/clefelhocz1)) is the DRI for deciding if a merge request is customer critical.
- The [VP of Development](https://about.gitlab.com/job-families/engineering/development/management/vp) ([@clefelhocz1](https://gitlab.com/clefelhocz1)) is the DRI for deciding if a merge request qualifies as customer critical.
- The DRI applies the `customer-critical-merge-request` label to the merge request.
- It is required that the reviewer(s) and maintainer(s) involved with a customer critical merge request are engaged as soon as this decision is made.
- It is required to prioritize work for those involved on a customer critical merge request so that they have the time available necessary to focus on it.

View File

@ -0,0 +1,117 @@
---
stage: Enablement
group: Database
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Transaction guidelines
This document gives a few examples of the usage of database transactions in application code.
For further reference please check PostgreSQL documentation about [transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html).
## Database decomposition and sharding
The [sharding group](https://about.gitlab.com/handbook/engineering/development/enablement/sharding) plans to split the main GitLab database and move some of the database tables to other database servers.
The group will start decomposing the `ci_*` related database tables first. To maintain the current application development experience, tooling and static analyzers will be added to the codebase to ensure correct data access and data modification methods. By using the correct form for defining database transactions, we can save significant refactoring work in the future.
## The transaction block
The `ActiveRecord` library provides a convenient way to group database statements into a transaction.
```ruby
issue = Issue.find(10)
project = issue.project
ApplicationRecord.transaction do
issue.update!(title: 'updated title')
project.update!(last_update_at: Time.now)
end
```
This transaction involves two database tables, in case of an error, each `UPDATE` statement will be rolled back to the previous, consistent state.
NOTE:
Avoid referencing the `ActiveRecord::Base` class and use `ApplicationRecord` instead.
## Transaction and database locks
When a transaction block is opened, the database will try to acquire the necessary locks on the resources. The type of locks will depend on the actual database statements.
Consider a concurrent update scenario where the following code is executed at the same time from two different processes:
```ruby
issue = Issue.find(10)
project = issue.project
ApplicationRecord.transaction do
issue.update!(title: 'updated title')
project.update!(last_update_at: Time.now)
end
```
The database will try to acquire the `FOR UPDATE` lock for the referenced `issue` and `project` records. In our case, we have two competing transactions for these locks, one of them will successfully acquire them. The other transaction will have to wait in the lock queue until the first transaction finishes. The execution of the second transaction is blocked at this point.
## Transaction speed
To prevent lock contention and maintain stable application performance, the transaction block should finish as fast as possible. When a transaction acquires locks, it will hold on to them until the transaction finishes.
Apart from application performance, long-running transactions can also affect the application upgrade processes by blocking database migrations.
### Dangerous example: 3rd party API calls
Consider the following example:
```ruby
member = Member.find(5)
Member.transaction do
member.update!(notification_email_sent: true)
member.send_notification_email
end
```
Here, we ensure that the `notification_email_sent` column is updated only when the `send_notification_email` method succeeds. The `send_notification_email` method executes a network request to an email sending service. If the underlying infrastructure does not specify timeouts or the network call takes too long time, the database transaction will stay open.
Ideally, a transaction should only contain database statements.
Avoid doing in a `transaction` block:
- External network requests such as: triggering Sidekiq jobs, sending emails, HTTP API calls and running database statements using a different connection.
- File system operations.
- Long, CPU intensive computation.
- Calling `sleep(n)`.
## Explicit model referencing
If a transaction modifies records from the same database table, it's advised to use the `Model.transaction` block:
```ruby
build_1 = Ci::Build.find(1)
build_2 = Ci::Build.find(2)
Ci::Build.transaction do
build_1.touch
build_2.touch
end
```
The transaction above will use the same database connection for the transaction as the models in the `transaction` block. In a multi-database environment the following example would be dangerous:
```ruby
# `ci_builds` table is located on another database
class Ci::Build < CiDatabase
end
build_1 = Ci::Build.find(1)
build_2 = Ci::Build.find(2)
ActiveRecord::Base.transaction do
build_1.touch
build_2.touch
end
```
The `ActiveRecord::Base` class uses a different database connection than the `Ci::Build` records. The two statements in the transaction block will not be part of the transaction and will not be rolled back in case something goes wrong. They act as 3rd part calls.

View File

@ -272,14 +272,15 @@ If you add a member to a group by using the [share a group with another group](.
## CI pipeline minutes
CI pipeline minutes are the execution time for your
[pipelines](../../ci/pipelines/index.md) on GitLab shared runners. Each
[GitLab SaaS tier](https://about.gitlab.com/pricing/) includes a monthly quota
of CI pipeline minutes:
CI pipeline minutes are the execution time for your [pipelines](../../ci/pipelines/index.md)
on GitLab shared runners. Each [GitLab SaaS tier](https://about.gitlab.com/pricing/)
includes a monthly quota of CI pipeline minutes for private and public projects:
- Free: 400 minutes
- Premium: 10,000 minutes
- Ultimate: 50,000 minutes
| Plan | Private projects | Public projects |
|----------|------------------|-----------------|
| Free | 400 | 50,000 |
| Premium | 10,000 | 1,250,000 |
| Ultimate | 50,000 | 6,250,000 |
Quotas apply to:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -47,8 +47,7 @@ When GitLab detects a **Denied** license, you can view it in the [license list](
![License List](img/license_list_v13_0.png)
You can view and modify existing policies from the [policies](#policies) tab.
![Edit Policy](img/policies_maintainer_edit_v13_2.png)
![Edit Policy](img/policies_maintainer_edit_v14_2.png)
## Supported languages and package managers
@ -722,7 +721,7 @@ which enables a designated approver that can approve and then merge a merge requ
The **Policies** tab in the project's license compliance section displays your project's license
policies. Project maintainers can specify policies in this section.
![Edit Policy](img/policies_maintainer_edit_v13_2.png)
![Edit Policy](img/policies_maintainer_edit_v14_2.png)
![Add Policy](img/policies_maintainer_add_v13_2.png)
Developers of the project can view the policies configured in a project.

View File

@ -41,7 +41,7 @@ To see the latest code coverage for each project in your group:
1. In the **Latest test coverage results** section, use the **Select projects** dropdown to choose the projects you want to check.
You can download code coverage data for specific projects using
[code coverage history](../../../ci/pipelines/settings.md#code-coverage-history).
[code coverage history](../../../ci/pipelines/settings.md#view-code-coverage-history).
## Download historic test coverage data

View File

@ -11,7 +11,7 @@ module API
resource :runners do
desc 'Registers a new Runner' do
success Entities::RunnerRegistrationDetails
success Entities::Ci::RunnerRegistrationDetails
http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
end
params do
@ -47,7 +47,7 @@ module API
@runner = ::Ci::Runner.create(attributes)
if @runner.persisted?
present @runner, with: Entities::RunnerRegistrationDetails
present @runner, with: Entities::Ci::RunnerRegistrationDetails
else
render_validation_error!(@runner)
end
@ -82,7 +82,7 @@ module API
before { set_application_context }
desc 'Request a job' do
success Entities::JobRequest::Response
success Entities::Ci::JobRequest::Response
http_codes [[201, 'Job was scheduled'],
[204, 'No job for Runner'],
[403, 'Forbidden']]
@ -267,7 +267,7 @@ module API
end
desc 'Upload artifacts for job' do
success Entities::JobRequest::Response
success Entities::Ci::JobRequest::Response
http_codes [[201, 'Artifact uploaded'],
[400, 'Bad request'],
[403, 'Forbidden'],

View File

@ -11,7 +11,7 @@ module API
resource :runners do
desc 'Get runners available for user' do
success Entities::Runner
success Entities::Ci::Runner
end
params do
optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES,
@ -30,11 +30,11 @@ module API
runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
present paginate(runners), with: Entities::Runner
present paginate(runners), with: Entities::Ci::Runner
end
desc 'Get all runners - shared and specific' do
success Entities::Runner
success Entities::Ci::Runner
end
params do
optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_SCOPES,
@ -55,11 +55,11 @@ module API
runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
present paginate(runners), with: Entities::Runner
present paginate(runners), with: Entities::Ci::Runner
end
desc "Get runner's details" do
success Entities::RunnerDetails
success Entities::Ci::RunnerDetails
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
@ -68,11 +68,11 @@ module API
runner = get_runner(params[:id])
authenticate_show_runner!(runner)
present runner, with: Entities::RunnerDetails, current_user: current_user
present runner, with: Entities::Ci::RunnerDetails, current_user: current_user
end
desc "Update runner's details" do
success Entities::RunnerDetails
success Entities::Ci::RunnerDetails
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
@ -92,14 +92,14 @@ module API
update_service = ::Ci::UpdateRunnerService.new(runner)
if update_service.update(declared_params(include_missing: false))
present runner, with: Entities::RunnerDetails, current_user: current_user
present runner, with: Entities::Ci::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
end
end
desc 'Remove a runner' do
success Entities::Runner
success Entities::Ci::Runner
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
@ -139,7 +139,7 @@ module API
before { authorize_admin_project }
desc 'Get runners available for project' do
success Entities::Runner
success Entities::Ci::Runner
end
params do
optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_SCOPES,
@ -158,11 +158,11 @@ module API
runners = filter_runners(runners, params[:scope])
runners = apply_filter(runners, params)
present paginate(runners), with: Entities::Runner
present paginate(runners), with: Entities::Ci::Runner
end
desc 'Enable a runner for a project' do
success Entities::Runner
success Entities::Ci::Runner
end
params do
requires :runner_id, type: Integer, desc: 'The ID of the runner'
@ -172,14 +172,14 @@ module API
authenticate_enable_runner!(runner)
if runner.assign_to(user_project)
present runner, with: Entities::Runner
present runner, with: Entities::Ci::Runner
else
render_validation_error!(runner)
end
end
desc "Disable project's runner" do
success Entities::Runner
success Entities::Ci::Runner
end
params do
requires :runner_id, type: Integer, desc: 'The ID of the runner'
@ -204,7 +204,7 @@ module API
before { authorize_admin_group }
desc 'Get runners available for group' do
success Entities::Runner
success Entities::Ci::Runner
end
params do
optional :type, type: String, values: ::Ci::Runner::AVAILABLE_TYPES,
@ -218,7 +218,7 @@ module API
runners = ::Ci::Runner.belonging_to_group(user_group.id, include_ancestors: true)
runners = apply_filter(runners, params)
present paginate(runners), with: Entities::Runner
present paginate(runners), with: Entities::Ci::Runner
end
end

View File

@ -7,7 +7,7 @@ module API
# artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5)
expose :artifacts_file, using: ::API::Entities::Ci::JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :job_artifacts, as: :artifacts, using: ::API::Entities::Ci::JobArtifact
expose :runner, with: ::API::Entities::Runner
expose :runner, with: ::API::Entities::Ci::Runner
expose :artifacts_expire_at
expose :tag_list do |job|
job.tags.map(&:name).sort

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Artifacts < Grape::Entity
expose :name
expose :untracked
expose :paths
expose :exclude, expose_nil: false
expose :when
expose :expire_in
expose :artifact_type
expose :artifact_format
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Cache < Grape::Entity
expose :key, :untracked, :paths, :policy, :when
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Credentials < Grape::Entity
expose :type, :url, :username, :password
end
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Dependency < Grape::Entity
expose :id, :name, :token
expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? }
end
end
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class GitInfo < Grape::Entity
expose :repo_url, :ref, :sha, :before_sha
expose :ref_type
expose :refspecs
expose :git_depth, as: :depth
end
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Image < Grape::Entity
expose :name, :entrypoint
expose :ports, using: Entities::Ci::JobRequest::Port
end
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class JobInfo < Grape::Entity
expose :id, :name, :stage
expose :project_id, :project_name
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Port < Grape::Entity
expose :number, :protocol, :name
end
end
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Response < Grape::Entity
expose :id
expose :token
expose :allow_git_fetch
expose :job_info, using: Entities::Ci::JobRequest::JobInfo do |model|
model
end
expose :git_info, using: Entities::Ci::JobRequest::GitInfo do |model|
model
end
expose :runner_info, using: Entities::Ci::JobRequest::RunnerInfo do |model|
model
end
expose :runner_variables, as: :variables
expose :steps, using: Entities::Ci::JobRequest::Step
expose :image, using: Entities::Ci::JobRequest::Image
expose :services, using: Entities::Ci::JobRequest::Service
expose :artifacts, using: Entities::Ci::JobRequest::Artifacts
expose :cache, using: Entities::Ci::JobRequest::Cache
expose :credentials, using: Entities::Ci::JobRequest::Credentials
expose :all_dependencies, as: :dependencies, using: Entities::Ci::JobRequest::Dependency
expose :features
end
end
end
end
end
API::Entities::Ci::JobRequest::Response.prepend_mod_with('API::Entities::Ci::JobRequest::Response')

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class RunnerInfo < Grape::Entity
expose :metadata_timeout, as: :timeout
expose :runner_session_url
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Service < Entities::Ci::JobRequest::Image
expose :alias, :command
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Ci
module JobRequest
class Step < Grape::Entity
expose :name, :script, :timeout, :when, :allow_failure
end
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module API
module Entities
module Ci
class Runner < Grape::Entity
expose :id
expose :description
expose :ip_address
expose :active
expose :instance_type?, as: :is_shared
expose :runner_type
expose :name
expose :online?, as: :online
expose :status
end
end
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module API
module Entities
module Ci
class RunnerDetails < Runner
expose :tag_list
expose :run_untagged
expose :locked
expose :maximum_timeout
expose :access_level
expose :version, :revision, :platform, :architecture
expose :contacted_at
# rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options|
if options[:current_user].admin? # rubocop: disable Cop/UserAdmin
runner.projects
else
options[:current_user].authorized_projects.where(id: runner.projects)
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
expose :groups, with: Entities::BasicGroupDetails do |runner, options|
if options[:current_user].admin? # rubocop: disable Cop/UserAdmin
runner.groups
else
options[:current_user].authorized_groups.where(id: runner.groups)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module Ci
class RunnerRegistrationDetails < Grape::Entity
expose :id, :token
end
end
end
end

View File

@ -1,18 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Artifacts < Grape::Entity
expose :name
expose :untracked
expose :paths
expose :exclude, expose_nil: false
expose :when
expose :expire_in
expose :artifact_type
expose :artifact_format
end
end
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Cache < Grape::Entity
expose :key, :untracked, :paths, :policy, :when
end
end
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Credentials < Grape::Entity
expose :type, :url, :username, :password
end
end
end
end

View File

@ -1,12 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Dependency < Grape::Entity
expose :id, :name, :token
expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? }
end
end
end
end

View File

@ -1,14 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class GitInfo < Grape::Entity
expose :repo_url, :ref, :sha, :before_sha
expose :ref_type
expose :refspecs
expose :git_depth, as: :depth
end
end
end
end

View File

@ -1,12 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Image < Grape::Entity
expose :name, :entrypoint
expose :ports, using: Entities::JobRequest::Port
end
end
end
end

View File

@ -1,12 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class JobInfo < Grape::Entity
expose :id, :name, :stage
expose :project_id, :project_name
end
end
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Port < Grape::Entity
expose :number, :protocol, :name
end
end
end
end

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Response < Grape::Entity
expose :id
expose :token
expose :allow_git_fetch
expose :job_info, using: Entities::JobRequest::JobInfo do |model|
model
end
expose :git_info, using: Entities::JobRequest::GitInfo do |model|
model
end
expose :runner_info, using: Entities::JobRequest::RunnerInfo do |model|
model
end
expose :runner_variables, as: :variables
expose :steps, using: Entities::JobRequest::Step
expose :image, using: Entities::JobRequest::Image
expose :services, using: Entities::JobRequest::Service
expose :artifacts, using: Entities::JobRequest::Artifacts
expose :cache, using: Entities::JobRequest::Cache
expose :credentials, using: Entities::JobRequest::Credentials
expose :all_dependencies, as: :dependencies, using: Entities::JobRequest::Dependency
expose :features
end
end
end
end
API::Entities::JobRequest::Response.prepend_mod_with('API::Entities::JobRequest::Response')

View File

@ -1,12 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class RunnerInfo < Grape::Entity
expose :metadata_timeout, as: :timeout
expose :runner_session_url
end
end
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Service < Entities::JobRequest::Image
expose :alias, :command
end
end
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Step < Grape::Entity
expose :name, :script, :timeout, :when, :allow_failure
end
end
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
module API
module Entities
class Runner < Grape::Entity
expose :id
expose :description
expose :ip_address
expose :active
expose :instance_type?, as: :is_shared
expose :runner_type
expose :name
expose :online?, as: :online
expose :status
end
end
end

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
module API
module Entities
class RunnerDetails < Runner
expose :tag_list
expose :run_untagged
expose :locked
expose :maximum_timeout
expose :access_level
expose :version, :revision, :platform, :architecture
expose :contacted_at
# rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options|
if options[:current_user].admin?
runner.projects
else
options[:current_user].authorized_projects.where(id: runner.projects)
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
expose :groups, with: Entities::BasicGroupDetails do |runner, options|
if options[:current_user].admin?
runner.groups
else
options[:current_user].authorized_groups.where(id: runner.groups)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
module API
module Entities
class RunnerRegistrationDetails < Grape::Entity
expose :id, :token
end
end
end

View File

@ -17489,6 +17489,9 @@ msgstr ""
msgid "Input the remote repository URL"
msgstr ""
msgid "Insert"
msgstr ""
msgid "Insert a %{rows}x%{cols} table."
msgstr ""
@ -19173,6 +19176,9 @@ msgstr ""
msgid "Last item before this page loaded in your browser:"
msgstr ""
msgid "Last modified"
msgstr ""
msgid "Last month"
msgstr ""
@ -21749,15 +21755,9 @@ msgstr ""
msgid "NetworkPolicies|Kubernetes error: %{error}"
msgstr ""
msgid "NetworkPolicies|Last modified"
msgstr ""
msgid "NetworkPolicies|Name"
msgstr ""
msgid "NetworkPolicies|Namespace"
msgstr ""
msgid "NetworkPolicies|Network"
msgstr ""
@ -22905,12 +22905,6 @@ msgstr ""
msgid "OnCallSchedules|The schedule could not be updated. Please try again."
msgstr ""
msgid "OnCallSchedules|To create an escalation policy that defines which schedule is used when, visit the %{linkStart}escalation policy%{linkEnd} page."
msgstr ""
msgid "OnCallSchedules|To create an escalation policy using this schedule, visit the %{linkStart}escalation policy%{linkEnd} page."
msgstr ""
msgid "OnCallSchedules|Try adding a rotation"
msgstr ""
@ -22926,7 +22920,7 @@ msgstr ""
msgid "OnCallSchedules|You are currently a part of:"
msgstr ""
msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the add a rotation button."
msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the add a rotation button. To create an escalation policy that defines which schedule is used when, visit the %{linkStart}escalation policy%{linkEnd} page."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
@ -24227,6 +24221,9 @@ msgstr ""
msgid "Pipelines|Merged YAML is view only"
msgstr ""
msgid "Pipelines|Merged YAML unavailable"
msgstr ""
msgid "Pipelines|More Information"
msgstr ""
@ -24263,6 +24260,9 @@ msgstr ""
msgid "Pipelines|The GitLab CI configuration could not be updated."
msgstr ""
msgid "Pipelines|The merged YAML view is only available for the default branch. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "Pipelines|There are currently no finished pipelines."
msgstr ""
@ -29111,6 +29111,9 @@ msgstr ""
msgid "SecurityOrchestration|Security policy project"
msgstr ""
msgid "SecurityPolicies|+%{count} more"
msgstr ""
msgid "SecurityPolicies|All policies"
msgstr ""
@ -29120,6 +29123,9 @@ msgstr ""
msgid "SecurityPolicies|Enforcement status"
msgstr ""
msgid "SecurityPolicies|Environment(s)"
msgstr ""
msgid "SecurityPolicies|Latest scan"
msgstr ""
@ -35334,6 +35340,9 @@ msgstr ""
msgid "Upload file"
msgstr ""
msgid "Upload image"
msgstr ""
msgid "Upload license"
msgstr ""

View File

@ -10,12 +10,15 @@ module QA
view "app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue" do
element :import_item
element :target_namespace_selector_dropdown
element :target_group_dropdown_item
element :import_status_indicator
element :import_group_button
end
view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
element :target_namespace_selector_dropdown
end
# Import source group in to target group
#
# @param [String] source_group_name

View File

@ -14,13 +14,16 @@ module QA
view 'app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue' do
element :project_import_row
element :project_namespace_select
element :project_path_field
element :import_button
element :project_path_content
element :go_to_project_button
end
view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
element :target_namespace_selector_dropdown
end
def add_personal_access_token(personal_access_token)
# If for some reasons this process is retried, user cannot re-enter github token in the same group
# In this case skip this step and proceed to import project row
@ -59,10 +62,9 @@ module QA
def choose_test_namespace(full_path)
within_repo_path(full_path) do
click_element :project_namespace_select
within_element(:target_namespace_selector_dropdown) { click_button(class: 'dropdown-toggle') }
click_element(:target_group_dropdown_item, group_name: Runtime::Namespace.path)
end
search_and_select(Runtime::Namespace.path)
end
def set_path(full_path, name)

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Database
# @example
# # bad
# ActiveRecord::Base.connection
#
# # good
# ApplicationRecord.connection
#
class MultipleDatabases < RuboCop::Cop::Cop
AR_BASE_MESSAGE = <<~EOF
Do not use methods from ActiveRecord::Base, use the ApplicationRecord class instead
For fixing offenses related to the ActiveRecord::Base.transaction method, see our guidelines:
https://docs.gitlab.com/ee/development/database/transaction_guidelines.html
EOF
def_node_matcher :active_record_base_method_is_used?, <<~PATTERN
(send (const (const nil? :ActiveRecord) :Base) $_)
PATTERN
def on_send(node)
return unless active_record_base_method_is_used?(node)
add_offense(node, location: :expression, message: AR_BASE_MESSAGE)
end
end
end
end
end

View File

@ -78,6 +78,8 @@ describe('ProjectsDropdownFilter component', () => {
const selectDropdownItemAtIndex = (index) =>
findDropdownAtIndex(index).find('button').trigger('click');
const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id);
describe('queryParams are applied when fetching data', () => {
beforeEach(() => {
createComponent({
@ -238,14 +240,15 @@ describe('ProjectsDropdownFilter component', () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[projects[0], projects[1]]]]);
expect(selectedIds()).toEqual([projects[0].id, projects[1].id]);
});
it('should remove from selection when clicked again', () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(0);
expect(selectedIds()).toEqual([projects[0].id]);
expect(wrapper.emitted().selected).toEqual([[[projects[0]]], [[]]]);
selectDropdownItemAtIndex(0);
expect(selectedIds()).toEqual([]);
});
it('renders the correct placeholder text when multiple projects are selected', async () => {

View File

@ -35,6 +35,7 @@ describe('Board Store Mutations', () => {
describe('SET_INITIAL_BOARD_DATA', () => {
it('Should set initial Boards data to state', () => {
const allowSubEpics = true;
const boardId = 1;
const fullPath = 'gitlab-org';
const boardType = 'group';
@ -45,6 +46,7 @@ describe('Board Store Mutations', () => {
const issuableType = issuableTypes.issue;
mutations[types.SET_INITIAL_BOARD_DATA](state, {
allowSubEpics,
boardId,
fullPath,
boardType,
@ -53,6 +55,7 @@ describe('Board Store Mutations', () => {
issuableType,
});
expect(state.allowSubEpics).toBe(allowSubEpics);
expect(state.boardId).toEqual(boardId);
expect(state.fullPath).toEqual(fullPath);
expect(state.boardType).toEqual(boardType);

View File

@ -34,7 +34,7 @@ describe('Delete branch button', () => {
expect(findDeleteButton().attributes()).toMatchObject({
title: 'Delete branch',
variant: 'danger',
variant: 'default',
icon: 'remove',
});
});
@ -44,7 +44,7 @@ describe('Delete branch button', () => {
expect(findDeleteButton().attributes()).toMatchObject({
title: 'Delete protected branch',
variant: 'danger',
variant: 'default',
icon: 'remove',
});
});
@ -78,7 +78,7 @@ describe('Delete branch button', () => {
expect(findDeleteButton().attributes()).toMatchObject({
title: 'Delete branch',
variant: 'danger',
variant: 'default',
});
});

View File

@ -0,0 +1,78 @@
import { GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
import { configure as configureImageExtension } from '~/content_editor/extensions/image';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_image_button', () => {
let wrapper;
let editor;
const buildWrapper = () => {
wrapper = mountExtended(ToolbarImageButton, {
propsData: {
tiptapEditor: editor,
},
});
};
const findImageURLInput = () =>
wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
const findApplyImageButton = () => wrapper.findComponent(GlButton);
const selectFile = async (file) => {
const input = wrapper.find({ ref: 'fileSelector' });
// override the property definition because `input.files` isn't directly modifyable
Object.defineProperty(input.element, 'files', { value: [file], writable: true });
await input.trigger('change');
};
beforeEach(() => {
const { tiptapExtension: Image } = configureImageExtension({
renderMarkdown: jest.fn(),
uploadsPath: '/uploads/',
});
editor = createTestEditor({
extensions: [Image],
});
buildWrapper();
});
afterEach(() => {
editor.destroy();
wrapper.destroy();
});
it('sets the image to the value in the URL input when "Insert" button is clicked', async () => {
const commands = mockChainedCommands(editor, ['focus', 'setImage', 'run']);
await findImageURLInput().setValue('https://example.com/img.jpg');
await findApplyImageButton().trigger('click');
expect(commands.focus).toHaveBeenCalled();
expect(commands.setImage).toHaveBeenCalledWith({
alt: 'img',
src: 'https://example.com/img.jpg',
canonicalSrc: 'https://example.com/img.jpg',
});
expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'url' }]);
});
it('uploads the selected image when file input changes', async () => {
const commands = mockChainedCommands(editor, ['focus', 'uploadImage', 'run']);
const file = new File(['foo'], 'foo.png', { type: 'image/png' });
await selectFile(file);
expect(commands.focus).toHaveBeenCalled();
expect(commands.uploadImage).toHaveBeenCalledWith({ file });
expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]);
});
});

View File

@ -76,15 +76,17 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(commands.unsetLink).toHaveBeenCalled();
expect(commands.setLink).toHaveBeenCalledWith({
href: 'https://example',
'data-canonical-src': 'https://example',
canonicalSrc: 'https://example',
});
expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
});
describe('on selection update', () => {
it('updates link input box with canonical-src if present', async () => {
jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
'data-canonical-src': 'uploads/my-file.zip',
canonicalSrc: 'uploads/my-file.zip',
href: '/username/my-project/uploads/abcdefgh133535/my-file.zip',
});
@ -130,9 +132,11 @@ describe('content_editor/components/toolbar_link_button', () => {
expect(commands.focus).toHaveBeenCalled();
expect(commands.setLink).toHaveBeenCalledWith({
href: 'https://example',
'data-canonical-src': 'https://example',
canonicalSrc: 'https://example',
});
expect(commands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
});
});

View File

@ -51,6 +51,7 @@ describe('content_editor/components/top_toolbar', () => {
${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
${'text-styles'} | ${{}}
${'link'} | ${{}}
${'image'} | ${{}}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();

View File

@ -0,0 +1,44 @@
import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
describe('Import entities group dropdown component', () => {
let wrapper;
let namespacesTracker;
const createComponent = (propsData) => {
namespacesTracker = jest.fn();
wrapper = shallowMount(GroupDropdown, {
scopedSlots: {
default: namespacesTracker,
},
stubs: { GlDropdown },
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
it('passes namespaces from props to default slot', () => {
const namespaces = ['ns1', 'ns2'];
createComponent({ namespaces });
expect(namespacesTracker).toHaveBeenCalledWith({ namespaces });
});
it('filters namespaces based on user input', async () => {
const namespaces = ['match1', 'some unrelated', 'match2'];
createComponent({ namespaces });
namespacesTracker.mockReset();
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'match');
await nextTick();
expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: ['match1', 'match2'] });
});
});

View File

@ -1,8 +1,9 @@
import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui';
import { GlButton, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
@ -41,7 +42,7 @@ describe('import table row', () => {
};
const findImportButton = () => findByText(GlButton, 'Import');
const findNameInput = () => wrapper.find(GlFormInput);
const findNamespaceDropdown = () => wrapper.find(GlDropdown);
const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
const createComponent = (props) => {
apolloProvider = createMockApollo([
@ -65,6 +66,7 @@ describe('import table row', () => {
wrapper = shallowMount(ImportTableRow, {
apolloProvider,
stubs: { ImportGroupDropdown },
propsData: {
availableNamespaces: availableNamespacesFixture,
groupPathRegex: /.*/,

View File

@ -11,6 +11,8 @@ import state from '~/import_entities/import_projects/store/state';
describe('ImportProjectsTable', () => {
let wrapper;
const USER_NAMESPACE = 'root';
const findFilterField = () =>
wrapper
.findAllComponents(GlFormInput)
@ -48,7 +50,7 @@ describe('ImportProjectsTable', () => {
localVue.use(Vuex);
const store = new Vuex.Store({
state: { ...state(), ...initialState },
state: { ...state(), defaultTargetNamespace: USER_NAMESPACE, ...initialState },
getters: {
...getters,
...customGetters,

View File

@ -1,11 +1,11 @@
import { GlBadge, GlButton } from '@gitlab/ui';
import { GlBadge, GlButton, GlDropdown } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { STATUSES } from '~/import_entities//constants';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import ImportStatus from '~/import_entities/components/import_status.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
import Select2Select from '~/vue_shared/components/select2_select.vue';
describe('ProviderRepoTableRow', () => {
let wrapper;
@ -16,10 +16,8 @@ describe('ProviderRepoTableRow', () => {
newName: 'newName',
};
const availableNamespaces = [
{ text: 'Groups', children: [{ id: 'test', text: 'test' }] },
{ text: 'Users', children: [{ id: 'root', text: 'root' }] },
];
const availableNamespaces = ['test'];
const userNamespace = 'root';
function initStore(initialState) {
const store = new Vuex.Store({
@ -48,7 +46,7 @@ describe('ProviderRepoTableRow', () => {
wrapper = shallowMount(ProviderRepoTableRow, {
localVue,
store,
propsData: { availableNamespaces, ...props },
propsData: { availableNamespaces, userNamespace, ...props },
});
}
@ -81,9 +79,8 @@ describe('ProviderRepoTableRow', () => {
expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
});
it('renders a select2 namespace select', () => {
expect(wrapper.find(Select2Select).exists()).toBe(true);
expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
it('renders a group namespace select', () => {
expect(wrapper.find(ImportGroupDropdown).props().namespaces).toBe(availableNamespaces);
});
it('renders import button', () => {
@ -133,7 +130,7 @@ describe('ProviderRepoTableRow', () => {
});
it('does not renders a namespace select', () => {
expect(wrapper.find(Select2Select).exists()).toBe(false);
expect(wrapper.find(GlDropdown).exists()).toBe(false);
});
it('does not render import button', () => {

View File

@ -1,10 +1,12 @@
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { EDITOR_READY_EVENT } from '~/editor/constants';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import { mockLintResponse, mockCiConfigPath } from '../../mock_data';
const DEFAULT_BRANCH = 'main';
describe('Text editor component', () => {
let wrapper;
@ -16,7 +18,7 @@ describe('Text editor component', () => {
},
};
const createComponent = ({ props = {} } = {}) => {
const createComponent = ({ props = {}, currentBranch = DEFAULT_BRANCH } = {}) => {
wrapper = shallowMount(CiConfigMergedPreview, {
propsData: {
ciConfigData: mockLintResponse,
@ -24,20 +26,45 @@ describe('Text editor component', () => {
},
provide: {
ciConfigPath: mockCiConfigPath,
defaultBranch: DEFAULT_BRANCH,
},
stubs: {
SourceEditor: MockSourceEditor,
},
data() {
return {
currentBranch,
};
},
});
};
const findIcon = () => wrapper.findComponent(GlIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEditor = () => wrapper.findComponent(MockSourceEditor);
afterEach(() => {
wrapper.destroy();
});
// This is testing a temporary feature.
// It may be slightly hacky code that doesn't follow best practice.
// See the related MR for more information.
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65972#note_626095644
describe('on a non-default branch', () => {
beforeEach(() => {
createComponent({ currentBranch: 'feature' });
});
it('does not load the editor', () => {
expect(findEditor().exists()).toBe(false);
});
it('renders an informational alert', () => {
expect(findAlert().exists()).toBe(true);
});
});
describe('when status is valid', () => {
beforeEach(() => {
createComponent();

View File

@ -19,6 +19,11 @@ function factory(path, data = () => ({})) {
mocks: {
$apollo,
},
provide: {
glFeatures: {
increasePageSizeExponentially: true,
},
},
});
}

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe API::Entities::JobRequest::Image do
RSpec.describe API::Entities::Ci::JobRequest::Image do
let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]}
let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)}
let(:entity) { described_class.new(image) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ::API::Entities::JobRequest::Port do
RSpec.describe ::API::Entities::Ci::JobRequest::Port do
let(:port) { double(number: 80, protocol: 'http', name: 'name')}
let(:entity) { described_class.new(port) }

View File

@ -10,13 +10,13 @@ RSpec.describe Integrations::Datadog do
let(:active) { true }
let(:dd_site) { 'datadoghq.com' }
let(:default_url) { 'https://webhooks-http-intake.logs.datadoghq.com/v1/input/' }
let(:default_url) { 'https://webhooks-http-intake.logs.datadoghq.com/api/v2/webhook' }
let(:api_url) { '' }
let(:api_key) { SecureRandom.hex(32) }
let(:dd_env) { 'ci' }
let(:dd_service) { 'awesome-gitlab' }
let(:expected_hook_url) { default_url + api_key + "?env=#{dd_env}&service=#{dd_service}" }
let(:expected_hook_url) { default_url + "?dd-api-key=#{api_key}&env=#{dd_env}&service=#{dd_service}" }
let(:instance) do
described_class.new(
@ -65,7 +65,7 @@ RSpec.describe Integrations::Datadog do
context 'with custom api_url' do
let(:dd_site) { '' }
let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/v1/input/' }
let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/api/v2/webhook' }
it { is_expected.not_to validate_presence_of(:datadog_site) }
it { is_expected.to validate_presence_of(:api_url) }
@ -107,9 +107,9 @@ RSpec.describe Integrations::Datadog do
end
context 'with custom URL' do
let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/v1/input/' }
let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/api/v2/webhook' }
it { is_expected.to eq(api_url + api_key + "?env=#{dd_env}&service=#{dd_service}") }
it { is_expected.to eq(api_url + "?dd-api-key=#{api_key}&env=#{dd_env}&service=#{dd_service}") }
context 'blank' do
let(:api_url) { '' }
@ -122,7 +122,7 @@ RSpec.describe Integrations::Datadog do
let(:dd_service) { '' }
let(:dd_env) { '' }
it { is_expected.to eq(default_url + api_key) }
it { is_expected.to eq(default_url + "?dd-api-key=#{api_key}") }
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/database/multiple_databases'
RSpec.describe RuboCop::Cop::Database::MultipleDatabases do
subject(:cop) { described_class.new }
it 'flags the use of ActiveRecord::Base.connection' do
expect_offense(<<~SOURCE)
ActiveRecord::Base.connection.inspect
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Do not use methods from ActiveRecord::Base, [...]
SOURCE
end
end

View File

@ -245,11 +245,63 @@ RSpec.describe ServicePing::SubmitService do
context 'and usage data is nil' do
before do
allow(ServicePing::BuildPayloadService).to receive(:execute).and_return(nil)
allow(Gitlab::UsageData).to receive(:data).and_return(nil)
end
it_behaves_like 'does not send a blank usage ping payload'
end
context 'if payload service fails' do
before do
stub_response(body: with_dev_ops_score_params)
allow(ServicePing::BuildPayloadService).to receive(:execute).and_raise(described_class::SubmissionError, 'SubmissionError')
end
it 'calls UsageData .data method' do
usage_data = build_usage_data
expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
subject.execute
end
end
context 'calls BuildPayloadService first' do
before do
stub_response(body: with_dev_ops_score_params)
end
it 'returns usage data' do
usage_data = build_usage_data
expect_next_instance_of(ServicePing::BuildPayloadService) do |service|
expect(service).to receive(:execute).and_return(usage_data)
end
subject.execute
end
end
context 'if version app response fails' do
before do
stub_response(body: with_dev_ops_score_params, status: 404)
usage_data = build_usage_data
allow_next_instance_of(ServicePing::BuildPayloadService) do |service|
allow(service).to receive(:execute).and_return(usage_data)
end
end
it 'calls UsageData .data method' do
usage_data = build_usage_data
expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
# SubmissionError is raised as a result of 404 in response from HTTP Request
expect { subject.execute }.to raise_error(described_class::SubmissionError)
end
end
end
def stub_response(body:, status: 201)
@ -260,4 +312,8 @@ RSpec.describe ServicePing::SubmitService do
status: status
)
end
def build_usage_data
{ uuid: 'uuid', recorded_at: Time.current }
end
end

View File

@ -221,7 +221,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, datateam, documentation, duplicate_yarn_dependencies, eslint, gitaly, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, documentation, duplicate_yarn_dependencies, eslint, gitaly, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
end
end

View File

@ -6,7 +6,6 @@ module Tooling
LOCAL_RULES ||= %w[
changelog
database
datateam
documentation
duplicate_yarn_dependencies
eslint
@ -22,6 +21,7 @@ module Tooling
CI_ONLY_RULES ||= %w[
ce_ee_vue_templates
ci_templates
datateam
metadata
feature_flag
roulette