Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ff89c3c372
commit
25eb713a7f
|
@ -31,38 +31,6 @@ FactoryBot/InlineAssociation:
|
|||
|
||||
InternalAffairs/DeprecateCopHelper:
|
||||
Exclude:
|
||||
- 'spec/rubocop/cop/group_public_or_visible_to_user_spec.rb'
|
||||
- 'spec/rubocop/cop/static_translation_definition_spec.rb'
|
||||
- 'spec/rubocop/cop/lint/last_keyword_argument_spec.rb'
|
||||
- 'spec/rubocop/cop/usage_data/large_table_spec.rb'
|
||||
- 'spec/rubocop/cop/usage_data/distinct_count_by_large_foreign_key_spec.rb'
|
||||
- 'spec/rubocop/cop/filename_length_spec.rb'
|
||||
- 'spec/rubocop/cop/put_project_routes_under_scope_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/rails_logger_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/avoid_uploaded_file_from_params_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/bulk_insert_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/intersect_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/json_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/httparty_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/policy_rule_boolean_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/except_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/change_timezone_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/predicate_memoization_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/union_spec.rb'
|
||||
- 'spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb'
|
||||
- 'spec/rubocop/cop/ruby_interpolation_in_translation_spec.rb'
|
||||
- 'spec/rubocop/cop/active_record_association_reload_spec.rb'
|
||||
- 'spec/rubocop/cop/ban_catch_throw_spec.rb'
|
||||
- 'spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb'
|
||||
- 'spec/rubocop/cop/avoid_becomes_spec.rb'
|
||||
- 'spec/rubocop/cop/qa/ambiguous_page_object_name_spec.rb'
|
||||
- 'spec/rubocop/cop/qa/element_with_pattern_spec.rb'
|
||||
- 'spec/rubocop/cop/inject_enterprise_edition_module_spec.rb'
|
||||
- 'spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb'
|
||||
- 'spec/rubocop/cop/default_scope_spec.rb'
|
||||
- 'spec/rubocop/cop/scalability/bulk_perform_with_context_spec.rb'
|
||||
- 'spec/rubocop/cop/scalability/idempotent_worker_spec.rb'
|
||||
- 'spec/rubocop/cop/scalability/cron_worker_context_spec.rb'
|
||||
|
|
|
@ -3,10 +3,7 @@
|
|||
require_relative 'tooling/gitlab_danger'
|
||||
require_relative 'tooling/danger/request_helper'
|
||||
|
||||
danger.import_plugin('danger/plugins/helper.rb')
|
||||
danger.import_plugin('danger/plugins/roulette.rb')
|
||||
danger.import_plugin('danger/plugins/changelog.rb')
|
||||
danger.import_plugin('danger/plugins/sidekiq_queues.rb')
|
||||
Dir["danger/plugins/*.rb"].sort.each { |f| danger.import_plugin(f) }
|
||||
|
||||
return if helper.release_automation?
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import initFilteredSearch from '~/pages/search/init_filtered_search';
|
||||
import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
|
||||
import { FILTERED_SEARCH } from '~/pages/constants';
|
||||
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initFilteredSearch({
|
||||
|
@ -8,4 +9,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
|
||||
useDefaultState: true,
|
||||
});
|
||||
|
||||
if (gon?.features?.runnerInstructions) {
|
||||
initInstallRunner();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
|
|||
import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
|
||||
import { FILTERED_SEARCH } from '~/pages/constants';
|
||||
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
|
||||
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize expandable settings panels
|
||||
|
@ -18,4 +19,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
initSharedRunnersForm();
|
||||
initVariableList();
|
||||
|
||||
if (gon?.features?.runnerInstructions) {
|
||||
initInstallRunner();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import initDeployFreeze from '~/deploy_freeze';
|
|||
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
|
||||
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
|
||||
import initArtifactsSettings from '~/artifacts_settings';
|
||||
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize expandable settings panels
|
||||
|
@ -39,4 +40,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (gon?.features?.vueifySharedRunnersToggle) {
|
||||
initSharedRunnersToggle();
|
||||
}
|
||||
|
||||
if (gon?.features?.runnerInstructions) {
|
||||
initInstallRunner();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import InstallRunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
export function initInstallRunner(componentId = 'js-install-runner') {
|
||||
const installRunnerEl = document.getElementById(componentId);
|
||||
|
||||
if (installRunnerEl) {
|
||||
const defaultClient = createDefaultClient();
|
||||
const { projectPath, groupPath } = installRunnerEl.dataset;
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: installRunnerEl,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
projectPath,
|
||||
groupPath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(InstallRunnerInstructions);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
|
||||
import ciHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
const DELETE_MODAL_ID = 'pipeline-delete-modal';
|
||||
|
||||
export default {
|
||||
name: 'PipelineHeaderSection',
|
||||
components: {
|
||||
ciHeader,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCanceling: false,
|
||||
isRetrying: false,
|
||||
isDeleting: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
status() {
|
||||
return this.pipeline.details && this.pipeline.details.status;
|
||||
},
|
||||
shouldRenderContent() {
|
||||
return !this.isLoading && Object.keys(this.pipeline).length;
|
||||
},
|
||||
deleteModalConfirmationText() {
|
||||
return __(
|
||||
'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cancelPipeline() {
|
||||
this.isCanceling = true;
|
||||
eventHub.$emit('headerPostAction', this.pipeline.cancel_path);
|
||||
},
|
||||
retryPipeline() {
|
||||
this.isRetrying = true;
|
||||
eventHub.$emit('headerPostAction', this.pipeline.retry_path);
|
||||
},
|
||||
deletePipeline() {
|
||||
this.isDeleting = true;
|
||||
eventHub.$emit('headerDeleteAction', this.pipeline.delete_path);
|
||||
},
|
||||
},
|
||||
DELETE_MODAL_ID,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="pipeline-header-container">
|
||||
<ci-header
|
||||
v-if="shouldRenderContent"
|
||||
:status="status"
|
||||
:item-id="pipeline.id"
|
||||
:time="pipeline.created_at"
|
||||
:user="pipeline.user"
|
||||
item-name="Pipeline"
|
||||
>
|
||||
<gl-button
|
||||
v-if="pipeline.retry_path"
|
||||
:loading="isRetrying"
|
||||
:disabled="isRetrying"
|
||||
data-testid="retryButton"
|
||||
category="secondary"
|
||||
variant="info"
|
||||
@click="retryPipeline()"
|
||||
>
|
||||
{{ __('Retry') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.cancel_path"
|
||||
:loading="isCanceling"
|
||||
:disabled="isCanceling"
|
||||
data-testid="cancelPipeline"
|
||||
class="gl-ml-3"
|
||||
category="primary"
|
||||
variant="danger"
|
||||
@click="cancelPipeline()"
|
||||
>
|
||||
{{ __('Cancel running') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="pipeline.delete_path"
|
||||
v-gl-modal="$options.DELETE_MODAL_ID"
|
||||
:loading="isDeleting"
|
||||
:disabled="isDeleting"
|
||||
data-testid="deletePipeline"
|
||||
class="gl-ml-3"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
>
|
||||
{{ __('Delete') }}
|
||||
</gl-button>
|
||||
</ci-header>
|
||||
|
||||
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" />
|
||||
|
||||
<gl-modal
|
||||
:modal-id="$options.DELETE_MODAL_ID"
|
||||
:title="__('Delete pipeline')"
|
||||
:ok-title="__('Delete pipeline')"
|
||||
ok-variant="danger"
|
||||
@ok="deletePipeline()"
|
||||
>
|
||||
<p>
|
||||
{{ deleteModalConfirmationText }}
|
||||
</p>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
|
@ -2,12 +2,9 @@ import Vue from 'vue';
|
|||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import { __ } from '~/locale';
|
||||
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
|
||||
import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue';
|
||||
import createDagApp from './pipeline_details_dag';
|
||||
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
|
||||
import legacyPipelineHeader from './components/legacy_header_component.vue';
|
||||
import eventHub from './event_hub';
|
||||
import TestReports from './components/test_reports/test_reports.vue';
|
||||
import createTestReportsStore from './stores/test_reports';
|
||||
import { reportToSentry } from './components/graph/utils';
|
||||
|
@ -59,58 +56,6 @@ const createLegacyPipelinesDetailApp = (mediator) => {
|
|||
});
|
||||
};
|
||||
|
||||
const createLegacyPipelineHeaderApp = (mediator) => {
|
||||
if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: SELECTORS.PIPELINE_HEADER,
|
||||
components: {
|
||||
legacyPipelineHeader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mediator,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('headerPostAction', this.postAction);
|
||||
eventHub.$on('headerDeleteAction', this.deleteAction);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('headerPostAction', this.postAction);
|
||||
eventHub.$off('headerDeleteAction', this.deleteAction);
|
||||
},
|
||||
errorCaptured(err, _vm, info) {
|
||||
reportToSentry('pipeline_details_bundle_legacy', `error: ${err}, info: ${info}`);
|
||||
},
|
||||
methods: {
|
||||
postAction(path) {
|
||||
this.mediator.service
|
||||
.postAction(path)
|
||||
.then(() => this.mediator.refreshPipeline())
|
||||
.catch(() => Flash(__('An error occurred while making the request.')));
|
||||
},
|
||||
deleteAction(path) {
|
||||
this.mediator.stopPipelinePoll();
|
||||
this.mediator.service
|
||||
.deleteAction(path)
|
||||
.then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success')))
|
||||
.catch(() => Flash(__('An error occurred while deleting the pipeline.')));
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('legacy-pipeline-header', {
|
||||
props: {
|
||||
isLoading: this.mediator.state.isLoading,
|
||||
pipeline: this.mediator.store.state.pipeline,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createTestDetails = () => {
|
||||
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
|
||||
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
|
||||
|
@ -140,19 +85,6 @@ export default async function () {
|
|||
gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers;
|
||||
|
||||
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
|
||||
let mediator;
|
||||
|
||||
if (!gon.features.graphqlPipelineHeader || !canShowNewPipelineDetails) {
|
||||
try {
|
||||
const { default: PipelinesMediator } = await import(
|
||||
/* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
|
||||
);
|
||||
mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
|
||||
mediator.fetchPipeline();
|
||||
} catch {
|
||||
Flash(__('An error occurred while loading the pipeline.'));
|
||||
}
|
||||
}
|
||||
|
||||
if (canShowNewPipelineDetails) {
|
||||
try {
|
||||
|
@ -166,19 +98,21 @@ export default async function () {
|
|||
Flash(__('An error occurred while loading the pipeline.'));
|
||||
}
|
||||
} else {
|
||||
const { default: PipelinesMediator } = await import(
|
||||
/* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
|
||||
);
|
||||
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
|
||||
mediator.fetchPipeline();
|
||||
|
||||
createLegacyPipelinesDetailApp(mediator);
|
||||
}
|
||||
|
||||
if (gon.features.graphqlPipelineHeader) {
|
||||
try {
|
||||
const { createPipelineHeaderApp } = await import(
|
||||
/* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header'
|
||||
);
|
||||
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER);
|
||||
} catch {
|
||||
Flash(__('An error occurred while loading a section of this page.'));
|
||||
}
|
||||
} else {
|
||||
createLegacyPipelineHeaderApp(mediator);
|
||||
try {
|
||||
const { createPipelineHeaderApp } = await import(
|
||||
/* webpackChunkName: 'createPipelineHeaderApp' */ './pipeline_details_header'
|
||||
);
|
||||
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER);
|
||||
} catch {
|
||||
Flash(__('An error occurred while loading a section of this page.'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,14 @@ export default () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { endpoint, expirationPolicy, isGroupPage, isAdmin, ...config } = el.dataset;
|
||||
const {
|
||||
endpoint,
|
||||
expirationPolicy,
|
||||
isGroupPage,
|
||||
isAdmin,
|
||||
showUnfinishedTagCleanupCallout,
|
||||
...config
|
||||
} = el.dataset;
|
||||
|
||||
// This is a mini state to help the breadcrumb have the correct name in the details page
|
||||
const breadCrumbState = Vue.observable({
|
||||
|
@ -57,6 +64,7 @@ export default () => {
|
|||
expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined,
|
||||
isGroupPage: parseBoolean(isGroupPage),
|
||||
isAdmin: parseBoolean(isAdmin),
|
||||
showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout),
|
||||
},
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
dockerBuildCommand: `docker build -t ${config.repositoryUrl} .`,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui';
|
|||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import createFlash from '~/flash';
|
||||
import Tracking from '~/tracking';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import DeleteAlert from '../components/details_page/delete_alert.vue';
|
||||
import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
|
||||
|
@ -68,7 +69,7 @@ export default {
|
|||
isMobile: false,
|
||||
mutationLoading: false,
|
||||
deleteAlertType: null,
|
||||
dismissPartialCleanupWarning: false,
|
||||
hidePartialCleanupWarning: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -86,8 +87,9 @@ export default {
|
|||
},
|
||||
showPartialCleanupWarning() {
|
||||
return (
|
||||
this.config.showUnfinishedTagCleanupCallout &&
|
||||
this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
|
||||
!this.dismissPartialCleanupWarning
|
||||
!this.hidePartialCleanupWarning
|
||||
);
|
||||
},
|
||||
tracking() {
|
||||
|
@ -168,6 +170,12 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
dismissPartialCleanupWarning() {
|
||||
this.hidePartialCleanupWarning = true;
|
||||
axios.post(this.config.userCalloutsPath, {
|
||||
feature_name: this.config.userCalloutId,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -185,7 +193,7 @@ export default {
|
|||
v-if="showPartialCleanupWarning"
|
||||
:run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
|
||||
:cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
|
||||
@dismiss="dismissPartialCleanupWarning = true"
|
||||
@dismiss="dismissPartialCleanupWarning"
|
||||
/>
|
||||
|
||||
<details-header :image="image" :metadata-loading="isLoading" />
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
|
||||
|
||||
export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
|
||||
docker: {
|
||||
instructions: s__(
|
||||
'Runners|To install Runner in a container follow the instructions described in the GitLab documentation',
|
||||
),
|
||||
link: 'https://docs.gitlab.com/runner/install/docker.html',
|
||||
},
|
||||
kubernetes: {
|
||||
instructions: s__(
|
||||
'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
|
||||
),
|
||||
link: 'https://docs.gitlab.com/runner/install/kubernetes.html',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) {
|
||||
runnerPlatforms {
|
||||
nodes {
|
||||
name
|
||||
humanReadableName
|
||||
architectures {
|
||||
nodes {
|
||||
name
|
||||
downloadLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
}
|
||||
group(fullPath: $groupPath) {
|
||||
id
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
query runnerSetupInstructions(
|
||||
$platform: String!
|
||||
$architecture: String!
|
||||
$projectId: ID!
|
||||
$groupId: ID!
|
||||
) {
|
||||
runnerSetup(
|
||||
platform: $platform
|
||||
architecture: $architecture
|
||||
projectId: $projectId
|
||||
groupId: $groupId
|
||||
) {
|
||||
installInstructions
|
||||
registerInstructions
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlModal,
|
||||
GlModalDirective,
|
||||
GlButtonGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
import {
|
||||
PLATFORMS_WITHOUT_ARCHITECTURES,
|
||||
INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
|
||||
} from './constants';
|
||||
import getRunnerPlatforms from './graphql/queries/get_runner_platforms.query.graphql';
|
||||
import getRunnerSetupInstructions from './graphql/queries/get_runner_setup.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlButtonGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlModal,
|
||||
GlIcon,
|
||||
ModalCopyButton,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
inject: {
|
||||
projectPath: {
|
||||
default: '',
|
||||
},
|
||||
groupPath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
runnerPlatforms: {
|
||||
query: getRunnerPlatforms,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
groupPath: this.groupPath,
|
||||
};
|
||||
},
|
||||
error() {
|
||||
this.showAlert = true;
|
||||
},
|
||||
result({ data }) {
|
||||
this.project = data?.project;
|
||||
this.group = data?.group;
|
||||
|
||||
this.selectPlatform(this.platforms[0].name);
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAlert: false,
|
||||
selectedPlatformArchitectures: [],
|
||||
selectedPlatform: {
|
||||
name: '',
|
||||
},
|
||||
selectedArchitecture: {},
|
||||
runnerPlatforms: {},
|
||||
instructions: {},
|
||||
project: {},
|
||||
group: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isPlatformSelected() {
|
||||
return Object.keys(this.selectedPlatform).length > 0;
|
||||
},
|
||||
instructionsEmpty() {
|
||||
return Object.keys(this.instructions).length === 0;
|
||||
},
|
||||
groupId() {
|
||||
return this.group?.id ?? '';
|
||||
},
|
||||
projectId() {
|
||||
return this.project?.id ?? '';
|
||||
},
|
||||
platforms() {
|
||||
return this.runnerPlatforms?.nodes;
|
||||
},
|
||||
hasArchitecureList() {
|
||||
return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatform?.name);
|
||||
},
|
||||
instructionsWithoutArchitecture() {
|
||||
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.instructions;
|
||||
},
|
||||
runnerInstallationLink() {
|
||||
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.link;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectPlatform(name) {
|
||||
this.selectedPlatform = this.platforms.find((platform) => platform.name === name);
|
||||
if (this.hasArchitecureList) {
|
||||
this.selectedPlatformArchitectures = this.selectedPlatform?.architectures?.nodes;
|
||||
[this.selectedArchitecture] = this.selectedPlatformArchitectures;
|
||||
this.selectArchitecture(this.selectedArchitecture);
|
||||
}
|
||||
},
|
||||
selectArchitecture(architecture) {
|
||||
this.selectedArchitecture = architecture;
|
||||
|
||||
this.$apollo.addSmartQuery('instructions', {
|
||||
variables() {
|
||||
return {
|
||||
platform: this.selectedPlatform.name,
|
||||
architecture: this.selectedArchitecture.name,
|
||||
projectId: this.projectId,
|
||||
groupId: this.groupId,
|
||||
};
|
||||
},
|
||||
query: getRunnerSetupInstructions,
|
||||
update(data) {
|
||||
return data?.runnerSetup;
|
||||
},
|
||||
error() {
|
||||
this.showAlert = true;
|
||||
},
|
||||
});
|
||||
},
|
||||
toggleAlert(state) {
|
||||
this.showAlert = state;
|
||||
},
|
||||
},
|
||||
modalId: 'installation-instructions-modal',
|
||||
i18n: {
|
||||
installARunner: s__('Runners|Install a Runner'),
|
||||
architecture: s__('Runners|Architecture'),
|
||||
downloadInstallBinary: s__('Runners|Download and Install Binary'),
|
||||
downloadLatestBinary: s__('Runners|Download Latest Binary'),
|
||||
registerRunner: s__('Runners|Register Runner'),
|
||||
method: __('Method'),
|
||||
fetchError: s__('Runners|An error has occurred fetching instructions'),
|
||||
instructions: s__('Runners|Show Runner installation instructions'),
|
||||
copyInstructions: s__('Runners|Copy instructions'),
|
||||
},
|
||||
closeButton: {
|
||||
text: __('Close'),
|
||||
attributes: [{ variant: 'default' }],
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-button
|
||||
v-gl-modal-directive="$options.modalId"
|
||||
class="gl-mt-4"
|
||||
data-testid="show-modal-button"
|
||||
>
|
||||
{{ $options.i18n.instructions }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
:modal-id="$options.modalId"
|
||||
:title="$options.i18n.installARunner"
|
||||
:action-secondary="$options.closeButton"
|
||||
>
|
||||
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
|
||||
{{ $options.i18n.fetchError }}
|
||||
</gl-alert>
|
||||
<h5>{{ __('Environment') }}</h5>
|
||||
<gl-button-group class="gl-mb-5">
|
||||
<gl-button
|
||||
v-for="platform in platforms"
|
||||
:key="platform.name"
|
||||
data-testid="platform-button"
|
||||
@click="selectPlatform(platform.name)"
|
||||
>
|
||||
{{ platform.humanReadableName }}
|
||||
</gl-button>
|
||||
</gl-button-group>
|
||||
<template v-if="hasArchitecureList">
|
||||
<template v-if="isPlatformSelected">
|
||||
<h5>
|
||||
{{ $options.i18n.architecture }}
|
||||
</h5>
|
||||
<gl-dropdown class="gl-mb-5" :text="selectedArchitecture.name">
|
||||
<gl-dropdown-item
|
||||
v-for="architecture in selectedPlatformArchitectures"
|
||||
:key="architecture.name"
|
||||
data-testid="architecture-dropdown-item"
|
||||
@click="selectArchitecture(architecture)"
|
||||
>
|
||||
{{ architecture.name }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
<div class="gl-display-flex gl-align-items-center gl-mb-5">
|
||||
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
|
||||
<gl-button
|
||||
class="gl-ml-auto"
|
||||
:href="selectedArchitecture.downloadLocation"
|
||||
download
|
||||
data-testid="binary-download-button"
|
||||
>
|
||||
{{ $options.i18n.downloadLatestBinary }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!instructionsEmpty">
|
||||
<div class="gl-display-flex">
|
||||
<pre
|
||||
class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
|
||||
data-testid="binary-instructions"
|
||||
>
|
||||
|
||||
{{ instructions.installInstructions }}
|
||||
</pre
|
||||
>
|
||||
<modal-copy-button
|
||||
:title="$options.i18n.copyInstructions"
|
||||
:text="instructions.installInstructions"
|
||||
:modal-id="$options.modalId"
|
||||
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
|
||||
category="tertiary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<h5 class="gl-mb-5">{{ $options.i18n.registerRunner }}</h5>
|
||||
<h5 class="gl-mb-5">{{ $options.i18n.method }}</h5>
|
||||
<div class="gl-display-flex">
|
||||
<pre
|
||||
class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
|
||||
data-testid="runner-instructions"
|
||||
>
|
||||
{{ instructions.registerInstructions }}
|
||||
</pre
|
||||
>
|
||||
<modal-copy-button
|
||||
:title="$options.i18n.copyInstructions"
|
||||
:text="instructions.registerInstructions"
|
||||
:modal-id="$options.modalId"
|
||||
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
|
||||
category="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<p>{{ instructionsWithoutArchitecture }}</p>
|
||||
<gl-button :href="runnerInstallationLink">
|
||||
<gl-icon name="external-link" />
|
||||
{{ s__('Runners|View installation instructions') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
|
@ -1,4 +1,3 @@
|
|||
@import './pages/admin';
|
||||
@import './pages/branches';
|
||||
@import './pages/ci_projects';
|
||||
@import './pages/clusters';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.usage-data {
|
||||
max-height: 400px;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.admin-builds-table {
|
||||
td:last-child {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
.usage-data {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
[data-page='admin:jobs:index'] {
|
||||
.admin-builds-table {
|
||||
td:last-child {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,9 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
include RunnerSetupScripts
|
||||
|
||||
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:runner_instructions, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
feature_category :continuous_integration
|
||||
|
||||
|
|
|
@ -9,6 +9,9 @@ module Groups
|
|||
before_action :authorize_admin_group!
|
||||
before_action :authorize_update_max_artifacts_size!, only: [:update]
|
||||
before_action :define_variables, only: [:show]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:runner_instructions, @group, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
feature_category :continuous_integration
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
before_action do
|
||||
push_frontend_feature_flag(:pipelines_security_report_summary, project)
|
||||
push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false)
|
||||
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development, default_enabled: true)
|
||||
|
|
|
@ -12,6 +12,7 @@ module Projects
|
|||
before_action do
|
||||
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
|
||||
push_frontend_feature_flag(:vueify_shared_runners_toggle, @project)
|
||||
push_frontend_feature_flag(:runner_instructions, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
helper_method :highlight_badge
|
||||
|
|
|
@ -4,7 +4,7 @@ module Mutations
|
|||
module AlertManagement
|
||||
module HttpIntegration
|
||||
class Create < HttpIntegrationBase
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'HttpIntegrationCreate'
|
||||
|
||||
|
@ -21,7 +21,7 @@ module Mutations
|
|||
description: 'Whether the integration is receiving alerts.'
|
||||
|
||||
def resolve(args)
|
||||
project = authorized_find!(full_path: args[:project_path])
|
||||
project = authorized_find!(args[:project_path])
|
||||
|
||||
response ::AlertManagement::HttpIntegrations::CreateService.new(
|
||||
project,
|
||||
|
@ -29,12 +29,6 @@ module Mutations
|
|||
http_integration_params(project, args)
|
||||
).execute
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module Mutations
|
|||
module AlertManagement
|
||||
module PrometheusIntegration
|
||||
class Create < PrometheusIntegrationBase
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'PrometheusIntegrationCreate'
|
||||
|
||||
|
@ -21,7 +21,7 @@ module Mutations
|
|||
description: 'Endpoint at which prometheus can be queried.'
|
||||
|
||||
def resolve(args)
|
||||
project = authorized_find!(full_path: args[:project_path])
|
||||
project = authorized_find!(args[:project_path])
|
||||
|
||||
return integration_exists if project.prometheus_service
|
||||
|
||||
|
@ -37,10 +37,6 @@ module Mutations
|
|||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
|
||||
def integration_exists
|
||||
response(nil, message: _('Multiple Prometheus integrations are not supported'))
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Mutations
|
||||
module Branches
|
||||
class Create < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'CreateBranch'
|
||||
|
||||
|
@ -28,7 +28,7 @@ module Mutations
|
|||
authorize :push_code
|
||||
|
||||
def resolve(project_path:, name:, ref:)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
context.scoped_set!(:branch_project, project)
|
||||
|
||||
|
@ -40,12 +40,6 @@ module Mutations
|
|||
errors: Array.wrap(result[:message])
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Mutations
|
||||
module Commits
|
||||
class Create < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'CommitCreate'
|
||||
|
||||
|
@ -37,7 +37,7 @@ module Mutations
|
|||
authorize :push_code
|
||||
|
||||
def resolve(project_path:, branch:, message:, actions:, **args)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
attributes = {
|
||||
commit_message: message,
|
||||
|
@ -53,12 +53,6 @@ module Mutations
|
|||
errors: Array.wrap(result[:message])
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Mutations
|
||||
module ContainerExpirationPolicies
|
||||
class Update < Mutations::BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'UpdateContainerExpirationPolicy'
|
||||
|
||||
|
@ -50,7 +50,7 @@ module Mutations
|
|||
description: 'The container expiration policy after mutation.'
|
||||
|
||||
def resolve(project_path:, **args)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
result = ::ContainerExpirationPolicies::UpdateService
|
||||
.new(container: project, current_user: current_user, params: args)
|
||||
|
@ -61,12 +61,6 @@ module Mutations
|
|||
errors: result.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Mutations
|
||||
module Issues
|
||||
class Create < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
graphql_name 'CreateIssue'
|
||||
|
||||
authorize :create_issue
|
||||
|
@ -70,7 +70,7 @@ module Mutations
|
|||
end
|
||||
|
||||
def resolve(project_path:, **attributes)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
params = build_create_issue_params(attributes.merge(author_id: current_user.id))
|
||||
|
||||
issue = ::Issues::CreateService.new(project, current_user, params).execute
|
||||
|
@ -98,10 +98,6 @@ module Mutations
|
|||
def mutually_exclusive_label_args
|
||||
[:labels, :label_ids]
|
||||
end
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
module Mutations
|
||||
module JiraImport
|
||||
class ImportUsers < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'JiraImportUsers'
|
||||
|
||||
authorize :admin_project
|
||||
|
||||
field :jira_users,
|
||||
[Types::JiraUserType],
|
||||
null: true,
|
||||
|
@ -20,7 +22,7 @@ module Mutations
|
|||
description: 'The index of the record the import should started at, default 0 (50 records returned).'
|
||||
|
||||
def resolve(project_path:, start_at: 0)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
service_response = ::JiraImport::UsersImporter.new(context[:current_user], project, start_at.to_i).execute
|
||||
|
||||
|
@ -29,16 +31,6 @@ module Mutations
|
|||
errors: service_response.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
|
||||
def authorized_resource?(project)
|
||||
Ability.allowed?(context[:current_user], :admin_project, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
module Mutations
|
||||
module JiraImport
|
||||
class Start < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'JiraImportStart'
|
||||
|
||||
authorize :admin_project
|
||||
|
||||
field :jira_import,
|
||||
Types::JiraImportType,
|
||||
null: true,
|
||||
|
@ -27,7 +29,7 @@ module Mutations
|
|||
description: 'The mapping of Jira to GitLab users.'
|
||||
|
||||
def resolve(project_path:, jira_project_key:, users_mapping:)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
mapping = users_mapping.to_ary.map { |map| map.to_hash }
|
||||
|
||||
service_response = ::JiraImport::StartImportService
|
||||
|
@ -40,16 +42,6 @@ module Mutations
|
|||
errors: service_response.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
|
||||
def authorized_resource?(project)
|
||||
Ability.allowed?(context[:current_user], :admin_project, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Mutations
|
||||
module MergeRequests
|
||||
class Create < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'MergeRequestCreate'
|
||||
|
||||
|
@ -39,7 +39,7 @@ module Mutations
|
|||
authorize :create_merge_request_from
|
||||
|
||||
def resolve(project_path:, **attributes)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
params = attributes.merge(author_id: current_user.id)
|
||||
|
||||
merge_request = ::MergeRequests::CreateService.new(project, current_user, params).execute
|
||||
|
@ -49,12 +49,6 @@ module Mutations
|
|||
errors: errors_on_object(merge_request)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,17 +3,11 @@
|
|||
module Mutations
|
||||
module Releases
|
||||
class Base < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
argument :project_path, GraphQL::ID_TYPE,
|
||||
required: true,
|
||||
description: 'Full path of the project the release is associated with.'
|
||||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ module Mutations
|
|||
authorize :create_release
|
||||
|
||||
def resolve(project_path:, assets: nil, **scalars)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
params = {
|
||||
**scalars,
|
||||
|
|
|
@ -17,7 +17,7 @@ module Mutations
|
|||
authorize :destroy_release
|
||||
|
||||
def resolve(project_path:, tag:)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
params = { tag: tag }.with_indifferent_access
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ module Mutations
|
|||
end
|
||||
|
||||
def resolve(project_path:, **scalars)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
params = scalars.with_indifferent_access
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ module Mutations
|
|||
module Security
|
||||
module CiConfiguration
|
||||
class ConfigureSast < BaseMutation
|
||||
include ResolvesProject
|
||||
include FindsProject
|
||||
|
||||
graphql_name 'ConfigureSast'
|
||||
|
||||
|
@ -25,7 +25,7 @@ module Mutations
|
|||
authorize :push_code
|
||||
|
||||
def resolve(project_path:, configuration:)
|
||||
project = authorized_find!(full_path: project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
result = ::Security::CiConfiguration::SastCreateService.new(project, current_user, configuration).execute
|
||||
prepare_response(result)
|
||||
|
@ -33,10 +33,6 @@ module Mutations
|
|||
|
||||
private
|
||||
|
||||
def find_object(full_path:)
|
||||
resolve_project(full_path: full_path)
|
||||
end
|
||||
|
||||
def prepare_response(result)
|
||||
{
|
||||
status: result[:status],
|
||||
|
|
|
@ -11,6 +11,7 @@ module UserCalloutsHelper
|
|||
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
|
||||
FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
|
||||
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
|
||||
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
|
||||
|
||||
def show_admin_integrations_moved?
|
||||
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
|
||||
|
@ -56,6 +57,10 @@ module UserCalloutsHelper
|
|||
!user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
|
||||
end
|
||||
|
||||
def show_unfinished_tag_cleanup_callout?
|
||||
!user_dismissed?(UNFINISHED_TAG_CLEANUP_CALLOUT)
|
||||
end
|
||||
|
||||
def show_registration_enabled_user_callout?
|
||||
!Gitlab.com? &&
|
||||
current_user&.admin? &&
|
||||
|
|
|
@ -5,6 +5,7 @@ module Pages
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
LegacyStorageDisabledError = Class.new(::StandardError)
|
||||
MIGRATED_FILE_NAME = "_migrated.zip"
|
||||
|
||||
def initialize(project, trim_prefix: nil, domain: nil)
|
||||
@project = project
|
||||
|
@ -54,6 +55,8 @@ module Pages
|
|||
|
||||
return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project)
|
||||
|
||||
return if deployment.file.filename == MIGRATED_FILE_NAME && !Feature.enabled?(:pages_serve_from_migrated_zip, project)
|
||||
|
||||
global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
|
||||
|
||||
{
|
||||
|
|
|
@ -27,7 +27,8 @@ class UserCallout < ApplicationRecord
|
|||
customize_homepage: 23,
|
||||
feature_flags_new_version: 24,
|
||||
registration_enabled_callout: 25,
|
||||
new_user_signups_cap_reached: 26 # EE-only
|
||||
new_user_signups_cap_reached: 26, # EE-only
|
||||
unfinished_tag_cleanup_callout: 27
|
||||
}
|
||||
|
||||
validates :user, presence: true
|
||||
|
|
|
@ -16,6 +16,7 @@ module Git
|
|||
wiki.after_post_receive
|
||||
|
||||
process_changes
|
||||
perform_housekeeping if Feature.enabled?(:wiki_housekeeping, wiki.container)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -72,6 +73,14 @@ module Git
|
|||
def default_branch_changes
|
||||
@default_branch_changes ||= changes.select { |change| on_default_branch?(change) }
|
||||
end
|
||||
|
||||
def perform_housekeeping
|
||||
housekeeping = Repositories::HousekeepingService.new(wiki)
|
||||
housekeeping.increment!
|
||||
housekeeping.execute if housekeeping.needed?
|
||||
rescue Repositories::HousekeepingService::LeaseTaken
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- add_page_specific_style 'page_bundles/admin/application_settings_metrics_and_profiling'
|
||||
|
||||
- breadcrumb_title _("Metrics and profiling")
|
||||
- page_title _("Metrics and profiling")
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- add_page_specific_style 'page_bundles/ci_status'
|
||||
- add_page_specific_style 'page_bundles/admin/jobs_index'
|
||||
|
||||
- breadcrumb_title _("Jobs")
|
||||
- page_title _("Jobs")
|
||||
|
|
|
@ -39,7 +39,9 @@
|
|||
= render partial: 'ci/runner/how_to_setup_runner',
|
||||
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
||||
type: 'shared',
|
||||
reset_token_url: reset_registration_token_admin_application_settings_path }
|
||||
reset_token_url: reset_registration_token_admin_application_settings_path,
|
||||
project_path: '',
|
||||
group_path: '' }
|
||||
|
||||
.row
|
||||
.col-sm-9
|
||||
|
|
|
@ -21,3 +21,5 @@
|
|||
= button_to _("Reset registration token"), reset_token_url,
|
||||
method: :put, class: 'gl-button btn btn-default',
|
||||
data: { confirm: _("Are you sure you want to reset the registration token?") }
|
||||
|
||||
#js-install-runner{ data: { project_path: project_path, group_path: group_path } }
|
||||
|
|
|
@ -17,4 +17,7 @@
|
|||
is_group_page: "true",
|
||||
"group_path": @group.full_path,
|
||||
"gid_prefix": container_repository_gid_prefix,
|
||||
character_error: @character_error.to_s } }
|
||||
character_error: @character_error.to_s,
|
||||
user_callouts_path: user_callouts_path,
|
||||
user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
|
||||
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s } }
|
||||
|
|
|
@ -17,5 +17,7 @@
|
|||
= render partial: 'ci/runner/how_to_setup_runner',
|
||||
locals: { registration_token: @group.runners_token,
|
||||
type: 'group',
|
||||
reset_token_url: reset_registration_token_group_settings_ci_cd_path }
|
||||
reset_token_url: reset_registration_token_group_settings_ci_cd_path,
|
||||
project_path: '',
|
||||
group_path: @group.path }
|
||||
%br
|
||||
|
|
|
@ -19,4 +19,7 @@
|
|||
"project_path": @project.full_path,
|
||||
"gid_prefix": container_repository_gid_prefix,
|
||||
"is_admin": current_user&.admin.to_s,
|
||||
character_error: @character_error.to_s } }
|
||||
character_error: @character_error.to_s,
|
||||
user_callouts_path: user_callouts_path,
|
||||
user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
|
||||
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s, } }
|
||||
|
|
|
@ -9,9 +9,11 @@
|
|||
clusters_path: project_clusters_path(@project) }
|
||||
%hr
|
||||
= render partial: 'ci/runner/how_to_setup_runner',
|
||||
locals: { registration_token: @project.runners_token,
|
||||
type: 'specific',
|
||||
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path }
|
||||
locals: { registration_token: @project.runners_token,
|
||||
type: 'specific',
|
||||
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path,
|
||||
project_path: @project.path_with_namespace,
|
||||
group_path: '' }
|
||||
|
||||
%hr
|
||||
|
||||
|
|
|
@ -7,6 +7,17 @@ module Projects
|
|||
|
||||
private
|
||||
|
||||
# At the moment this was added, the default key was like this.
|
||||
# With the addition of wikis to housekeeping, this will bring a
|
||||
# problem because the wiki for project 1 will have the same
|
||||
# lease key as project 1.
|
||||
#
|
||||
# In the `GitGarbageCollectMethods` we namespaced the resource,
|
||||
# giving us the option to have different resources. Nevertheless,
|
||||
# we kept this override in order for backward compatibility and avoid
|
||||
# starting all projects from scratch.
|
||||
#
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/299903
|
||||
override :default_lease_key
|
||||
def default_lease_key(task, resource)
|
||||
"git_gc:#{task}:#{resource.id}"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add callout disabling feature to cleanup policy alert
|
||||
merge_request: 52327
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove graphql_pipeline_header feature flag
|
||||
merge_request: 52247
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'BulkImports: Import Group Labels'
|
||||
merge_request: 52260
|
||||
author:
|
||||
type: added
|
|
@ -176,6 +176,8 @@ module Gitlab
|
|||
config.assets.precompile << "notify.css"
|
||||
config.assets.precompile << "mailers/*.css"
|
||||
config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css"
|
||||
config.assets.precompile << "page_bundles/admin/application_settings_metrics_and_profiling.css"
|
||||
config.assets.precompile << "page_bundles/admin/jobs_index.css"
|
||||
config.assets.precompile << "page_bundles/alert_management_details.css"
|
||||
config.assets.precompile << "page_bundles/alert_management_settings.css"
|
||||
config.assets.precompile << "page_bundles/boards.css"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: graphql_pipeline_header
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39494
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254235
|
||||
milestone: '13.5'
|
||||
name: pages_serve_from_migrated_zip
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52573
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300021
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
group: group::release
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: runner_instructions
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51014
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296818
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::"continuous integration"
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: wiki_housekeeping
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51576
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299343
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::editor
|
||||
default_enabled: false
|
|
@ -35,7 +35,7 @@ def check_changelog_yaml(path)
|
|||
elsif yaml["merge_request"] != gitlab.mr_json["iid"] && !cherry_pick_against_stable_branch
|
||||
fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
|
||||
end
|
||||
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
|
||||
rescue Psych::Exception
|
||||
# YAML could not be parsed, fail the build.
|
||||
fail "#{gitlab.html_link(path)} isn't valid YAML! #{SEE_DOC}"
|
||||
rescue StandardError => e
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
# rubocop:disable Style/SignalException
|
||||
|
||||
SEE_DOC = "See the [feature flag documentation](https://docs.gitlab.com/ee/development/feature_flags/development.html#feature-flag-definition-and-validation)."
|
||||
|
||||
SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT
|
||||
```suggestion
|
||||
group: "%<group>s"
|
||||
```
|
||||
|
||||
#{SEE_DOC}
|
||||
SUGGEST_COMMENT
|
||||
|
||||
def check_feature_flag_yaml(feature_flag)
|
||||
mr_group_label = helper.group_label(gitlab.mr_labels)
|
||||
|
||||
if feature_flag.group.nil?
|
||||
message_for_feature_flag_missing_group!(feature_flag: feature_flag, mr_group_label: mr_group_label)
|
||||
else
|
||||
message_for_feature_flag_with_group!(feature_flag: feature_flag, mr_group_label: mr_group_label)
|
||||
end
|
||||
rescue Psych::Exception
|
||||
# YAML could not be parsed, fail the build.
|
||||
fail "#{gitlab.html_link(feature_flag.path)} isn't valid YAML! #{SEE_DOC}"
|
||||
rescue StandardError => e
|
||||
warn "There was a problem trying to check the Feature Flag file. Exception: #{e.class.name} - #{e.message}"
|
||||
end
|
||||
|
||||
def message_for_feature_flag_missing_group!(feature_flag:, mr_group_label:)
|
||||
if mr_group_label.nil?
|
||||
warn "Consider setting `group` in #{gitlab.html_link(feature_flag.path)}. #{SEE_DOC}"
|
||||
else
|
||||
mr_line = feature_flag.raw.lines.find_index("group:\n")
|
||||
|
||||
if mr_line
|
||||
markdown(format(SUGGEST_MR_COMMENT, group: mr_group_label), file: feature_flag.path, line: mr_line.succ)
|
||||
else
|
||||
warn %(Consider setting `group: "#{mr_group_label}"` in #{gitlab.html_link(feature_flag.path)}. #{SEE_DOC})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def message_for_feature_flag_with_group!(feature_flag:, mr_group_label:)
|
||||
return if feature_flag.group_match_mr_label?(mr_group_label)
|
||||
|
||||
if mr_group_label.nil?
|
||||
gitlab.api.update_merge_request(gitlab.mr_json['project_id'],
|
||||
gitlab.mr_json['iid'],
|
||||
add_labels: feature_flag.group)
|
||||
else
|
||||
fail %(`group` is set to ~"#{feature_flag.group}" in #{gitlab.html_link(feature_flag.path)}, which does not match ~"#{mr_group_label}" set on the MR!)
|
||||
end
|
||||
end
|
||||
|
||||
feature_flag.feature_flag_files.each do |feature_flag|
|
||||
check_feature_flag_yaml(feature_flag)
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../tooling/danger/feature_flag'
|
||||
|
||||
module Danger
|
||||
class FeatureFlag < Plugin
|
||||
# Put the helper code somewhere it can be tested
|
||||
include Tooling::Danger::FeatureFlag
|
||||
end
|
||||
end
|
|
@ -381,10 +381,7 @@ only. For example:
|
|||
}
|
||||
```
|
||||
|
||||
## `audit_json.log`
|
||||
|
||||
NOTE:
|
||||
Most log entries only exist in [GitLab Starter](https://about.gitlab.com/pricing/), however a few exist in GitLab Core.
|
||||
## `audit_json.log` **(STARTER)**
|
||||
|
||||
This file lives in `/var/log/gitlab/gitlab-rails/audit_json.log` for
|
||||
Omnibus GitLab packages or in `/home/git/gitlab/log/audit_json.log` for
|
||||
|
|
|
@ -375,7 +375,7 @@ The files defined by `include` are:
|
|||
|
||||
NOTE:
|
||||
Use merging to customize and override included CI/CD configurations with local
|
||||
definitions. Local definitions in `.gitlab-ci.yml` override included definitions.
|
||||
configurations. Local configurations in `.gitlab-ci.yml` override included configurations.
|
||||
|
||||
#### Variables with `include` **(CORE ONLY)**
|
||||
|
||||
|
@ -2121,7 +2121,7 @@ build_job:
|
|||
|
||||
`build_job` downloads the artifacts from the latest successful `build-1` job
|
||||
on the `master` branch in the `group/project-name` project. If the project is in the
|
||||
same group or namespace, you can omit them from the `project:` key. For example,
|
||||
same group or namespace, you can omit them from the `project:` keyword. For example,
|
||||
`project: group/project-name` or `project: project-name`.
|
||||
|
||||
The user running the pipeline must have at least `reporter` access to the group or project, or the group/project must have public visibility.
|
||||
|
@ -2460,8 +2460,8 @@ by authorized users.
|
|||
Use `when: delayed` to execute scripts after a waiting period, or if you want to avoid
|
||||
jobs immediately entering the `pending` state.
|
||||
|
||||
You can set the period with `start_in` key. The value of `start_in` key is an elapsed time in seconds, unless a unit is
|
||||
provided. `start_in` key must be less than or equal to one week. Examples of valid values include:
|
||||
You can set the period with `start_in` keyword. The value of `start_in` is an elapsed time in seconds, unless a unit is
|
||||
provided. `start_in` must be less than or equal to one week. Examples of valid values include:
|
||||
|
||||
- `'5'`
|
||||
- `5 seconds`
|
||||
|
@ -2728,7 +2728,7 @@ as Review Apps. You can see an example that uses Review Apps at
|
|||
cached between jobs. You can only use paths that are in the local working copy.
|
||||
|
||||
If `cache` is defined outside the scope of jobs, it means it's set
|
||||
globally and all jobs use that definition.
|
||||
globally and all jobs use that configuration.
|
||||
|
||||
Caching is shared between pipelines and jobs. Caches are restored before [artifacts](#artifacts).
|
||||
|
||||
|
@ -3677,8 +3677,8 @@ deploystacks:
|
|||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8997) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Core in 12.8.
|
||||
|
||||
Use `trigger` to define a downstream pipeline trigger. When GitLab starts a job created
|
||||
with a `trigger` definition, a downstream pipeline is created.
|
||||
Use `trigger` to define a downstream pipeline trigger. When GitLab starts a `trigger` job,
|
||||
a downstream pipeline is created.
|
||||
|
||||
Jobs with `trigger` can only use a [limited set of keywords](../multi_project_pipelines.md#limitations).
|
||||
For example, you can't run commands with [`script`](#script), [`before_script`](#before_script),
|
||||
|
@ -3904,7 +3904,7 @@ To avoid these errors, the `resource_group` attribute can be used to ensure that
|
|||
the runner doesn't run certain jobs simultaneously. Resource groups behave similar
|
||||
to semaphores in other programming languages.
|
||||
|
||||
When the `resource_group` key is defined for a job in `.gitlab-ci.yml`,
|
||||
When the `resource_group` keyword is defined for a job in `.gitlab-ci.yml`,
|
||||
job executions are mutually exclusive across different pipelines for the same project.
|
||||
If multiple jobs belonging to the same resource group are enqueued simultaneously,
|
||||
only one of the jobs is picked by the runner. The other jobs wait until the
|
||||
|
@ -3936,7 +3936,7 @@ For more information, see [Deployments Safety](../environments/deployment_safety
|
|||
|
||||
`release` indicates that the job creates a [Release](../../user/project/releases/index.md).
|
||||
|
||||
These methods are supported:
|
||||
These keywords are supported:
|
||||
|
||||
- [`tag_name`](#releasetag_name)
|
||||
- [`description`](#releasedescription)
|
||||
|
@ -4325,26 +4325,26 @@ The following example uses anchors and map merging. It creates two jobs,
|
|||
with their own custom `script` defined:
|
||||
|
||||
```yaml
|
||||
.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition'
|
||||
.job_template: &job_configuration # Hidden yaml configuration that defines an anchor named 'job_configuration'
|
||||
image: ruby:2.6
|
||||
services:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
test1:
|
||||
<<: *job_definition # Merge the contents of the 'job_definition' alias
|
||||
<<: *job_configuration # Merge the contents of the 'job_configuration' alias
|
||||
script:
|
||||
- test1 project
|
||||
|
||||
test2:
|
||||
<<: *job_definition # Merge the contents of the 'job_definition' alias
|
||||
<<: *job_configuration # Merge the contents of the 'job_configuration' alias
|
||||
script:
|
||||
- test2 project
|
||||
```
|
||||
|
||||
`&` sets up the name of the anchor (`job_definition`), `<<` means "merge the
|
||||
`&` sets up the name of the anchor (`job_configuration`), `<<` means "merge the
|
||||
given hash into the current one", and `*` includes the named anchor
|
||||
(`job_definition` again). The expanded version of the example above is:
|
||||
(`job_configuration` again). The expanded version of the example above is:
|
||||
|
||||
```yaml
|
||||
.job_template:
|
||||
|
@ -4375,31 +4375,31 @@ and `test:mysql` share the `script` defined in `.job_template`, but use differen
|
|||
`services`, defined in `.postgres_services` and `.mysql_services`:
|
||||
|
||||
```yaml
|
||||
.job_template: &job_definition
|
||||
.job_template: &job_configuration
|
||||
script:
|
||||
- test project
|
||||
tags:
|
||||
- dev
|
||||
|
||||
.postgres_services:
|
||||
services: &postgres_definition
|
||||
services: &postgres_configuration
|
||||
- postgres
|
||||
- ruby
|
||||
|
||||
.mysql_services:
|
||||
services: &mysql_definition
|
||||
services: &mysql_configuration
|
||||
- mysql
|
||||
- ruby
|
||||
|
||||
test:postgres:
|
||||
<<: *job_definition
|
||||
services: *postgres_definition
|
||||
<<: *job_configuration
|
||||
services: *postgres_configuration
|
||||
tags:
|
||||
- postgres
|
||||
|
||||
test:mysql:
|
||||
<<: *job_definition
|
||||
services: *mysql_definition
|
||||
<<: *job_configuration
|
||||
services: *mysql_configuration
|
||||
```
|
||||
|
||||
The expanded version is:
|
||||
|
|
|
@ -36,9 +36,11 @@ Incident, you have two options to do this manually.
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230857) in GitLab 13.4.
|
||||
|
||||
- Navigate to **Issues > List** and click **Create Issue**.
|
||||
- Create a new issue using the `type` drop-down and select `Incident`.
|
||||
- The page refreshes and the page only displays fields relevant to Incidents.
|
||||
1. Go to **Issues > List**, and select **New issue**.
|
||||
1. In the **Type** dropdown, select **Incident**. Only fields relevant to
|
||||
incidents are displayed on the page.
|
||||
1. Create the incident as needed, and select **Submit issue** to save the
|
||||
incident.
|
||||
|
||||
![Incident List Create](img/new_incident_create_v13_4.png)
|
||||
|
||||
|
|
|
@ -22,6 +22,29 @@ View pipeline duration history:
|
|||
|
||||
![Pipeline duration](img/pipelines_duration_chart.png)
|
||||
|
||||
## DORA4 Metrics
|
||||
|
||||
Customer experience is a key metric. Users want to measure platform stability and other
|
||||
post-deployment performance KPIs, and set targets for customer behavior, experience, and financial
|
||||
impact. Tracking and measuring these indicators solves an important pain point. Similarly, creating
|
||||
views that manage products, not projects or repos, provides users with a more relevant data set.
|
||||
Since GitLab is a tool for the entire DevOps life-cycle, information from different workflows is
|
||||
integrated and can be used to measure the success of the teams.
|
||||
|
||||
The DevOps Research and Assessment ([DORA](https://cloud.google.com/blog/products/devops-sre/the-2019-accelerate-state-of-devops-elite-performance-productivity-and-scaling))
|
||||
team developed four key metrics that the industry has widely adopted. You can use these metrics as
|
||||
performance indicators for software development teams:
|
||||
|
||||
- Deployment frequency: How often an organization successfully releases to production.
|
||||
- Lead time for changes: The amount of time it takes for code to reach production.
|
||||
- Change failure rate: The percentage of deployments that cause a failure in production.
|
||||
- Time to restore service: How long it takes an organization to recover from a failure in
|
||||
production.
|
||||
|
||||
GitLab plans to add support for all the DORA4 metrics at the project and group levels. GitLab added
|
||||
the first metric, deployment frequency, at the project level for [CI/CD charts](ci_cd_analytics.md#deployment-frequency-charts)
|
||||
and the [API]( ../../api/project_analytics.md).
|
||||
|
||||
## Deployment frequency charts **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.8.
|
||||
|
|
|
@ -730,6 +730,9 @@ To enable this feature, navigate to the group settings page, expand the
|
|||
|
||||
![Group file template settings](img/group_file_template_settings.png)
|
||||
|
||||
To learn how to create templates for issues and merge requests, visit
|
||||
[Description templates](../project/description_templates.md).
|
||||
|
||||
#### Group-level project templates **(PREMIUM)**
|
||||
|
||||
Define project templates at a group level by setting a group as the template source.
|
||||
|
|
|
@ -6,16 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Description templates
|
||||
|
||||
>[Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4981) in GitLab 8.11.
|
||||
|
||||
We all know that a properly submitted issue is more likely to be addressed in
|
||||
a timely manner by the developers of a project.
|
||||
|
||||
Description templates allow you to define context-specific templates for issue
|
||||
and merge request description fields for your project, as well as help filter
|
||||
out a lot of unnecessary noise from issues.
|
||||
|
||||
## Overview
|
||||
With description templates, you can define context-specific templates for issue and merge request
|
||||
description fields for your project, and filter out unnecessary noise from issues.
|
||||
|
||||
By using the description templates, users that create a new issue or merge
|
||||
request can select a description template to help them communicate with other
|
||||
|
@ -28,7 +25,10 @@ Description templates must be written in [Markdown](../markdown.md) and stored
|
|||
in your project's repository under a directory named `.gitlab`. Only the
|
||||
templates of the default branch are taken into account.
|
||||
|
||||
## Use-cases
|
||||
To learn how to create templates for various file types in groups, visit
|
||||
[Group file templates](../group/index.md#group-file-templates).
|
||||
|
||||
## Use cases
|
||||
|
||||
- Add a template to be used in every issue for a specific project,
|
||||
giving instructions and guidelines, requiring for information specific to that subject.
|
||||
|
@ -80,17 +80,17 @@ to the issue description field. The **Reset template** button discards any
|
|||
changes you made after picking the template and returns it to its initial status.
|
||||
|
||||
NOTE:
|
||||
You can create short-cut links to create an issue using a designated template. For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
|
||||
You can create shortcut links to create an issue using a designated template.
|
||||
For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
|
||||
|
||||
![Description templates](img/description_templates.png)
|
||||
|
||||
## Setting a default template for merge requests and issues **(STARTER)**
|
||||
|
||||
> - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
|
||||
> - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
|
||||
> - Templates for merge requests were [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab EE 6.9.
|
||||
> - Templates for merge requests [introduced](https://gitlab.com/gitlab-org/gitlab/commit/7478ece8b48e80782b5465b96c79f85cc91d391b) in GitLab 6.9.
|
||||
> - Templates for issues [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab 8.1.
|
||||
|
||||
The visibility of issues and/or merge requests should be set to either "Everyone
|
||||
The visibility of issues or merge requests should be set to either "Everyone
|
||||
with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
|
||||
template text areas don't show. This is the default behavior, so in most cases
|
||||
you should be fine.
|
||||
|
@ -113,52 +113,46 @@ pre-filled with the text you entered in the template(s).
|
|||
|
||||
## Description template example
|
||||
|
||||
We make use of Description Templates for Issues and Merge Requests within the GitLab Community
|
||||
Edition project. Please refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab)
|
||||
for some examples.
|
||||
We make use of description templates for issues and merge requests in the GitLab project.
|
||||
For some examples, refer to the [`.gitlab` folder](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab).
|
||||
|
||||
NOTE:
|
||||
It's possible to use [quick actions](quick_actions.md) within description templates to quickly add
|
||||
It's possible to use [quick actions](quick_actions.md) in description templates to quickly add
|
||||
labels, assignees, and milestones. The quick actions are only executed if the user submitting
|
||||
the issue or merge request has the permissions to perform the relevant actions.
|
||||
|
||||
Here is an example of a Bug report template:
|
||||
|
||||
```plaintext
|
||||
Summary
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
|
||||
Steps to reproduce
|
||||
## Steps to reproduce
|
||||
|
||||
(How one can reproduce the issue - this is very important)
|
||||
|
||||
## Example Project
|
||||
|
||||
Example Project
|
||||
(If possible, please create an example project here on GitLab.com that exhibits the problematic
|
||||
behavior, and link to it here in the bug report.
|
||||
If you are using an older version of GitLab, this will also determine whether the bug has been fixed
|
||||
in a more recent version)
|
||||
|
||||
(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
|
||||
|
||||
(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
|
||||
|
||||
|
||||
What is the current bug behavior?
|
||||
## What is the current bug behavior?
|
||||
|
||||
(What actually happens)
|
||||
|
||||
|
||||
What is the expected correct behavior?
|
||||
## What is the expected correct behavior?
|
||||
|
||||
(What you should see instead)
|
||||
|
||||
## Relevant logs and/or screenshots
|
||||
|
||||
Relevant logs and/or screenshots
|
||||
(Paste any relevant logs - please use code blocks (```) to format console output, logs, and code, as
|
||||
it's very hard to read otherwise.)
|
||||
|
||||
(Paste any relevant logs - please use code blocks (```) to format console output,
|
||||
logs, and code as it's very hard to read otherwise.)
|
||||
|
||||
|
||||
Possible fixes
|
||||
## Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
module Groups
|
||||
module Graphql
|
||||
module GetLabelsQuery
|
||||
extend self
|
||||
|
||||
def to_s
|
||||
<<-'GRAPHQL'
|
||||
query ($full_path: ID!, $cursor: String) {
|
||||
group(fullPath: $full_path) {
|
||||
labels(first: 100, after: $cursor) {
|
||||
page_info: pageInfo {
|
||||
end_cursor: endCursor
|
||||
has_next_page: hasNextPage
|
||||
}
|
||||
nodes {
|
||||
title
|
||||
description
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GRAPHQL
|
||||
end
|
||||
|
||||
def variables(entity)
|
||||
{
|
||||
full_path: entity.source_full_path,
|
||||
cursor: entity.next_page_for(:labels)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
module Groups
|
||||
module Loaders
|
||||
class LabelsLoader
|
||||
def initialize(*); end
|
||||
|
||||
def load(context, data)
|
||||
Array.wrap(data['nodes']).each do |entry|
|
||||
Labels::CreateService.new(entry)
|
||||
.execute(group: context.entity.group)
|
||||
end
|
||||
|
||||
context.entity.update_tracker_for(
|
||||
relation: :labels,
|
||||
has_next_page: data.dig('page_info', 'has_next_page'),
|
||||
next_page: data.dig('page_info', 'end_cursor')
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
module Groups
|
||||
module Pipelines
|
||||
class LabelsPipeline
|
||||
include Pipeline
|
||||
|
||||
extractor BulkImports::Common::Extractors::GraphqlExtractor,
|
||||
query: BulkImports::Groups::Graphql::GetLabelsQuery
|
||||
|
||||
transformer BulkImports::Common::Transformers::HashKeyDigger, key_path: %w[data group labels]
|
||||
transformer Common::Transformers::ProhibitedAttributesTransformer
|
||||
|
||||
loader BulkImports::Groups::Loaders::LabelsLoader
|
||||
|
||||
def after_run(context)
|
||||
if context.entity.has_next_page?(:labels)
|
||||
run(context)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -29,7 +29,8 @@ module BulkImports
|
|||
def pipelines
|
||||
[
|
||||
BulkImports::Groups::Pipelines::GroupPipeline,
|
||||
BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline
|
||||
BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline,
|
||||
BulkImports::Groups::Pipelines::LabelsPipeline
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24624,21 +24624,36 @@ msgstr ""
|
|||
msgid "Runners|Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|An error has occurred fetching instructions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Architecture"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Can run untagged jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Copy instructions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Download Latest Binary"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Download and Install Binary"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|IP Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Install a Runner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Last contact"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24660,24 +24675,39 @@ msgstr ""
|
|||
msgid "Runners|Protected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Register Runner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Revision"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Shared"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Show Runner installation instructions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Specific"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|To install Runner in a container follow the instructions described in the GitLab documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|View installation instructions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|You have used %{quotaUsed} out of %{quotaLimit} of your shared Runners pipeline minutes."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ module QA
|
|||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'creates a basic merge request', :smoke, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/409' do
|
||||
it 'creates a basic merge request', :smoke, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1276' do
|
||||
Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request|
|
||||
merge_request.project = project
|
||||
merge_request.title = merge_request_title
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module QA
|
||||
RSpec.describe 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/30226', type: :bug } do
|
||||
describe 'Merge request rebasing' do
|
||||
it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/398' do
|
||||
it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1274' do
|
||||
Flow::Login.sign_in
|
||||
|
||||
project = Resource::Project.fabricate_via_api! do |project|
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
describe 'Git push over HTTP', :ldap_no_tls, :smoke do
|
||||
it 'user using a personal access token pushes code to the repository', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/430' do
|
||||
it 'user using a personal access token pushes code to the repository', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1278' do
|
||||
Flow::Login.sign_in
|
||||
|
||||
access_token = Resource::PersonalAccessToken.fabricate!.access_token
|
||||
|
|
|
@ -24,7 +24,7 @@ module QA
|
|||
runner.remove_via_api!
|
||||
end
|
||||
|
||||
it 'users creates a pipeline which gets processed', :smoke, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/391' do
|
||||
it 'users creates a pipeline which gets processed', :smoke, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1279' do
|
||||
Flow::Login.sign_in
|
||||
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
|
|
|
@ -117,7 +117,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
it 'runs an AutoDevOps pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/444' do
|
||||
it 'runs an AutoDevOps pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1277' do
|
||||
Flow::Pipeline.visit_latest_pipeline
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
|
|
|
@ -4,13 +4,13 @@ module RuboCop
|
|||
module Cop
|
||||
module Gitlab
|
||||
class HTTParty < RuboCop::Cop::Cop
|
||||
MSG_SEND = <<~EOL.freeze
|
||||
MSG_SEND = <<~EOL
|
||||
Avoid calling `HTTParty` directly. Instead, use the Gitlab::HTTP
|
||||
wrapper. To allow request to localhost or the private network set
|
||||
the option :allow_local_requests in the request call.
|
||||
EOL
|
||||
|
||||
MSG_INCLUDE = <<~EOL.freeze
|
||||
MSG_INCLUDE = <<~EOL
|
||||
Avoid including `HTTParty` directly. Instead, use the Gitlab::HTTP
|
||||
wrapper. To allow request to localhost or the private network set
|
||||
the option :allow_local_requests in the request call.
|
||||
|
|
|
@ -4,7 +4,7 @@ module RuboCop
|
|||
module Cop
|
||||
module Gitlab
|
||||
class Json < RuboCop::Cop::Cop
|
||||
MSG_SEND = <<~EOL.freeze
|
||||
MSG = <<~EOL
|
||||
Avoid calling `JSON` directly. Instead, use the `Gitlab::Json`
|
||||
wrapper. This allows us to alter the JSON parser being used.
|
||||
EOL
|
||||
|
@ -14,7 +14,7 @@ module RuboCop
|
|||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
add_offense(node, location: :expression, message: MSG_SEND) if json_node?(node)
|
||||
add_offense(node) if json_node?(node)
|
||||
end
|
||||
|
||||
def autocorrect(node)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Gitlab
|
||||
class ModuleWithInstanceVariables < RuboCop::Cop::Cop
|
||||
MSG = <<~EOL.freeze
|
||||
MSG = <<~EOL
|
||||
Do not use instance variables in a module. Please read this
|
||||
for the rationale behind it:
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Gitlab
|
||||
class PredicateMemoization < RuboCop::Cop::Cop
|
||||
MSG = <<~EOL.freeze
|
||||
MSG = <<~EOL
|
||||
Avoid using `@value ||= query` inside predicate methods in order to
|
||||
properly memoize `false` or `nil` values.
|
||||
https://docs.gitlab.com/ee/development/utilities.html#strongmemoize
|
||||
|
@ -12,7 +14,7 @@ module RuboCop
|
|||
return unless predicate_method?(node)
|
||||
|
||||
select_offenses(node).each do |offense|
|
||||
add_offense(offense, location: :expression)
|
||||
add_offense(offense)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -138,9 +138,8 @@ RSpec.describe 'Commits' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when accessing internal project with disallowed access', :js do
|
||||
context 'when accessing internal project with disallowed access', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299575' do
|
||||
before do
|
||||
stub_feature_flags(graphql_pipeline_header: false)
|
||||
project.update(
|
||||
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
|
||||
public_builds: false)
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import LegacyHeaderComponent from '~/pipelines/components/legacy_header_component.vue';
|
||||
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import eventHub from '~/pipelines/event_hub';
|
||||
|
||||
describe('Pipeline details header', () => {
|
||||
let wrapper;
|
||||
let glModalDirective;
|
||||
|
||||
const threeWeeksAgo = new Date();
|
||||
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
|
||||
|
||||
const findDeleteModal = () => wrapper.find(GlModal);
|
||||
|
||||
const defaultProps = {
|
||||
pipeline: {
|
||||
details: {
|
||||
status: {
|
||||
group: 'failed',
|
||||
icon: 'status_failed',
|
||||
label: 'failed',
|
||||
text: 'failed',
|
||||
details_path: 'path',
|
||||
},
|
||||
},
|
||||
id: 123,
|
||||
created_at: threeWeeksAgo.toISOString(),
|
||||
user: {
|
||||
web_url: 'path',
|
||||
name: 'Foo',
|
||||
username: 'foobar',
|
||||
email: 'foo@bar.com',
|
||||
avatar_url: 'link',
|
||||
},
|
||||
retry_path: 'retry',
|
||||
cancel_path: 'cancel',
|
||||
delete_path: 'delete',
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
glModalDirective = jest.fn();
|
||||
|
||||
wrapper = shallowMount(LegacyHeaderComponent, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
directives: {
|
||||
glModal: {
|
||||
bind(el, { value }) {
|
||||
glModalDirective(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
||||
createComponent(defaultProps);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
eventHub.$off();
|
||||
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('should render provided pipeline info', () => {
|
||||
expect(wrapper.find(CiHeader).props()).toMatchObject({
|
||||
status: defaultProps.pipeline.details.status,
|
||||
itemId: defaultProps.pipeline.id,
|
||||
time: defaultProps.pipeline.created_at,
|
||||
user: defaultProps.pipeline.user,
|
||||
});
|
||||
});
|
||||
|
||||
describe('action buttons', () => {
|
||||
it('should not trigger eventHub when nothing happens', () => {
|
||||
expect(eventHub.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call postAction when retry button action is clicked', () => {
|
||||
wrapper.find('[data-testid="retryButton"]').vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
|
||||
});
|
||||
|
||||
it('should call postAction when cancel button action is clicked', () => {
|
||||
wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
|
||||
});
|
||||
|
||||
it('does not show delete modal', () => {
|
||||
expect(findDeleteModal()).not.toBeVisible();
|
||||
});
|
||||
|
||||
describe('when delete button action is clicked', () => {
|
||||
it('displays delete modal', () => {
|
||||
expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
|
||||
});
|
||||
|
||||
it('should call delete when modal is submitted', () => {
|
||||
findDeleteModal().vm.$emit('ok');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import Tracking from '~/tracking';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import component from '~/registry/explorer/pages/details.vue';
|
||||
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
|
||||
import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
|
||||
|
@ -401,6 +402,9 @@ describe('Details Page', () => {
|
|||
const config = {
|
||||
runCleanupPoliciesHelpPagePath: 'foo',
|
||||
cleanupPoliciesHelpPagePath: 'bar',
|
||||
userCalloutsPath: 'call_out_path',
|
||||
userCalloutId: 'call_out_id',
|
||||
showUnfinishedTagCleanupCallout: true,
|
||||
};
|
||||
|
||||
describe(`when expirationPolicyCleanupStatus is ${UNFINISHED_STATUS}`, () => {
|
||||
|
@ -413,8 +417,9 @@ describe('Details Page', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('exists', async () => {
|
||||
mountComponent({ resolver });
|
||||
mountComponent({ resolver, config });
|
||||
|
||||
await waitForApolloRequestRender();
|
||||
|
||||
|
@ -426,11 +431,16 @@ describe('Details Page', () => {
|
|||
|
||||
await waitForApolloRequestRender();
|
||||
|
||||
expect(findPartialCleanupAlert().props()).toEqual({ ...config });
|
||||
expect(findPartialCleanupAlert().props()).toEqual({
|
||||
runCleanupPoliciesHelpPagePath: config.runCleanupPoliciesHelpPagePath,
|
||||
cleanupPoliciesHelpPagePath: config.cleanupPoliciesHelpPagePath,
|
||||
});
|
||||
});
|
||||
|
||||
it('dismiss hides the component', async () => {
|
||||
mountComponent({ resolver });
|
||||
jest.spyOn(axios, 'post').mockReturnValue();
|
||||
|
||||
mountComponent({ resolver, config });
|
||||
|
||||
await waitForApolloRequestRender();
|
||||
|
||||
|
@ -440,13 +450,25 @@ describe('Details Page', () => {
|
|||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(config.userCalloutsPath, {
|
||||
feature_name: config.userCalloutId,
|
||||
});
|
||||
expect(findPartialCleanupAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('is hidden if the callout is dismissed', async () => {
|
||||
mountComponent({ resolver });
|
||||
|
||||
await waitForApolloRequestRender();
|
||||
|
||||
expect(findPartialCleanupAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when expirationPolicyCleanupStatus is not ${UNFINISHED_STATUS}`, () => {
|
||||
it('the component is hidden', async () => {
|
||||
mountComponent();
|
||||
mountComponent({ config });
|
||||
|
||||
await waitForApolloRequestRender();
|
||||
|
||||
expect(findPartialCleanupAlert().exists()).toBe(false);
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
export const mockGraphqlRunnerPlatforms = {
|
||||
data: {
|
||||
runnerPlatforms: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'linux',
|
||||
humanReadableName: 'Linux',
|
||||
architectures: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'amd64',
|
||||
downloadLocation:
|
||||
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
|
||||
__typename: 'RunnerArchitecture',
|
||||
},
|
||||
{
|
||||
name: '386',
|
||||
downloadLocation:
|
||||
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
|
||||
__typename: 'RunnerArchitecture',
|
||||
},
|
||||
{
|
||||
name: 'arm',
|
||||
downloadLocation:
|
||||
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
|
||||
__typename: 'RunnerArchitecture',
|
||||
},
|
||||
{
|
||||
name: 'arm64',
|
||||
downloadLocation:
|
||||
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
|
||||
__typename: 'RunnerArchitecture',
|
||||
},
|
||||
],
|
||||
__typename: 'RunnerArchitectureConnection',
|
||||
},
|
||||
__typename: 'RunnerPlatform',
|
||||
},
|
||||
{
|
||||
name: 'osx',
|
||||
humanReadableName: 'macOS',
|
||||
architectures: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'amd64',
|
||||
downloadLocation:
|
||||
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
|
||||
__typename: 'RunnerArchitecture',
|
||||
},
|
||||
],
|
||||
__typename: 'RunnerArchitectureConnection',
|
||||
},
|
||||
__typename: 'RunnerPlatform',
|
||||
},
|
||||
{
|
||||
name: 'windows',
|
||||
humanReadableName: 'Windows',
|
||||
architectures: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'amd64',
|
||||
downloadLocation:
|
||||
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
|
||||
__typename: 'RunnerArchitecture',
|
||||
},
|
||||
{
|
||||
name: '386',
|
||||
downloadLocation:
|
||||
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
|
||||
__typename: 'RunnerArchitecture',
|
||||
},
|
||||
],
|
||||
__typename: 'RunnerArchitectureConnection',
|
||||
},
|
||||
__typename: 'RunnerPlatform',
|
||||
},
|
||||
{
|
||||
name: 'docker',
|
||||
humanReadableName: 'Docker',
|
||||
architectures: null,
|
||||
__typename: 'RunnerPlatform',
|
||||
},
|
||||
{
|
||||
name: 'kubernetes',
|
||||
humanReadableName: 'Kubernetes',
|
||||
architectures: null,
|
||||
__typename: 'RunnerPlatform',
|
||||
},
|
||||
],
|
||||
__typename: 'RunnerPlatformConnection',
|
||||
},
|
||||
project: { id: 'gid://gitlab/Project/1', __typename: 'Project' },
|
||||
group: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGraphqlInstructions = {
|
||||
data: {
|
||||
runnerSetup: {
|
||||
installInstructions:
|
||||
"# Download the binary for your system\nsudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64\n\n# Give it permissions to execute\nsudo chmod +x /usr/local/bin/gitlab-runner\n\n# Create a GitLab CI user\nsudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash\n\n# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start\n",
|
||||
registerInstructions:
|
||||
'sudo gitlab-runner register --url http://192.168.1.81:3000/ --registration-token GE5gsjeep_HAtBf9s3Yz',
|
||||
__typename: 'RunnerSetup',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
|
||||
import getRunnerPlatforms from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
|
||||
import getRunnerSetupInstructions from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
|
||||
|
||||
import { mockGraphqlRunnerPlatforms, mockGraphqlInstructions } from './mock_data';
|
||||
|
||||
const projectPath = 'gitlab-org/gitlab';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
describe('RunnerInstructions component', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findModalButton = () => wrapper.find('[data-testid="show-modal-button"]');
|
||||
const findPlatformButtons = () => wrapper.findAll('[data-testid="platform-button"]');
|
||||
const findArchitectureDropdownItems = () =>
|
||||
wrapper.findAll('[data-testid="architecture-dropdown-item"]');
|
||||
const findBinaryInstructionsSection = () => wrapper.find('[data-testid="binary-instructions"]');
|
||||
const findRunnerInstructionsSection = () => wrapper.find('[data-testid="runner-instructions"]');
|
||||
|
||||
beforeEach(async () => {
|
||||
const requestHandlers = [
|
||||
[getRunnerPlatforms, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
|
||||
[getRunnerSetupInstructions, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
|
||||
wrapper = shallowMount(RunnerInstructions, {
|
||||
provide: {
|
||||
projectPath,
|
||||
},
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('should show the "Show Runner installation instructions" button', () => {
|
||||
const button = findModalButton();
|
||||
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.text()).toBe('Show Runner installation instructions');
|
||||
});
|
||||
|
||||
it('should contain a number of platforms buttons', () => {
|
||||
const buttons = findPlatformButtons();
|
||||
|
||||
expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
|
||||
});
|
||||
|
||||
it('should contain a number of dropdown items for the architecture options', () => {
|
||||
const platformButton = findPlatformButtons().at(0);
|
||||
platformButton.vm.$emit('click');
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
const dropdownItems = findArchitectureDropdownItems();
|
||||
|
||||
expect(dropdownItems).toHaveLength(
|
||||
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the binary installation instructions for a selected architecture', async () => {
|
||||
const platformButton = findPlatformButtons().at(0);
|
||||
platformButton.vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dropdownItem = findArchitectureDropdownItems().at(0);
|
||||
dropdownItem.vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const runner = findBinaryInstructionsSection();
|
||||
|
||||
expect(runner.text()).toMatch('sudo chmod +x /usr/local/bin/gitlab-runner');
|
||||
expect(runner.text()).toMatch(
|
||||
`sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash`,
|
||||
);
|
||||
expect(runner.text()).toMatch(
|
||||
'sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner',
|
||||
);
|
||||
expect(runner.text()).toMatch('sudo gitlab-runner start');
|
||||
});
|
||||
|
||||
it('should display the runner register instructions for a selected architecture', async () => {
|
||||
const platformButton = findPlatformButtons().at(0);
|
||||
platformButton.vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dropdownItem = findArchitectureDropdownItems().at(0);
|
||||
dropdownItem.vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const runner = findRunnerInstructionsSection();
|
||||
|
||||
expect(runner.text()).toMatch(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
|
||||
});
|
||||
});
|
|
@ -254,4 +254,31 @@ RSpec.describe IssuesHelper do
|
|||
expect(helper.use_startup_call?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issue_header_actions_data' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(current_user)
|
||||
allow(helper).to receive(:can?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns expected result' do
|
||||
expected = {
|
||||
can_create_issue: "true",
|
||||
can_reopen_issue: "true",
|
||||
can_report_spam: "false",
|
||||
can_update_issue: "true",
|
||||
iid: issue.iid,
|
||||
is_issue_author: "false",
|
||||
issue_type: "issue",
|
||||
new_issue_path: new_project_issue_path(project),
|
||||
project_path: project.full_path,
|
||||
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
|
||||
submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
|
||||
}
|
||||
|
||||
expect(helper.issue_header_actions_data(project, issue, current_user)).to include(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -222,4 +222,24 @@ RSpec.describe UserCalloutsHelper do
|
|||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.show_unfinished_tag_cleanup_callout?' do
|
||||
subject { helper.show_unfinished_tag_cleanup_callout? }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:user_dismissed?).with(described_class::UNFINISHED_TAG_CLEANUP_CALLOUT) { dismissed }
|
||||
end
|
||||
|
||||
context 'when user has not dismissed' do
|
||||
let(:dismissed) { false }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when user dismissed' do
|
||||
let(:dismissed) { true }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,14 +81,6 @@ window.addEventListener('unhandledrejection', (event) => {
|
|||
console.error(event.reason.stack || event.reason);
|
||||
});
|
||||
|
||||
// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
|
||||
// because it appears to lock up the thread that communicates to Karma's socket
|
||||
// This async beforeEach gets called on every spec and releases the JS thread long
|
||||
// enough for the socket to continue to communicate.
|
||||
// The downside is that it creates a minor performance penalty in the time it takes
|
||||
// to run our unit tests.
|
||||
beforeEach((done) => done());
|
||||
|
||||
let longRunningTestTimeoutHandle;
|
||||
|
||||
beforeEach((done) => {
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:entity) do
|
||||
create(
|
||||
:bulk_import_entity,
|
||||
source_full_path: 'source/full/path',
|
||||
destination_name: 'My Destination Group',
|
||||
destination_namespace: group.full_path,
|
||||
group: group
|
||||
)
|
||||
end
|
||||
|
||||
let(:context) do
|
||||
BulkImports::Pipeline::Context.new(
|
||||
current_user: user,
|
||||
entity: entity
|
||||
)
|
||||
end
|
||||
|
||||
def extractor_data(title:, has_next_page:, cursor: "")
|
||||
{
|
||||
"data" => {
|
||||
"group" => {
|
||||
"labels" => {
|
||||
"page_info" => {
|
||||
"end_cursor" => cursor,
|
||||
"has_next_page" => has_next_page
|
||||
},
|
||||
"nodes" => [
|
||||
{
|
||||
"title" => title,
|
||||
"description" => "desc",
|
||||
"color" => "#428BCA"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
describe '#run' do
|
||||
it 'imports a group labels' do
|
||||
first_page = extractor_data(title: 'label1', has_next_page: true, cursor: 'nextPageCursor')
|
||||
last_page = extractor_data(title: 'label2', has_next_page: false)
|
||||
|
||||
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
|
||||
allow(extractor)
|
||||
.to receive(:extract)
|
||||
.and_return(first_page, last_page)
|
||||
end
|
||||
|
||||
expect { subject.run(context) }.to change(Label, :count).by(2)
|
||||
|
||||
label = group.labels.order(:created_at).last
|
||||
|
||||
expect(label.title).to eq('label2')
|
||||
expect(label.description).to eq('desc')
|
||||
expect(label.color).to eq('#428BCA')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'pipeline parts' do
|
||||
it { expect(described_class).to include_module(BulkImports::Pipeline) }
|
||||
it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
|
||||
|
||||
it 'has extractors' do
|
||||
expect(described_class.get_extractor)
|
||||
.to eq(
|
||||
klass: BulkImports::Common::Extractors::GraphqlExtractor,
|
||||
options: {
|
||||
query: BulkImports::Groups::Graphql::GetLabelsQuery
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'has transformers' do
|
||||
expect(described_class.transformers)
|
||||
.to contain_exactly(
|
||||
{ klass: BulkImports::Common::Transformers::HashKeyDigger, options: { key_path: %w[data group labels] } },
|
||||
{ klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }
|
||||
)
|
||||
end
|
||||
|
||||
it 'has loaders' do
|
||||
expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::LabelsLoader, options: nil)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,6 +24,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
|
|||
describe '#execute' do
|
||||
it 'starts the entity and run its pipelines' do
|
||||
expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context
|
||||
expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
|
||||
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee?
|
||||
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
|
||||
|
||||
|
|
|
@ -125,6 +125,44 @@ RSpec.describe Pages::LookupPath do
|
|||
include_examples 'uses disk storage'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when deployment were created during migration' do
|
||||
before do
|
||||
FileUtils.mkdir_p File.join(project.pages_path, "public")
|
||||
File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
|
||||
f.write("Hello!")
|
||||
end
|
||||
|
||||
expect(::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute[:status]).to eq(:success)
|
||||
|
||||
project.reload
|
||||
end
|
||||
|
||||
let(:deployment) { project.pages_metadatum.pages_deployment }
|
||||
|
||||
it 'uses deployment from object storage' do
|
||||
freeze_time do
|
||||
expect(source).to(
|
||||
eq({
|
||||
type: 'zip',
|
||||
path: deployment.file.url(expire_at: 1.day.from_now),
|
||||
global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
|
||||
sha256: deployment.file_sha256,
|
||||
file_size: deployment.size,
|
||||
file_count: deployment.file_count
|
||||
})
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pages_serve_from_migrated_zip feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(pages_serve_from_migrated_zip: false)
|
||||
end
|
||||
|
||||
include_examples 'uses disk storage'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prefix' do
|
||||
|
|
|
@ -5,8 +5,6 @@ require 'rubocop'
|
|||
require_relative '../../../rubocop/cop/active_record_association_reload'
|
||||
|
||||
RSpec.describe RuboCop::Cop::ActiveRecordAssociationReload do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
context 'when using ActiveRecord::Base' do
|
||||
|
|
|
@ -2,33 +2,31 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../rubocop/cop/avoid_becomes'
|
||||
|
||||
RSpec.describe RuboCop::Cop::AvoidBecomes do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags the use of becomes with a constant parameter' do
|
||||
inspect_source('foo.becomes(Project)')
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect_offense(<<~CODE)
|
||||
foo.becomes(Project)
|
||||
^^^^^^^^^^^^^^^^^^^^ Avoid the use of becomes(SomeConstant), [...]
|
||||
CODE
|
||||
end
|
||||
|
||||
it 'flags the use of becomes with a namespaced constant parameter' do
|
||||
inspect_source('foo.becomes(Namespace::Group)')
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect_offense(<<~CODE)
|
||||
foo.becomes(Namespace::Group)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid the use of becomes(SomeConstant), [...]
|
||||
CODE
|
||||
end
|
||||
|
||||
it 'flags the use of becomes with a dynamic parameter' do
|
||||
inspect_source(<<~RUBY)
|
||||
model = Namespace
|
||||
project = Project.first
|
||||
project.becomes(model)
|
||||
RUBY
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect_offense(<<~CODE)
|
||||
model = Namespace
|
||||
project = Project.first
|
||||
project.becomes(model)
|
||||
^^^^^^^^^^^^^^^^^^^^^^ Avoid the use of becomes(SomeConstant), [...]
|
||||
CODE
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,8 +5,6 @@ require 'rubocop'
|
|||
require_relative '../../../rubocop/cop/avoid_break_from_strong_memoize'
|
||||
|
||||
RSpec.describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags violation for break inside strong_memoize' do
|
||||
|
@ -56,7 +54,7 @@ RSpec.describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
|
|||
call do
|
||||
strong_memoize(:result) do
|
||||
break if something
|
||||
|
||||
^^^^^ Do not use break inside strong_memoize, use next instead.
|
||||
do_an_heavy_calculation
|
||||
end
|
||||
end
|
||||
|
@ -65,7 +63,7 @@ RSpec.describe RuboCop::Cop::AvoidBreakFromStrongMemoize do
|
|||
expect(instance).to receive(:add_offense).once
|
||||
end
|
||||
|
||||
inspect_source(source)
|
||||
expect_offense(source)
|
||||
end
|
||||
|
||||
it "doesn't check when block is empty" do
|
||||
|
|
|
@ -2,18 +2,15 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers'
|
||||
|
||||
RSpec.describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags violation for keyword arguments usage in perform method signature' do
|
||||
expect_offense(<<~RUBY)
|
||||
def perform(id:)
|
||||
^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
|
||||
^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, [...]
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
@ -21,7 +18,7 @@ RSpec.describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
|
|||
it 'flags violation for optional keyword arguments usage in perform method signature' do
|
||||
expect_offense(<<~RUBY)
|
||||
def perform(id: nil)
|
||||
^^^^^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
|
||||
^^^^^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, [...]
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
|
|
@ -1,30 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
|
||||
require_relative '../../../rubocop/cop/ban_catch_throw'
|
||||
|
||||
RSpec.describe RuboCop::Cop::BanCatchThrow do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'registers an offense when `catch` or `throw` are used' do
|
||||
inspect_source("catch(:foo) {\n throw(:foo)\n}")
|
||||
|
||||
aggregate_failures do
|
||||
expect(cop.offenses.size).to eq(2)
|
||||
expect(cop.offenses.map(&:line)).to eq([1, 2])
|
||||
expect(cop.highlights).to eq(['catch(:foo)', 'throw(:foo)'])
|
||||
end
|
||||
expect_offense(<<~CODE)
|
||||
catch(:foo) {
|
||||
^^^^^^^^^^^ Do not use catch or throw unless a gem's API demands it.
|
||||
throw(:foo)
|
||||
^^^^^^^^^^^ Do not use catch or throw unless a gem's API demands it.
|
||||
}
|
||||
CODE
|
||||
end
|
||||
|
||||
it 'does not register an offense for a method called catch or throw' do
|
||||
inspect_source("foo.catch(:foo) {\n foo.throw(:foo)\n}")
|
||||
|
||||
expect(cop.offenses).to be_empty
|
||||
expect_no_offenses(<<~CODE)
|
||||
foo.catch(:foo) {
|
||||
foo.throw(:foo)
|
||||
}
|
||||
CODE
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,47 +2,44 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../rubocop/cop/default_scope'
|
||||
|
||||
RSpec.describe RuboCop::Cop::DefaultScope do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'does not flag the use of default_scope with a send receiver' do
|
||||
inspect_source('foo.default_scope')
|
||||
|
||||
expect(cop.offenses.size).to eq(0)
|
||||
expect_no_offenses('foo.default_scope')
|
||||
end
|
||||
|
||||
it 'flags the use of default_scope with a constant receiver' do
|
||||
inspect_source('User.default_scope')
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect_offense(<<~SOURCE)
|
||||
User.default_scope
|
||||
^^^^^^^^^^^^^^^^^^ Do not use `default_scope`, [...]
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it 'flags the use of default_scope with a nil receiver' do
|
||||
inspect_source('class Foo ; default_scope ; end')
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect_offense(<<~SOURCE)
|
||||
class Foo ; default_scope ; end
|
||||
^^^^^^^^^^^^^ Do not use `default_scope`, [...]
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it 'flags the use of default_scope when passing arguments' do
|
||||
inspect_source('class Foo ; default_scope(:foo) ; end')
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect_offense(<<~SOURCE)
|
||||
class Foo ; default_scope(:foo) ; end
|
||||
^^^^^^^^^^^^^^^^^^^ Do not use `default_scope`, [...]
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it 'flags the use of default_scope when passing a block' do
|
||||
inspect_source('class Foo ; default_scope { :foo } ; end')
|
||||
|
||||
expect(cop.offenses.size).to eq(1)
|
||||
expect_offense(<<~SOURCE)
|
||||
class Foo ; default_scope { :foo } ; end
|
||||
^^^^^^^^^^^^^ Do not use `default_scope`, [...]
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it 'ignores the use of default_scope with a local variable receiver' do
|
||||
inspect_source('users = User.all ; users.default_scope')
|
||||
|
||||
expect(cop.offenses.size).to eq(0)
|
||||
expect_no_offenses('users = User.all ; users.default_scope')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,19 +2,16 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/gitlab/avoid_uploaded_file_from_params'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Gitlab::AvoidUploadedFileFromParams do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
context 'UploadedFile.from_params' do
|
||||
context 'when using UploadedFile.from_params' do
|
||||
it 'flags its call' do
|
||||
expect_offense(<<~SOURCE)
|
||||
UploadedFile.from_params(params)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `UploadedFile` set by `multipart.rb` instead of calling `UploadedFile.from_params` directly. See https://docs.gitlab.com/ee/development/uploads.html#how-to-add-a-new-upload-route
|
||||
UploadedFile.from_params(params)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `UploadedFile` set by `multipart.rb` instead of calling [...]
|
||||
SOURCE
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,25 +2,22 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/gitlab/bulk_insert'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Gitlab::BulkInsert do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags the use of Gitlab::Database.bulk_insert' do
|
||||
expect_offense(<<~SOURCE)
|
||||
Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{RuboCop::Cop::Gitlab::BulkInsert::MSG}
|
||||
Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, [...]
|
||||
SOURCE
|
||||
end
|
||||
|
||||
it 'flags the use of ::Gitlab::Database.bulk_insert' do
|
||||
expect_offense(<<~SOURCE)
|
||||
::Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{RuboCop::Cop::Gitlab::BulkInsert::MSG}
|
||||
::Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `BulkInsertSafe` concern, [...]
|
||||
SOURCE
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,19 +2,16 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/gitlab/change_timzone'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Gitlab::ChangeTimezone do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
context 'Time.zone=' do
|
||||
it 'registers an offense with no 2nd argument' do
|
||||
expect_offense(<<~PATTERN)
|
||||
Time.zone = 'Awkland'
|
||||
^^^^^^^^^^^^^^^^^^^^^ Do not change timezone in the runtime (application or rspec), it could result in silently modifying other behavior.
|
||||
^^^^^^^^^^^^^^^^^^^^^ Do not change timezone in the runtime (application or rspec), it could result [...]
|
||||
PATTERN
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,78 +2,75 @@
|
|||
|
||||
require 'fast_spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../../rubocop/cop/gitlab/const_get_inherit_false'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Gitlab::ConstGetInheritFalse do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
context 'Object.const_get' do
|
||||
it 'registers an offense with no 2nd argument' do
|
||||
it 'registers an offense with no 2nd argument and corrects' do
|
||||
expect_offense(<<~PATTERN)
|
||||
Object.const_get(:CONSTANT)
|
||||
^^^^^^^^^ Use inherit=false when using const_get.
|
||||
PATTERN
|
||||
end
|
||||
|
||||
it 'autocorrects' do
|
||||
expect(autocorrect_source('Object.const_get(:CONSTANT)')).to eq('Object.const_get(:CONSTANT, false)')
|
||||
expect_correction(<<~PATTERN)
|
||||
Object.const_get(:CONSTANT, false)
|
||||
PATTERN
|
||||
end
|
||||
|
||||
context 'inherit=false' do
|
||||
it 'does not register an offense' do
|
||||
expect_no_offenses(<<~PATTERN)
|
||||
Object.const_get(:CONSTANT, false)
|
||||
Object.const_get(:CONSTANT, false)
|
||||
PATTERN
|
||||
end
|
||||
end
|
||||
|
||||
context 'inherit=true' do
|
||||
it 'registers an offense' do
|
||||
it 'registers an offense and corrects' do
|
||||
expect_offense(<<~PATTERN)
|
||||
Object.const_get(:CONSTANT, true)
|
||||
^^^^^^^^^ Use inherit=false when using const_get.
|
||||
Object.const_get(:CONSTANT, true)
|
||||
^^^^^^^^^ Use inherit=false when using const_get.
|
||||
PATTERN
|
||||
end
|
||||
|
||||
it 'autocorrects' do
|
||||
expect(autocorrect_source('Object.const_get(:CONSTANT, true)')).to eq('Object.const_get(:CONSTANT, false)')
|
||||
expect_correction(<<~PATTERN)
|
||||
Object.const_get(:CONSTANT, false)
|
||||
PATTERN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'const_get for a nested class' do
|
||||
it 'registers an offense on reload usage' do
|
||||
it 'registers an offense on reload usage and corrects' do
|
||||
expect_offense(<<~PATTERN)
|
||||
Nested::Blog.const_get(:CONSTANT)
|
||||
^^^^^^^^^ Use inherit=false when using const_get.
|
||||
PATTERN
|
||||
end
|
||||
|
||||
it 'autocorrects' do
|
||||
expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
|
||||
expect_correction(<<~PATTERN)
|
||||
Nested::Blog.const_get(:CONSTANT, false)
|
||||
PATTERN
|
||||
end
|
||||
|
||||
context 'inherit=false' do
|
||||
it 'does not register an offense' do
|
||||
expect_no_offenses(<<~PATTERN)
|
||||
Nested::Blog.const_get(:CONSTANT, false)
|
||||
Nested::Blog.const_get(:CONSTANT, false)
|
||||
PATTERN
|
||||
end
|
||||
end
|
||||
|
||||
context 'inherit=true' do
|
||||
it 'registers an offense if inherit is true' do
|
||||
it 'registers an offense if inherit is true and corrects' do
|
||||
expect_offense(<<~PATTERN)
|
||||
Nested::Blog.const_get(:CONSTANT, true)
|
||||
^^^^^^^^^ Use inherit=false when using const_get.
|
||||
Nested::Blog.const_get(:CONSTANT, true)
|
||||
^^^^^^^^^ Use inherit=false when using const_get.
|
||||
PATTERN
|
||||
end
|
||||
|
||||
it 'autocorrects' do
|
||||
expect(autocorrect_source('Nested::Blag.const_get(:CONSTANT, true)')).to eq('Nested::Blag.const_get(:CONSTANT, false)')
|
||||
expect_correction(<<~PATTERN)
|
||||
Nested::Blog.const_get(:CONSTANT, false)
|
||||
PATTERN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue