Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-26 18:09:30 +00:00
parent ff89c3c372
commit 25eb713a7f
127 changed files with 1776 additions and 1054 deletions

View File

@ -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'

View File

@ -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?

View File

@ -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();
}
});

View File

@ -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();
}
});

View File

@ -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();
}
});

View File

@ -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);
},
});
}
}

View File

@ -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>

View File

@ -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.'));
}
}

View File

@ -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} .`,

View File

@ -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" />

View File

@ -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',
},
};

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -1,4 +1,3 @@
@import './pages/admin';
@import './pages/branches';
@import './pages/ci_projects';
@import './pages/clusters';

View File

@ -0,0 +1,3 @@
.usage-data {
max-height: 400px;
}

View File

@ -0,0 +1,5 @@
.admin-builds-table {
td:last-child {
min-width: 120px;
}
}

View File

@ -1,11 +0,0 @@
.usage-data {
max-height: 400px;
}
[data-page='admin:jobs:index'] {
.admin-builds-table {
td:last-child {
min-width: 120px;
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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],

View File

@ -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? &&

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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 } }

View File

@ -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 } }

View File

@ -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

View File

@ -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, } }

View File

@ -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

View File

@ -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}"

View File

@ -0,0 +1,5 @@
---
title: Add callout disabling feature to cleanup policy alert
merge_request: 52327
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove graphql_pipeline_header feature flag
merge_request: 52247
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: 'BulkImports: Import Group Labels'
merge_request: 52260
author:
type: added

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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|

View File

@ -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|

View File

@ -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.

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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');
});
});
});
});

View File

@ -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);

View File

@ -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',
},
},
};

View File

@ -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);
});
});

View File

@ -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

View File

@ -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

View File

@ -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) => {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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