Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6b38cdf61a
commit
db79e99417
|
|
@ -1512,6 +1512,8 @@ lib/gitlab/checks/**
|
|||
/**/lib/**/ci/reports/**/ @gitlab-org/maintainers/rails-backend
|
||||
/**/lib/**/ci/parsers/**/ @gitlab-org/maintainers/rails-backend
|
||||
/**/app/views/ci/**/ @gitlab-org/maintainers/rails-backend
|
||||
/ee/**/models/ai/ai_resource/ci/ @gitlab-org/maintainers/rails-backend
|
||||
/ee/**/serializers/ci/*_ai_*.rb @gitlab-org/maintainers/rails-backend
|
||||
/ee/lib/gitlab/ci/parsers/license_compliance/ @gitlab-org/secure/composition-analysis-be
|
||||
/ee/lib/gitlab/ci/parsers/security/ @gitlab-org/govern/threat-insights-backend-team
|
||||
/ee/lib/gitlab/ci/reports/coverage_fuzzing/ @gitlab-org/secure/fuzzing-be
|
||||
|
|
|
|||
|
|
@ -22,14 +22,12 @@ Gitlab/DocumentationLinks/Link:
|
|||
- 'ee/lib/gitlab/checks/secrets_check.rb'
|
||||
- 'ee/lib/gitlab/llm/chain/tools/tool.rb'
|
||||
- 'ee/lib/gitlab/llm/embeddings/utils/base_content_parser.rb'
|
||||
- 'ee/lib/system_check/geo/current_node_check.rb'
|
||||
- 'ee/spec/helpers/projects/learn_gitlab_helper_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/llm/embeddings/utils/base_content_parser_spec.rb'
|
||||
- 'ee/spec/presenters/gitlab_subscriptions/trials/duo_enterprise_status_widget_presenter_spec.rb'
|
||||
- 'ee/spec/support/shared_contexts/secrets_check_shared_contexts.rb'
|
||||
- 'lib/backup/tasks/database.rb'
|
||||
- 'lib/gitlab/security/features.rb'
|
||||
- 'lib/gitlab/usage_data_counters/hll_redis_counter.rb'
|
||||
- 'lib/slack/block_kit/app_home_opened.rb'
|
||||
- 'lib/system_check/helpers.rb'
|
||||
- 'spec/helpers/ide_helper_spec.rb'
|
||||
|
|
|
|||
|
|
@ -284,7 +284,6 @@ export default {
|
|||
<template #row-details="{ item }">
|
||||
<pipeline-failed-jobs-widget
|
||||
v-if="displayFailedJobsWidget(item)"
|
||||
:failed-jobs-count="failedJobsCount(item)"
|
||||
:is-pipeline-active="item.active"
|
||||
:pipeline-iid="item.iid"
|
||||
:pipeline-path="item.path"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import { s__ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export const EDITOR_APP_DRAWER_HELP = 'HELP';
|
||||
export const EDITOR_APP_DRAWER_JOB_ASSISTANT = 'JOB_ASSISTANT';
|
||||
export const EDITOR_APP_DRAWER_AI_ASSISTANT = 'AI_ASSISTANT';
|
||||
|
|
@ -93,36 +90,3 @@ export const pipelineEditorTrackingOptions = {
|
|||
export const VALIDATE_TAB_FEEDBACK_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/346687';
|
||||
|
||||
export const COMMIT_SHA_POLL_INTERVAL = 1000;
|
||||
|
||||
export const MIGRATION_PLAN_HELP_PATH = helpPagePath('ci/migration/plan_a_migration');
|
||||
export const MIGRATE_FROM_JENKINS_TRACKING_LABEL = 'migrate_from_jenkins_prompt';
|
||||
|
||||
export const I18N = {
|
||||
title: s__('Pipelines|Get started with GitLab CI/CD'),
|
||||
learnBasics: {
|
||||
title: s__('Pipelines|Learn the basics of pipelines and .yml files'),
|
||||
subtitle: s__(
|
||||
'Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works.',
|
||||
),
|
||||
gettingStarted: {
|
||||
title: s__('Pipelines|"Hello world" with GitLab CI'),
|
||||
description: s__(
|
||||
'Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a "Hello world" script to see how it runs, explore how CI/CD works.',
|
||||
),
|
||||
cta: s__('Pipelines|Try test template'),
|
||||
},
|
||||
migrateFromJenkins: {
|
||||
title: s__('Pipelines|Migrate to GitLab CI/CD from Jenkins'),
|
||||
description: s__(
|
||||
'Pipelines|Take advantage of simple, scalable pipelines and CI/CD-enabled features. You can view integration results, security scans, tests, code coverage and more directly in merge requests!',
|
||||
),
|
||||
cta: s__('Pipelines|Start with a migration plan'),
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
title: s__('Pipelines|Ready to set up CI/CD for your project?'),
|
||||
subtitle: s__(
|
||||
"Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD.",
|
||||
),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
<script>
|
||||
import { GlButton, GlCard } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
CATALOG_TRACKING_LABEL,
|
||||
GITLAB_UNIVERSITY_TRACKING_LABEL,
|
||||
MIGRATE_FROM_JENKINS_TRACKING_LABEL,
|
||||
} from '~/ci/pipelines_page/constants';
|
||||
import { STARTER_TEMPLATE_NAME } from '~/ci/pipeline_editor/constants';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
const cards = {
|
||||
jenkins_migration_card: {
|
||||
id: 'jenkins_migration',
|
||||
buttonLink: helpPagePath('ci/migration/plan_a_migration'),
|
||||
buttonText: s__('Pipelines|Start with a migration plan'),
|
||||
description: s__(
|
||||
'Pipelines|Take advantage of simple, scalable pipelines and CI/CD-enabled features. You can view integration results, security scans, tests, code coverage and more directly in merge requests!',
|
||||
),
|
||||
emoji: 'rocket',
|
||||
isVisible: false,
|
||||
styles: 'gl-bg-blue-50',
|
||||
title: s__('Pipelines|Migrate to GitLab CI/CD from Jenkins'),
|
||||
trackingLabel: MIGRATE_FROM_JENKINS_TRACKING_LABEL,
|
||||
},
|
||||
getting_started_card: {
|
||||
id: 'getting_started',
|
||||
buttonLink: '',
|
||||
buttonText: s__('Pipelines|Try test template'),
|
||||
description: s__(
|
||||
'Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a "Hello world" script to see how it runs, explore how CI/CD works.',
|
||||
),
|
||||
emoji: 'wave',
|
||||
isVisible: true,
|
||||
title: s__('Pipelines|"Hello world" with GitLab CI'),
|
||||
trackingLabel: STARTER_TEMPLATE_NAME,
|
||||
},
|
||||
gitlab_university_card: {
|
||||
id: 'gitlab_university',
|
||||
buttonLink: 'https://university.gitlab.com/pages/ci-cd-content',
|
||||
buttonText: s__('Pipelines|Access GitLab University'),
|
||||
description: s__(
|
||||
'Pipelines|Learn how to set up and use GitLab CI/CD with guided tutorials, videos, and best practices.',
|
||||
),
|
||||
emoji: 'mortar_board',
|
||||
isVisible: true,
|
||||
title: s__('Pipelines|Learn CI/CD with GitLab University'),
|
||||
trackingLabel: GITLAB_UNIVERSITY_TRACKING_LABEL,
|
||||
},
|
||||
ci_cd_catalog_card: {
|
||||
id: 'ci_cd_catalog',
|
||||
buttonLink: '/explore/catalog',
|
||||
buttonText: s__('Pipelines|Explore CI/CD Catalog'),
|
||||
description: s__(
|
||||
'Pipelines|Explore CI components in the CI/CD Catalog to see if they suit your requirements.',
|
||||
),
|
||||
emoji: 'bulb',
|
||||
isVisible: true,
|
||||
title: s__('Pipelines|Easy configuration with CI/CD Catalog'),
|
||||
trackingLabel: CATALOG_TRACKING_LABEL,
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'CiCards',
|
||||
components: {
|
||||
GlButton,
|
||||
GlCard,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['pipelineEditorPath', 'showJenkinsCiPrompt'],
|
||||
computed: {
|
||||
cards() {
|
||||
return [
|
||||
{
|
||||
...cards.jenkins_migration_card,
|
||||
isVisible: this.showJenkinsCiPrompt,
|
||||
},
|
||||
{
|
||||
...cards.getting_started_card,
|
||||
buttonLink: this.gettingStartedTemplateUrl,
|
||||
},
|
||||
cards.gitlab_university_card,
|
||||
cards.ci_cd_catalog_card,
|
||||
];
|
||||
},
|
||||
gettingStartedTemplateUrl() {
|
||||
return mergeUrlParams({ template: STARTER_TEMPLATE_NAME }, this.pipelineEditorPath);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.showJenkinsCiPrompt) {
|
||||
this.trackEvent('render', MIGRATE_FROM_JENKINS_TRACKING_LABEL);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
trackEvent(label) {
|
||||
this.track('template_clicked', { label });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-grid gl-gap-5 md:gl-grid-cols-2 lg:gl-grid-cols-4">
|
||||
<template v-for="card in cards">
|
||||
<gl-card
|
||||
v-if="card.isVisible"
|
||||
:key="card.id"
|
||||
:body-class="['gl-h-full']"
|
||||
:class="card.styles"
|
||||
>
|
||||
<div class="gl-flex !gl-h-full gl-flex-col gl-justify-between gl-gap-4">
|
||||
<div>
|
||||
<gl-emoji
|
||||
class="gl-pb-5 gl-text-size-h2-xl"
|
||||
:data-name="card.emoji"
|
||||
data-testid="ci-card-emoji"
|
||||
/>
|
||||
<p class="gl-mb-3">
|
||||
<strong class="gl-mb-2 gl-text-gray-800" data-testid="ci-card-title">{{
|
||||
card.title
|
||||
}}</strong>
|
||||
</p>
|
||||
<p class="gl-flex-grow gl-text-sm" data-testid="ci-card-description">
|
||||
{{ card.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:href="card.buttonLink"
|
||||
target="_blank"
|
||||
@click="trackEvent(card.trackingLabel)"
|
||||
>
|
||||
{{ card.buttonText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,124 +1,47 @@
|
|||
<script>
|
||||
import { GlButton, GlCard, GlSprintf } from '@gitlab/ui';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
STARTER_TEMPLATE_NAME,
|
||||
I18N,
|
||||
MIGRATION_PLAN_HELP_PATH,
|
||||
MIGRATE_FROM_JENKINS_TRACKING_LABEL,
|
||||
} from '~/ci/pipeline_editor/constants';
|
||||
import Tracking from '~/tracking';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
import CiCards from './ci_cards.vue';
|
||||
import CiTemplates from './ci_templates.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlCard,
|
||||
GlSprintf,
|
||||
CiTemplates,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
STARTER_TEMPLATE_NAME,
|
||||
I18N,
|
||||
inject: ['pipelineEditorPath', 'showJenkinsCiPrompt'],
|
||||
data() {
|
||||
return {
|
||||
gettingStartedTemplateUrl: mergeUrlParams(
|
||||
{ template: STARTER_TEMPLATE_NAME },
|
||||
this.pipelineEditorPath,
|
||||
name: 'PipelinesCiTemplates',
|
||||
i18n: {
|
||||
title: s__('Pipelines|Get started with GitLab CI/CD'),
|
||||
learnBasics: {
|
||||
title: s__('Pipelines|Learn the basics of pipelines and .yml files'),
|
||||
subtitle: s__(
|
||||
'Pipelines|Use CI/CD pipelines to automatically build, test, and deploy your code.',
|
||||
),
|
||||
tracker: null,
|
||||
migrationPlanUrl: MIGRATION_PLAN_HELP_PATH,
|
||||
migrationPromptTrackingLabel: MIGRATE_FROM_JENKINS_TRACKING_LABEL,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.showJenkinsCiPrompt) {
|
||||
this.trackEvent('render', this.migrationPromptTrackingLabel);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
trackEvent(action, label) {
|
||||
this.track(action, { label });
|
||||
},
|
||||
templates: {
|
||||
title: s__('Pipelines|Ready to set up CI/CD for your project?'),
|
||||
subtitle: s__(
|
||||
"Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD.",
|
||||
),
|
||||
},
|
||||
},
|
||||
components: {
|
||||
CiCards,
|
||||
CiTemplates,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="gl-text-size-h2 gl-text-gray-900">{{ $options.I18N.title }}</h2>
|
||||
<h2 class="gl-text-size-h2">{{ $options.i18n.title }}</h2>
|
||||
|
||||
<h2 class="gl-text-lg gl-text-gray-900">{{ $options.I18N.learnBasics.title }}</h2>
|
||||
<p class="gl-mb-6 gl-text-gray-800">
|
||||
<gl-sprintf :message="$options.I18N.learnBasics.subtitle">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
||||
<div class="gl-flex gl-flex-row gl-flex-wrap">
|
||||
<div
|
||||
v-if="showJenkinsCiPrompt"
|
||||
class="gl-w-full gl-pb-8 md:gl-w-1/2 md:gl-pr-5 lg:gl-w-1/4"
|
||||
data-testid="migrate-from-jenkins-prompt"
|
||||
>
|
||||
<gl-card class="gl-bg-blue-50">
|
||||
<div class="gl-flex-row">
|
||||
<div class="gl-py-5"><gl-emoji class="gl-text-size-h2-xl" data-name="rocket" /></div>
|
||||
<div class="gl-mb-3">
|
||||
<strong class="gl-mb-2 gl-text-gray-800">{{
|
||||
$options.I18N.learnBasics.migrateFromJenkins.title
|
||||
}}</strong>
|
||||
</div>
|
||||
<p class="gl-h-13 gl-text-sm">
|
||||
{{ $options.I18N.learnBasics.migrateFromJenkins.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:href="migrationPlanUrl"
|
||||
target="_blank"
|
||||
@click="trackEvent('template_clicked', migrationPromptTrackingLabel)"
|
||||
>
|
||||
{{ $options.I18N.learnBasics.migrateFromJenkins.cta }}
|
||||
</gl-button>
|
||||
</gl-card>
|
||||
</div>
|
||||
|
||||
<div class="gl-w-full gl-pb-8 md:gl-w-1/2 lg:gl-w-1/4">
|
||||
<gl-card>
|
||||
<div class="gl-flex-row">
|
||||
<div class="gl-py-5"><gl-emoji class="gl-text-size-h2-xl" data-name="wave" /></div>
|
||||
<div class="gl-mb-3">
|
||||
<strong class="gl-mb-2 gl-text-gray-800">
|
||||
{{ $options.I18N.learnBasics.gettingStarted.title }}
|
||||
</strong>
|
||||
</div>
|
||||
<p class="gl-h-13 gl-text-sm">
|
||||
{{ $options.I18N.learnBasics.gettingStarted.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:href="gettingStartedTemplateUrl"
|
||||
data-testid="test-template-link"
|
||||
@click="trackEvent('template_clicked', $options.STARTER_TEMPLATE_NAME)"
|
||||
>
|
||||
{{ $options.I18N.learnBasics.gettingStarted.cta }}
|
||||
</gl-button>
|
||||
</gl-card>
|
||||
</div>
|
||||
<div class="gl-mb-8">
|
||||
<h3 class="gl-text-lg">{{ $options.i18n.learnBasics.title }}</h3>
|
||||
<p class="gl-mb-6 gl-text-gray-800">{{ $options.i18n.learnBasics.subtitle }}</p>
|
||||
<ci-cards />
|
||||
</div>
|
||||
|
||||
<h2 class="gl-text-lg gl-text-gray-900">{{ $options.I18N.templates.title }}</h2>
|
||||
<p class="gl-mb-6 gl-text-gray-800">{{ $options.I18N.templates.subtitle }}</p>
|
||||
|
||||
<ci-templates />
|
||||
<div>
|
||||
<h3 class="gl-text-lg">{{ $options.i18n.templates.title }}</h3>
|
||||
<p class="gl-mb-6 gl-text-gray-800">{{ $options.i18n.templates.subtitle }}</p>
|
||||
<ci-templates />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const POLL_INTERVAL = 10000;
|
||||
|
|
@ -7,8 +7,7 @@ import { graphqlEtagPipelinePath } from '~/ci/pipeline_details/utils';
|
|||
import getPipelineFailedJobs from '~/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql';
|
||||
import { sortJobsByStatus } from './utils';
|
||||
import FailedJobDetails from './failed_job_details.vue';
|
||||
|
||||
const POLL_INTERVAL = 10000;
|
||||
import { POLL_INTERVAL } from './constants';
|
||||
|
||||
const JOB_ID_HEADER = __('ID');
|
||||
const JOB_NAME_HEADER = __('Name');
|
||||
|
|
@ -24,18 +23,10 @@ export default {
|
|||
},
|
||||
inject: ['graphqlPath'],
|
||||
props: {
|
||||
failedJobsCount: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
isMaximumJobLimitReached: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
isPipelineActive: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
pipelineIid: {
|
||||
type: Number,
|
||||
required: true,
|
||||
|
|
@ -52,7 +43,6 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
failedJobs: [],
|
||||
isActive: false,
|
||||
isLoadingMore: false,
|
||||
canTroubleshootJob: false,
|
||||
};
|
||||
|
|
@ -77,11 +67,6 @@ export default {
|
|||
result({ data }) {
|
||||
const pipeline = data?.project?.pipeline;
|
||||
this.canTroubleshootJob = pipeline?.troubleshootJobWithAi || false;
|
||||
|
||||
if (pipeline?.jobs?.count) {
|
||||
this.$emit('failed-jobs-count', pipeline.jobs.count);
|
||||
this.isActive = pipeline.active;
|
||||
}
|
||||
},
|
||||
error(e) {
|
||||
createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
|
||||
|
|
@ -99,43 +84,12 @@ export default {
|
|||
return this.$apollo.queries.failedJobs.loading;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isPipelineActive(flag) {
|
||||
// Turn polling on and off based on REST actions
|
||||
// By refetching jobs, we will get the graphql `active`
|
||||
// field to update properly and cascade the polling changes
|
||||
this.refetchJobs();
|
||||
this.handlePolling(flag);
|
||||
},
|
||||
isActive(flag) {
|
||||
this.handlePolling(flag);
|
||||
},
|
||||
failedJobsCount(count) {
|
||||
// If the REST data is updated first, we force a refetch
|
||||
// to keep them in sync
|
||||
if (this.failedJobs.length !== count) {
|
||||
this.$apollo.queries.failedJobs.refetch();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isActive && !this.isPipelineActive) {
|
||||
this.handlePolling(false);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePolling(isActive) {
|
||||
// If the pipeline status has changed and the widget is not expanded,
|
||||
// We start polling.
|
||||
if (isActive) {
|
||||
this.$apollo.queries.failedJobs.startPolling(POLL_INTERVAL);
|
||||
} else {
|
||||
this.$apollo.queries.failedJobs.stopPolling();
|
||||
}
|
||||
},
|
||||
async retryJob(jobName) {
|
||||
await this.refetchJobs();
|
||||
|
||||
this.$emit('job-retried');
|
||||
|
||||
this.$toast.show(sprintf(this.$options.i18n.retriedJobsSuccess, { jobName }));
|
||||
},
|
||||
async refetchJobs() {
|
||||
|
|
@ -144,7 +98,7 @@ export default {
|
|||
try {
|
||||
await this.$apollo.queries.failedJobs.refetch();
|
||||
} catch {
|
||||
createAlert(this.$options.i18n.fetchError);
|
||||
createAlert({ message: this.$options.i18n.fetchError });
|
||||
} finally {
|
||||
this.isLoadingMore = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
<script>
|
||||
import { GlBadge, GlButton, GlIcon } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import { __ } from '~/locale';
|
||||
import { getQueryHeaders } from '~/ci/pipeline_details/graph/utils';
|
||||
import { graphqlEtagPipelinePath } from '~/ci/pipeline_details/utils';
|
||||
import getPipelineFailedJobsCount from '../../graphql/queries/get_pipeline_failed_jobs_count.query.graphql';
|
||||
import FailedJobsList from './failed_jobs_list.vue';
|
||||
import { POLL_INTERVAL } from './constants';
|
||||
|
||||
export default {
|
||||
fetchError: __('An error occured fetching failed jobs count'),
|
||||
components: {
|
||||
GlBadge,
|
||||
GlButton,
|
||||
|
|
@ -12,12 +18,8 @@ export default {
|
|||
FailedJobsList,
|
||||
CrudComponent,
|
||||
},
|
||||
inject: ['fullPath'],
|
||||
inject: ['fullPath', 'graphqlPath'],
|
||||
props: {
|
||||
failedJobsCount: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
isPipelineActive: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
|
|
@ -35,42 +37,64 @@ export default {
|
|||
type: String,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
failedJobsCount: {
|
||||
context() {
|
||||
return getQueryHeaders(this.graphqlResourceEtag);
|
||||
},
|
||||
query: getPipelineFailedJobsCount,
|
||||
// Only poll if the pipeline is active
|
||||
pollInterval() {
|
||||
return this.isPipelineActive ? POLL_INTERVAL : 0;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectPath,
|
||||
pipelineIid: this.pipelineIid,
|
||||
};
|
||||
},
|
||||
update({ project }) {
|
||||
return project?.pipeline?.jobs?.count || 0;
|
||||
},
|
||||
error() {
|
||||
createAlert({ message: this.$options.i18n.fetchError });
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentFailedJobsCount: this.failedJobsCount,
|
||||
isActive: false,
|
||||
failedJobsCount: 0,
|
||||
isExpanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
graphqlResourceEtag() {
|
||||
return graphqlEtagPipelinePath(this.graphqlPath, this.pipelineIid);
|
||||
},
|
||||
bodyClasses() {
|
||||
return this.isExpanded ? '' : 'gl-hidden';
|
||||
},
|
||||
failedJobsCountLabel() {
|
||||
return `${this.isMaximumJobLimitReached ? '100+' : this.currentFailedJobsCount}`;
|
||||
failedJobsCountBadge() {
|
||||
return `${this.isMaximumJobLimitReached ? '100+' : this.failedJobsCount}`;
|
||||
},
|
||||
iconName() {
|
||||
return this.isExpanded ? 'chevron-down' : 'chevron-right';
|
||||
},
|
||||
isMaximumJobLimitReached() {
|
||||
return this.currentFailedJobsCount > 100;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
failedJobsCount(val) {
|
||||
this.currentFailedJobsCount = val;
|
||||
return this.failedJobsCount > 100;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setFailedJobsCount(count) {
|
||||
this.currentFailedJobsCount = count;
|
||||
},
|
||||
toggleWidget() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
failedJobsLabel: __('Failed jobs'),
|
||||
async refetchCount() {
|
||||
try {
|
||||
await this.$apollo.queries.failedJobsCount.refetch();
|
||||
} catch {
|
||||
createAlert({ message: this.$options.i18n.fetchError });
|
||||
}
|
||||
},
|
||||
},
|
||||
ariaControlsId: 'pipeline-failed-jobs-widget',
|
||||
};
|
||||
|
|
@ -93,24 +117,22 @@ export default {
|
|||
>
|
||||
<gl-icon :name="iconName" class="gl-mr-2" />
|
||||
<span class="gl-font-bold gl-text-subtle">
|
||||
{{ $options.i18n.failedJobsLabel }}
|
||||
{{ __('Failed jobs') }}
|
||||
</span>
|
||||
</gl-button>
|
||||
</template>
|
||||
<template #count>
|
||||
<gl-badge>
|
||||
{{ failedJobsCountLabel }}
|
||||
{{ failedJobsCountBadge }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
<failed-jobs-list
|
||||
v-if="isExpanded"
|
||||
:failed-jobs-count="failedJobsCount"
|
||||
:is-maximum-job-limit-reached="isMaximumJobLimitReached"
|
||||
:is-pipeline-active="isPipelineActive"
|
||||
:pipeline-iid="pipelineIid"
|
||||
:pipeline-path="pipelinePath"
|
||||
:project-path="projectPath"
|
||||
@failed-jobs-count="setFailedJobsCount"
|
||||
@job-retried="refetchCount"
|
||||
/>
|
||||
</crud-component>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,3 +3,7 @@ export const FILTER_PIPELINES_SEARCH_DELAY = 200;
|
|||
export const SCHEDULE_ORIGIN = 'schedule';
|
||||
export const API_ORIGIN = 'api';
|
||||
export const TRIGGER_ORIGIN = 'trigger';
|
||||
|
||||
export const CATALOG_TRACKING_LABEL = 'ci_cd_catalog_prompt';
|
||||
export const GITLAB_UNIVERSITY_TRACKING_LABEL = 'gitlab_university_prompt';
|
||||
export const MIGRATE_FROM_JENKINS_TRACKING_LABEL = 'migrate_from_jenkins_prompt';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ query getPipelineFailedJobsCount($fullPath: ID!, $pipelineIid: ID!) {
|
|||
id
|
||||
pipeline(iid: $pipelineIid) {
|
||||
id
|
||||
active
|
||||
jobs(statuses: [FAILED], retried: false, jobKind: BUILD) {
|
||||
count
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ export default {
|
|||
v-if="config.showContainerRegistrySettings"
|
||||
v-gl-tooltip="$options.i18n.SETTINGS_TEXT"
|
||||
icon="settings"
|
||||
:href="config.cleanupPoliciesSettingsPath"
|
||||
:href="config.settingsPath"
|
||||
:aria-label="$options.i18n.SETTINGS_TEXT"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -539,7 +539,7 @@
|
|||
mask-repeat: no-repeat;
|
||||
mask-size: cover;
|
||||
mask-position: center center;
|
||||
background: $black-normal;
|
||||
background: var(--gl-dropdown-option-indicator-color-selected-default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
@import '@gitlab/ui/src/tokens/build/scss/tokens.dark';
|
||||
|
||||
$black-normal: $gray-900;
|
||||
|
||||
$border-color: #4f4f4f;
|
||||
|
||||
$gl-text-color-secondary: $gray-700;
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ module ContainerRegistry
|
|||
gid_prefix: container_repository_gid_prefix,
|
||||
is_admin: current_user&.admin.to_s,
|
||||
show_cleanup_policy_link: show_cleanup_policy_link(project).to_s,
|
||||
show_container_registry_settings: show_container_registry_settings(project).to_s,
|
||||
cleanup_policies_settings_path: cleanup_image_tags_project_settings_packages_and_registries_path(project),
|
||||
show_container_registry_settings: show_container_registry_settings(project).to_s,
|
||||
settings_path: project_settings_packages_and_registries_path(project),
|
||||
connection_error: (!!connection_error).to_s,
|
||||
invalid_path_error: (!!invalid_path_error).to_s,
|
||||
user_callouts_path: callouts_path,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module DeployTokens
|
||||
class RevokeService < BaseProjectService
|
||||
attr_accessor :token, :source
|
||||
|
||||
def execute
|
||||
return ServiceResponse.error(message: 'Unauthorized to revoke project deploy token') unless can_revoke_token?
|
||||
|
||||
@token = project.deploy_tokens.find(params[:id])
|
||||
|
||||
token.revoke!
|
||||
|
||||
ServiceResponse.success(message: 'Token was revoked')
|
||||
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved
|
||||
ServiceResponse.error(message: 'Token was not revoked')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_revoke_token?
|
||||
current_user.can_admin_all_resources?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
- add_to_breadcrumbs _("Messages"), admin_broadcast_messages_path
|
||||
- breadcrumb_title @broadcast_message.id
|
||||
- add_to_breadcrumbs @broadcast_message.id, edit_admin_broadcast_message_path(@broadcast_message)
|
||||
- breadcrumb_title _("Edit")
|
||||
- page_title _("Broadcast Messages")
|
||||
|
||||
#js-broadcast-message{ data: broadcast_message_data(@broadcast_message) }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
- @gfm_form = true
|
||||
|
||||
- breadcrumb_title _('Edit')
|
||||
- add_to_breadcrumbs s_('FeatureFlags|Feature flags'), project_feature_flags_path(@project)
|
||||
- breadcrumb_title @feature_flag.name
|
||||
- add_to_breadcrumbs @feature_flag.name, edit_project_feature_flag_path(@project, @feature_flag)
|
||||
- page_title s_('FeatureFlags|Edit Feature flag'), @feature_flag.name
|
||||
|
||||
#js-edit-feature-flag{ data: edit_feature_flag_data }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- page_title _('Edit Release')
|
||||
- breadcrumb_title _('Edit')
|
||||
- add_to_breadcrumbs _('Releases'), project_releases_path(@project)
|
||||
- add_to_breadcrumbs @release.name, project_release_path(@project, @release)
|
||||
- page_title _('Edit Release')
|
||||
|
||||
#js-edit-release-page{ data: data_for_edit_release_page }
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ module MergeRequests
|
|||
AutoMergeProcessWorker.bulk_perform_in_with_contexts(
|
||||
[1, index * DELAY].max,
|
||||
merge_requests,
|
||||
arguments_proc: ->(merge_request) { [merge_request.id] },
|
||||
arguments_proc: ->(merge_request) { [{ 'merge_request_id' => merge_request.id }] },
|
||||
context_proc: ->(merge_request) { { project: merge_request.project, user: merge_request.merge_user } }
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
data_consistency :always
|
||||
data_consistency :sticky, feature_flag: :update_merge_request_worker_sticky
|
||||
|
||||
sidekiq_options retry: 3
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: update_merge_request_worker_sticky
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/403184
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171352
|
||||
rollout_issue_url:
|
||||
milestone: '17.6'
|
||||
group: group::code review
|
||||
type: worker
|
||||
default_enabled: false
|
||||
|
|
@ -379,6 +379,8 @@
|
|||
- 1
|
||||
- - gitlab_subscriptions_add_on_purchases_cleanup_user_add_on_assignment
|
||||
- 1
|
||||
- - gitlab_subscriptions_add_on_purchases_email_on_duo_bulk_user_assignments
|
||||
- 1
|
||||
- - gitlab_subscriptions_add_on_purchases_refresh_user_assignments
|
||||
- 1
|
||||
- - gitlab_subscriptions_member_management_apply_pending_member_approvals
|
||||
|
|
@ -779,6 +781,8 @@
|
|||
- 1
|
||||
- - search_zoekt_repo_marked_as_to_delete_event
|
||||
- 1
|
||||
- - search_zoekt_repo_to_index_event
|
||||
- 1
|
||||
- - search_zoekt_task_failed_event
|
||||
- 1
|
||||
- - search_zoekt_update_index_used_bytes
|
||||
|
|
|
|||
|
|
@ -81,6 +81,28 @@ Try to split all your migrations into two steps:
|
|||
1. Refactor just the Vuex API: Don't change the store structure, make sure it works in Pinia ([example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/149489)).
|
||||
1. Refactor the structure: Split your store into multiple smaller, single purpose stores.
|
||||
|
||||
### Automated migration using codemods
|
||||
|
||||
You can use [ast-grep](https://ast-grep.github.io/) codemods to simplify migration from Vuex to Pinia.
|
||||
|
||||
1. [Install ast-grep](https://ast-grep.github.io/guide/quick-start.html#installation) on your system before proceeding.
|
||||
1. Run `scripts/frontend/codemods/vuex-to-pinia/migrate.sh path/to/your/store`
|
||||
|
||||
The codemods will migrate `actions.js`, `mutations.js` and `getters.js` located in your store folder.
|
||||
Manually scan these files after running the codemods to ensure they are properly migrated.
|
||||
Vuex specs can not be automatically migrated, migrate them by hand.
|
||||
|
||||
Vuex module calls are replaced using Pinia conventions:
|
||||
|
||||
1. `dispatch('anotherModule/action', ...args, { root: true })` → `useAnotherModule().action(...args)`
|
||||
1. `dispatch('action', ...args, { root: true })` → `useRootStore().action(...args)`
|
||||
1. `rootGetters['anotherModule/getter']` → `useAnotherModule().getter`
|
||||
1. `rootGetters.getter` → `useRootStore().getter`
|
||||
1. `rootState.anotherModule.state` → `useAnotherModule().state`
|
||||
|
||||
If you have not yet migrated a dependent module (`useAnotherModule` and `useRootStore` in the examples above) you can create a temporary dummy store.
|
||||
Use the guidance below to migrate Vuex modules.
|
||||
|
||||
### Migrating stores with nested modules
|
||||
|
||||
It is not trivial to iteratively migrate stores with nested modules that have dependencies between them.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ upgrade plan, document and share it with the answers to the following questions:
|
|||
It's possible that something may go wrong during an upgrade, so it's critical
|
||||
that a rollback plan be present for that scenario. A proper rollback plan
|
||||
creates a clear path to bring the instance back to its last working state. It is
|
||||
comprised of a way to back up the instance and a way to restore it.
|
||||
comprised of a way to back up the instance and a way to restore it. You should
|
||||
test the rollback plan before you need it. For an overview of the steps required
|
||||
for rolling back, see [Downgrade](package/downgrade.md).
|
||||
|
||||
### Back up GitLab
|
||||
|
||||
|
|
|
|||
|
|
@ -295,6 +295,26 @@ A vulnerability's status can be:
|
|||
vulnerability is reintroduced and detected again, its record is reinstated and its status set to
|
||||
detected.
|
||||
|
||||
A vulnerability typically goes through the following lifecycle:
|
||||
|
||||
```mermaid
|
||||
%%{init: { "fontFamily": "GitLab Sans" }}%%
|
||||
stateDiagram
|
||||
accTitle: Vulnerability lifecycle
|
||||
accDescr: Typical lifecycle of a vulnerability
|
||||
|
||||
direction LR
|
||||
Needs_triage: Needs triage
|
||||
|
||||
[*] --> Needs_triage
|
||||
Needs_triage --> Confirmed
|
||||
Needs_triage --> Dismissed
|
||||
Dismissed --> [*]
|
||||
Confirmed --> Resolved
|
||||
Resolved --> Needs_triage: If reintroduced and detected again
|
||||
Resolved --> [*]
|
||||
```
|
||||
|
||||
## Vulnerability dismissal reasons
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4942) in GitLab 15.11 [with a flag](../../../administration/feature_flags.md) named `dismissal_reason`.
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ module Gitlab
|
|||
return if used_in_aggregate_metric
|
||||
|
||||
if property_name && legacy_events.include?(event_name)
|
||||
link = Rails.application.routes.url_helpers.help_page_url('ee/development/internal_analytics/internal_event_instrumentation/migration.md#backend-1')
|
||||
link = Rails.application.routes.url_helpers.help_page_url('development/internal_analytics/internal_event_instrumentation/migration.md', anchor: 'backend-1')
|
||||
message = "Event #{event_name} has been invoked with property_name.\n" \
|
||||
"When an event gets migrated to Internal Events, its name needs to be removed " \
|
||||
"from hll_redis_legacy_events.yml and added to hll_redis_key_overrides.yml: #{link}"
|
||||
|
|
|
|||
|
|
@ -5843,6 +5843,9 @@ msgstr ""
|
|||
msgid "An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occured fetching failed jobs count"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred creating the new branch."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40488,6 +40491,9 @@ msgstr ""
|
|||
msgid "Pipelines|%{counterLabel} more downstream pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Access GitLab University"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Active"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40539,12 +40545,21 @@ msgstr ""
|
|||
msgid "Pipelines|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Easy configuration with CI/CD Catalog"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Explore CI components in the CI/CD Catalog to see if they suit your requirements."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Explore CI/CD Catalog"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Failed to update. Please reload page to update the list of artifacts."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40557,7 +40572,7 @@ msgstr ""
|
|||
msgid "Pipelines|Full configuration is view only"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a \"Hello world\" script to see how it runs, explore how CI/CD works."
|
||||
msgid "Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a \"Hello world\" script to see how it runs, explore how CI/CD works."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Get started with GitLab CI/CD"
|
||||
|
|
@ -40581,6 +40596,12 @@ msgstr ""
|
|||
msgid "Pipelines|Last Used"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Learn CI/CD with GitLab University"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Learn how to set up and use GitLab CI/CD with guided tutorials, videos, and best practices."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Learn the basics of pipelines and .yml files"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40743,7 +40764,7 @@ msgstr ""
|
|||
msgid "Pipelines|Update Trigger"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works."
|
||||
msgid "Pipelines|Use CI/CD pipelines to automatically build, test, and deploy your code."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD."
|
||||
|
|
@ -48424,6 +48445,9 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|Prevent branch modification"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Prevent group branch modification"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|Prevent pushing and force pushing"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -49901,7 +49925,7 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Override the following project settings"
|
||||
msgid "SecurityOrchestration|Override the following project settings:"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Pipeline execution"
|
||||
|
|
@ -50363,6 +50387,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|except projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|exceptions: %{exceptions}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|group level branch input"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/query-language": "^0.0.5-a-20241017",
|
||||
"@gitlab/svgs": "3.120.0",
|
||||
"@gitlab/ui": "101.6.1",
|
||||
"@gitlab/ui": "101.6.2",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20240909013227",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
"@rails/actioncable": "7.0.8-4",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Refer to `doc/development/fe_guide/pinia.md` to learn how to use these codemods.
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
id: arrow-statement-to-function
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: export const $ACTION = ({$$$}, $$$ARGS) => $BODY
|
||||
- pattern: export const $ACTION = ({$$$}) => $BODY
|
||||
- pattern: export const $ACTION = () => $BODY
|
||||
constraints: { BODY: { kind: statement_block } }
|
||||
fix: export function $ACTION($$$ARGS) $BODY
|
||||
|
||||
---
|
||||
|
||||
id: async-arrow-statement-to-function
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: export const $ACTION = async ({$$$}, $$$ARGS) => $BODY
|
||||
- pattern: export const $ACTION = async ({$$$}) => $BODY
|
||||
- pattern: export const $ACTION = async () => $BODY
|
||||
constraints: { BODY: { kind: statement_block } }
|
||||
fix: export async function $ACTION($$$ARGS) $BODY
|
||||
|
||||
---
|
||||
|
||||
id: arrow-call-expression-to-function
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: export const $ACTION = ({$$$}, $$$ARGS) => $BODY
|
||||
- pattern: export const $ACTION = ({$$$}) => $BODY
|
||||
- pattern: export const $ACTION = () => $BODY
|
||||
constraints: { BODY: { kind: call_expression } }
|
||||
fix: |-
|
||||
export function $ACTION($$$ARGS) {
|
||||
return $BODY
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
id: strip-first-argument
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: export function $ACTION ({$$$}, $$$ARGS) { $$$BODY }
|
||||
- pattern: export function $ACTION ({$$$}) { $$$BODY }
|
||||
fix: |-
|
||||
export function $ACTION($$$ARGS) {
|
||||
$$$BODY
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
id: strip-first-argument-async
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: export async function $ACTION ({$$$}, $$$ARGS) { $$$BODY }
|
||||
- pattern: export async function $ACTION ({$$$}) { $$$BODY }
|
||||
fix: |-
|
||||
export async function $ACTION($$$ARGS) {
|
||||
$$$BODY
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
id: state
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: state.$$$REST
|
||||
inside:
|
||||
kind: export_statement
|
||||
stopBy: end
|
||||
fix: this.$$$REST
|
||||
|
||||
---
|
||||
|
||||
id: state-id
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: state
|
||||
kind: identifier
|
||||
inside:
|
||||
any:
|
||||
- kind: call_expression
|
||||
inside:
|
||||
kind: export_statement
|
||||
stopBy: end
|
||||
- kind: variable_declarator
|
||||
inside:
|
||||
kind: export_statement
|
||||
stopBy: end
|
||||
stopBy:
|
||||
kind: formal_parameters
|
||||
fix: this
|
||||
|
||||
---
|
||||
|
||||
id: root-named-state
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: rootState.$STORE_NAME.$$$REST
|
||||
transform:
|
||||
PRE:
|
||||
replace:
|
||||
source: $STORE_NAME
|
||||
replace: (?<STORE>.+)
|
||||
by: use-$STORE
|
||||
STORE:
|
||||
convert:
|
||||
source: $PRE
|
||||
toCase: camelCase
|
||||
fix: $STORE().$$$REST
|
||||
|
||||
---
|
||||
|
||||
id: commit
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: commit($KEY, $$$ARGS)
|
||||
- pattern: commit($KEY)
|
||||
fix: this[$KEY]($$$ARGS)
|
||||
|
||||
---
|
||||
|
||||
id: getters
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: getters.$$$REST
|
||||
fix: this.$$$REST
|
||||
|
||||
---
|
||||
|
||||
id: root-getters
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: rootGetters.$$$REST
|
||||
fix: useRootStore().$$$REST
|
||||
|
||||
---
|
||||
|
||||
id: root-named-getters
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: "rootGetters['$NAME']"
|
||||
constraints: { NAME: { regex: / } }
|
||||
transform:
|
||||
PRE:
|
||||
replace:
|
||||
source: $NAME
|
||||
replace: ^(?<STORE>.+)/.*
|
||||
by: use-$STORE
|
||||
STORE:
|
||||
convert:
|
||||
source: $PRE
|
||||
toCase: camelCase
|
||||
GETTER_NAME:
|
||||
replace:
|
||||
source: $NAME
|
||||
replace: ^.*\/(?<GETTER_NAME>.+)
|
||||
by: $GETTER_NAME
|
||||
fix: $STORE().$GETTER_NAME
|
||||
|
||||
---
|
||||
|
||||
id: getters-destruct
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: const { $$$REST } = getters
|
||||
fix: const { $$$REST } = this
|
||||
|
||||
---
|
||||
|
||||
id: actions
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: dispatch('$ACTION', $$$ARGS)
|
||||
not:
|
||||
regex: 'root:'
|
||||
- pattern: dispatch('$ACTION')
|
||||
fix: this.$ACTION($$$ARGS)
|
||||
|
||||
---
|
||||
|
||||
id: root-named-actions
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: "dispatch('$ACTION', $$$ARGS, { root: true })"
|
||||
constraints: { ACTION: { regex: / } }
|
||||
transform:
|
||||
STORE_PRE:
|
||||
replace:
|
||||
source: $ACTION
|
||||
replace: ^(?<STORE>.+)/.*
|
||||
by: use-$STORE
|
||||
STORE:
|
||||
convert:
|
||||
source: $STORE_PRE
|
||||
toCase: camelCase
|
||||
ACTION_NAME:
|
||||
replace:
|
||||
source: $ACTION
|
||||
replace: ^.*\/(?<ACTION_NAME>.+)
|
||||
by: $ACTION_NAME
|
||||
fix: $STORE().$ACTION_NAME($$$ARGS)
|
||||
|
||||
---
|
||||
|
||||
id: root-unnamed-actions
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: "dispatch('$ACTION', $$$ARGS, { root: true })"
|
||||
constraints: { ACTION: { not: { regex: / } } }
|
||||
fix: useRootStore().$ACTION($$$ARGS)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
id: arrow-statement-to-function
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: export const $GETTER = ($$$) => $BODY
|
||||
constraints: { BODY: { kind: statement_block } }
|
||||
fix: export function $GETTER() $BODY
|
||||
|
||||
---
|
||||
|
||||
id: arrow-call-expression-to-function
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
pattern: export const $GETTER = ($$$) => $BODY
|
||||
constraints:
|
||||
BODY:
|
||||
any:
|
||||
- kind: call_expression
|
||||
- kind: binary_expression
|
||||
- kind: member_expression
|
||||
- kind: arrow_function
|
||||
- kind: parenthesized_expression
|
||||
- kind: subscript_expression
|
||||
fix: |-
|
||||
export function $GETTER() {
|
||||
return $BODY
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
id: strip-first-argument
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern: export function $GETTER ($$$) { $$$BODY }
|
||||
fix: |-
|
||||
export function $GETTER() {
|
||||
$$$BODY
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#!/bin/bash
|
||||
|
||||
if ! command -v sg &> /dev/null; then
|
||||
printf "Command not found: sg\nPlease install ast-grep first: https://ast-grep.github.io/guide/quick-start.html\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the path to the folder is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Missing folder to the Vuex store. Usage: $0 <path_to_folder>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_PATH=$(realpath $0)
|
||||
CODEMODS_PATH=$(dirname $SCRIPT_PATH)
|
||||
FOLDER_PATH=$1
|
||||
PARTS=("actions" "getters" "mutations")
|
||||
|
||||
for item in "${PARTS[@]}"; do
|
||||
FULL_PATH="${FOLDER_PATH}/${item}.js"
|
||||
|
||||
if [[ -f "$FULL_PATH" ]]; then
|
||||
while ! sg scan "$FULL_PATH" --rule "$CODEMODS_PATH/body.yml" --report-style short; do
|
||||
sg scan "$FULL_PATH" --rule "$CODEMODS_PATH/body.yml" -U
|
||||
done
|
||||
|
||||
sg scan "$FULL_PATH" --rule "$CODEMODS_PATH/$item.yml" -U
|
||||
|
||||
echo "$FULL_PATH migrated successfully."
|
||||
else
|
||||
echo "$FULL_PATH doesn't exist, skipping."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Migration completed. Please verify each file manually."
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
id: method-definition
|
||||
language: JavaScript
|
||||
severity: error
|
||||
rule:
|
||||
any:
|
||||
- pattern:
|
||||
selector: method_definition
|
||||
context: export default { [$$$FUNC]($STATE, $$$ARGS) { $$$BODY } }
|
||||
- pattern:
|
||||
selector: method_definition
|
||||
context: export default { [$$$FUNC]($STATE) { $$$BODY } }
|
||||
fix: |-
|
||||
[$$$FUNC]($$$ARGS) {
|
||||
$$$BODY
|
||||
}
|
||||
|
|
@ -616,6 +616,10 @@ FactoryBot.define do
|
|||
empty_repo
|
||||
end
|
||||
|
||||
factory :project_with_repo, parent: :project do
|
||||
repository
|
||||
end
|
||||
|
||||
factory :forked_project_with_submodules, parent: :project do
|
||||
path { 'forked-gitlabhq' }
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ RSpec.describe 'Container Registry', :js, feature_category: :container_registry
|
|||
it 'does not have link to settings' do
|
||||
visit_container_registry
|
||||
|
||||
expect(page).not_to have_link _('Configure in settings')
|
||||
expect(page).not_to have_link(_('Configure in settings'), href: project_settings_packages_and_registries_path(project))
|
||||
end
|
||||
|
||||
it 'has link to settings when user is maintainer' do
|
||||
|
|
@ -69,7 +69,7 @@ RSpec.describe 'Container Registry', :js, feature_category: :container_registry
|
|||
|
||||
visit_container_registry
|
||||
|
||||
expect(page).to have_link _('Configure in settings')
|
||||
expect(page).to have_link(_('Configure in settings'), href: project_settings_packages_and_registries_path(project))
|
||||
end
|
||||
|
||||
context 'when there are no image repositories' do
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
|
|||
{ text: project.name, href: project_path(project) },
|
||||
{ text: 'Releases', href: project_releases_path(project) },
|
||||
{ text: release.name, href: project_release_path(project, release) },
|
||||
{ text: 'Edit Release', href: edit_project_release_path(project, release) }
|
||||
{ text: 'Edit', href: edit_project_release_path(project, release) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { GlTableLite, GlSkeletonLoader } from '@gitlab/ui';
|
||||
// fixture located in spec/frontend/fixtures/pipelines.rb
|
||||
import fixture from 'test_fixtures/pipelines/pipelines.json';
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
|
@ -29,6 +30,7 @@ describe('Pipelines Table', () => {
|
|||
|
||||
const provideWithFailedJobsWidget = {
|
||||
useFailedJobsWidget: true,
|
||||
graphqlPath: 'api/graphql',
|
||||
};
|
||||
|
||||
const { pipelines } = fixture;
|
||||
|
|
@ -198,7 +200,6 @@ describe('Pipelines Table', () => {
|
|||
|
||||
it('passes the expected props', () => {
|
||||
expect(findPipelineFailureWidget().props()).toStrictEqual({
|
||||
failedJobsCount: firstPipeline.failed_builds_count,
|
||||
isPipelineActive: firstPipeline.active,
|
||||
pipelineIid: firstPipeline.iid,
|
||||
pipelinePath: firstPipeline.path,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import { GlButton, GlCard } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
|
||||
import CiCards from '~/ci/pipelines_page/components/empty_state/ci_cards.vue';
|
||||
|
||||
describe('CI Cards', () => {
|
||||
let wrapper;
|
||||
let trackingSpy;
|
||||
let firstCardData;
|
||||
|
||||
const createComponent = ({ showJenkinsCiPrompt = true } = {}) => {
|
||||
wrapper = shallowMountExtended(CiCards, {
|
||||
provide: {
|
||||
pipelineEditorPath: '/-/ci/editor',
|
||||
showJenkinsCiPrompt,
|
||||
},
|
||||
stubs: {
|
||||
GlEmoji: { template: '<div/>' },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findAllCards = () => wrapper.findAllComponents(GlCard);
|
||||
const findCardButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
const findDescription = () => wrapper.findByTestId('ci-card-description');
|
||||
const findEmoji = () => wrapper.findByTestId('ci-card-emoji');
|
||||
const findTitle = () => wrapper.findByTestId('ci-card-title');
|
||||
|
||||
describe('structure', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
[firstCardData] = wrapper.vm.cards;
|
||||
});
|
||||
|
||||
it('renders emoji', () => {
|
||||
expect(findEmoji().exists()).toBe(true);
|
||||
expect(findEmoji().attributes('data-name')).toBe(firstCardData.emoji);
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
expect(findTitle().exists()).toBe(true);
|
||||
expect(findTitle().text()).toBe(firstCardData.title);
|
||||
});
|
||||
|
||||
it('renders description', () => {
|
||||
expect(findDescription().exists()).toBe(true);
|
||||
expect(findDescription().text()).toBe(firstCardData.description);
|
||||
});
|
||||
|
||||
it('renders button', () => {
|
||||
expect(findCardButton().exists()).toBe(true);
|
||||
expect(findCardButton().text()).toBe(firstCardData.buttonText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('visibility', () => {
|
||||
it('renders all cards if `showJenkinsCiPrompt` is true', () => {
|
||||
createComponent();
|
||||
expect(findAllCards()).toHaveLength(wrapper.vm.cards.length);
|
||||
});
|
||||
|
||||
it('hides jenkins prompt if `showJenkinsCiPrompt` is false', () => {
|
||||
createComponent({ showJenkinsCiPrompt: false });
|
||||
expect(findAllCards()).toHaveLength(wrapper.vm.cards.length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockTracking();
|
||||
});
|
||||
|
||||
it('sends an event when template is clicked', () => {
|
||||
findCardButton().vm.$emit('click');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,108 +1,46 @@
|
|||
import '~/commons';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
|
||||
import CiCards from '~/ci/pipelines_page/components/empty_state/ci_cards.vue';
|
||||
import CiTemplates from '~/ci/pipelines_page/components/empty_state/ci_templates.vue';
|
||||
|
||||
const pipelineEditorPath = '/-/ci/editor';
|
||||
|
||||
describe('Pipelines CI Templates', () => {
|
||||
let wrapper;
|
||||
let trackingSpy;
|
||||
|
||||
const createWrapper = (propsData = {}, stubs = {}) => {
|
||||
return shallowMountExtended(PipelinesCiTemplates, {
|
||||
provide: {
|
||||
pipelineEditorPath,
|
||||
showJenkinsCiPrompt: false,
|
||||
...propsData,
|
||||
},
|
||||
components: {
|
||||
GlEmoji: { template: '<img/>' },
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
const createWrapper = () => {
|
||||
return shallowMountExtended(PipelinesCiTemplates, {});
|
||||
};
|
||||
|
||||
const findMigrateFromJenkinsPrompt = () => wrapper.findByTestId('migrate-from-jenkins-prompt');
|
||||
const findMigrationPlanBtn = () => findMigrateFromJenkinsPrompt().findComponent(GlButton);
|
||||
const findTestTemplateLink = () => wrapper.findByTestId('test-template-link');
|
||||
const findCiCards = () => wrapper.findComponent(CiCards);
|
||||
const findCiTemplates = () => wrapper.findComponent(CiTemplates);
|
||||
const findPageTitle = () => wrapper.findByText('Get started with GitLab CI/CD');
|
||||
|
||||
describe('on mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
expect(findPageTitle().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cards', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
});
|
||||
|
||||
it('renders CiCards component', () => {
|
||||
expect(findCiCards().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('templates', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
});
|
||||
|
||||
it('renders test template and Ci templates', () => {
|
||||
expect(findTestTemplateLink().attributes('href')).toBe(
|
||||
pipelineEditorPath.concat('?template=Getting-Started'),
|
||||
);
|
||||
it('renders CiTemplates component', () => {
|
||||
expect(findCiTemplates().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show migrate from jenkins prompt', () => {
|
||||
expect(findMigrateFromJenkinsPrompt().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when Jenkinsfile is detected', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ showJenkinsCiPrompt: true });
|
||||
});
|
||||
|
||||
it('shows migrate from jenkins prompt', () => {
|
||||
expect(findMigrateFromJenkinsPrompt().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('opens correct link in new tab after clicking migration plan CTA', () => {
|
||||
expect(findMigrationPlanBtn().attributes('href')).toBe(
|
||||
'/help/ci/migration/plan_a_migration',
|
||||
);
|
||||
expect(findMigrationPlanBtn().attributes('target')).toBe('_blank');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmockTracking();
|
||||
});
|
||||
|
||||
it('sends an event when Getting-Started template is clicked', () => {
|
||||
findTestTemplateLink().vm.$emit('click');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
|
||||
label: 'Getting-Started',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when Jenkinsfile detected', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ showJenkinsCiPrompt: true });
|
||||
});
|
||||
|
||||
it('creates render event on page load', () => {
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
|
||||
label: 'migrate_from_jenkins_prompt',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends an event when migration plan is clicked', () => {
|
||||
findMigrationPlanBtn().vm.$emit('click');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(2);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
|
||||
label: 'migrate_from_jenkins_prompt',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import FailedJobsList from '~/ci/pipelines_page/components/failure_widget/failed
|
|||
import FailedJobDetails from '~/ci/pipelines_page/components/failure_widget/failed_job_details.vue';
|
||||
import * as utils from '~/ci/pipelines_page/components/failure_widget/utils';
|
||||
import getPipelineFailedJobs from '~/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql';
|
||||
import { failedJobsMock, failedJobsMock2, failedJobsMockEmpty, activeFailedJobsMock } from './mock';
|
||||
import { failedJobsMock, failedJobsMock2 } from './mock';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(GlToast);
|
||||
|
|
@ -23,10 +23,8 @@ describe('FailedJobsList component', () => {
|
|||
const showToast = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
failedJobsCount: 0,
|
||||
graphqlResourceEtag: 'api/graphql',
|
||||
isMaximumJobLimitReached: false,
|
||||
isPipelineActive: false,
|
||||
pipelineIid: 1,
|
||||
pipelinePath: 'namespace/project/pipeline',
|
||||
projectPath: 'namespace/project/',
|
||||
|
|
@ -103,7 +101,7 @@ describe('FailedJobsList component', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not renders a loading icon', () => {
|
||||
it('does not render a loading icon', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -136,88 +134,23 @@ describe('FailedJobsList component', () => {
|
|||
});
|
||||
|
||||
describe('polling', () => {
|
||||
it.each`
|
||||
isGraphqlActive | text
|
||||
${true} | ${'polls'}
|
||||
${false} | ${'does not poll'}
|
||||
`(`$text when isGraphqlActive: $isGraphqlActive`, async ({ isGraphqlActive }) => {
|
||||
const defaultCount = 2;
|
||||
const newCount = 1;
|
||||
|
||||
const expectedCount = isGraphqlActive ? newCount : defaultCount;
|
||||
const expectedCallCount = isGraphqlActive ? 2 : 1;
|
||||
const mockResponse = isGraphqlActive ? activeFailedJobsMock : failedJobsMock;
|
||||
|
||||
// Second result is to simulate polling with a different response
|
||||
mockFailedJobsResponse.mockResolvedValueOnce(mockResponse);
|
||||
it('polls for failed jobs', async () => {
|
||||
mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock);
|
||||
mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
|
||||
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
// Initially, we get the first response which is always the default
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
|
||||
expect(findFailedJobRows()).toHaveLength(defaultCount);
|
||||
expect(findFailedJobRows()).toHaveLength(2);
|
||||
|
||||
jest.advanceTimersByTime(10000);
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(expectedCallCount);
|
||||
expect(findFailedJobRows()).toHaveLength(expectedCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a REST action occurs', () => {
|
||||
beforeEach(() => {
|
||||
// Second result is to simulate polling with a different response
|
||||
mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock);
|
||||
mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
|
||||
});
|
||||
|
||||
it.each([true, false])('triggers a refetch of the jobs count', async (isPipelineActive) => {
|
||||
const defaultCount = 2;
|
||||
const newCount = 1;
|
||||
|
||||
createComponent({ props: { isPipelineActive } });
|
||||
await waitForPromises();
|
||||
|
||||
// Initially, we get the first response which is always the default
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
|
||||
expect(findFailedJobRows()).toHaveLength(defaultCount);
|
||||
|
||||
wrapper.setProps({ isPipelineActive: !isPipelineActive });
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(2);
|
||||
expect(findFailedJobRows()).toHaveLength(newCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the job count changes from REST', () => {
|
||||
beforeEach(() => {
|
||||
mockFailedJobsResponse.mockResolvedValue(failedJobsMockEmpty);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('and the count is the same', () => {
|
||||
it('does not re-fetch the query', async () => {
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
|
||||
|
||||
await wrapper.setProps({ failedJobsCount: 0 });
|
||||
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the count is different', () => {
|
||||
it('re-fetches the query', async () => {
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
|
||||
|
||||
await wrapper.setProps({ failedJobsCount: 10 });
|
||||
|
||||
expect(mockFailedJobsResponse).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(findFailedJobRows()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -250,7 +183,7 @@ describe('FailedJobsList component', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('refetches all failed jobs', async () => {
|
||||
it('refetches all failed jobs and emits event', async () => {
|
||||
expect(findFailedJobRows()).not.toHaveLength(
|
||||
failedJobsMock2.data.project.pipeline.jobs.nodes.length,
|
||||
);
|
||||
|
|
@ -261,6 +194,7 @@ describe('FailedJobsList component', () => {
|
|||
expect(findFailedJobRows()).toHaveLength(
|
||||
failedJobsMock2.data.project.pipeline.jobs.nodes.length,
|
||||
);
|
||||
expect(wrapper.emitted()).toEqual({ 'job-retried': [[]] });
|
||||
});
|
||||
|
||||
it('shows a toast message', async () => {
|
||||
|
|
|
|||
|
|
@ -32,14 +32,13 @@ export const allowedToFailJob = {
|
|||
allowFailure: true,
|
||||
};
|
||||
|
||||
export const createFailedJobsMockCount = ({ count = 4, active = false } = {}) => {
|
||||
export const createFailedJobsMockCount = (count = 4) => {
|
||||
return {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/20',
|
||||
pipeline: {
|
||||
id: 'gid://gitlab/Pipeline/20',
|
||||
active,
|
||||
jobs: {
|
||||
count,
|
||||
},
|
||||
|
|
@ -74,3 +73,5 @@ export const failedJobsMockEmpty = createFailedJobsMock([]);
|
|||
export const activeFailedJobsMock = createFailedJobsMock([allowedToFailJob, job], true);
|
||||
|
||||
export const failedJobsMock2 = createFailedJobsMock([job]);
|
||||
|
||||
export const failedJobsCountMock = createFailedJobsMockCount();
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
import { GlBadge, GlButton } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
|
||||
import FailedJobsList from '~/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue';
|
||||
import getPipelineFailedJobsCount from '~/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs_count.query.graphql';
|
||||
import { failedJobsCountMock } from './mock';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/alert');
|
||||
|
||||
describe('PipelineFailedJobsWidget component', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
failedJobsCount: 4,
|
||||
isPipelineActive: false,
|
||||
pipelineIid: 1,
|
||||
pipelinePath: '/pipelines/1',
|
||||
|
|
@ -19,9 +25,18 @@ describe('PipelineFailedJobsWidget component', () => {
|
|||
|
||||
const defaultProvide = {
|
||||
fullPath: 'namespace/project/',
|
||||
graphqlPath: 'api/graphql',
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {}, provide = {} } = {}) => {
|
||||
const defaultHandler = jest.fn().mockResolvedValue(failedJobsCountMock);
|
||||
|
||||
const createMockApolloProvider = (handler) => {
|
||||
const requestHandlers = [[getPipelineFailedJobsCount, handler]];
|
||||
|
||||
return createMockApollo(requestHandlers);
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {}, provide = {}, handler = defaultHandler } = {}) => {
|
||||
wrapper = shallowMountExtended(PipelineFailedJobsWidget, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
|
|
@ -32,22 +47,25 @@ describe('PipelineFailedJobsWidget component', () => {
|
|||
...provide,
|
||||
},
|
||||
stubs: { CrudComponent },
|
||||
apolloProvider: createMockApolloProvider(handler),
|
||||
});
|
||||
};
|
||||
|
||||
const findFailedJobsButton = () => wrapper.findComponent(GlButton);
|
||||
const findFailedJobsList = () => wrapper.findAllComponents(FailedJobsList);
|
||||
const findFailedJobsList = () => wrapper.findComponent(FailedJobsList);
|
||||
const findCrudComponent = () => wrapper.findComponent(CrudComponent);
|
||||
const findCount = () => wrapper.findComponent(GlBadge);
|
||||
|
||||
describe('when there are failed jobs', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('renders the show failed jobs button with correct count', () => {
|
||||
expect(findFailedJobsButton().exists()).toBe(true);
|
||||
expect(findCount().text()).toBe(String(defaultProps.failedJobsCount));
|
||||
expect(findCount().text()).toBe('4');
|
||||
});
|
||||
|
||||
it('does not render the failed jobs widget', () => {
|
||||
|
|
@ -60,6 +78,7 @@ describe('PipelineFailedJobsWidget component', () => {
|
|||
describe('when the job button is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
|
||||
await findFailedJobsButton().vm.$emit('click');
|
||||
});
|
||||
|
||||
|
|
@ -90,40 +109,6 @@ describe('PipelineFailedJobsWidget component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the job count changes', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('from the prop', () => {
|
||||
it('updates the job count', async () => {
|
||||
const newJobCount = 12;
|
||||
|
||||
expect(findCount().text()).toBe(String(defaultProps.failedJobsCount));
|
||||
|
||||
await wrapper.setProps({ failedJobsCount: newJobCount });
|
||||
|
||||
expect(findCount().text()).toBe(String(newJobCount));
|
||||
});
|
||||
});
|
||||
|
||||
describe('from the event', () => {
|
||||
beforeEach(async () => {
|
||||
await findFailedJobsButton().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('updates the job count', async () => {
|
||||
const newJobCount = 12;
|
||||
|
||||
expect(findCount().text()).toBe(String(defaultProps.failedJobsCount));
|
||||
|
||||
await findFailedJobsList().at(0).vm.$emit('failed-jobs-count', newJobCount);
|
||||
|
||||
expect(findCount().text()).toBe(String(newJobCount));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('"aria-controls" attribute', () => {
|
||||
it('is set and identifies the correct element', () => {
|
||||
createComponent();
|
||||
|
|
@ -134,4 +119,50 @@ describe('PipelineFailedJobsWidget component', () => {
|
|||
expect(findCrudComponent().attributes('id')).toBe('pipeline-failed-jobs-widget');
|
||||
});
|
||||
});
|
||||
|
||||
describe('polling', () => {
|
||||
it('does not poll for failed jobs count when pipeline is inactive', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(defaultHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(defaultHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('polls for failed jobs count when pipeline is active', async () => {
|
||||
createComponent({ props: { isPipelineActive: true } });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(defaultHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(defaultHandler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('job retry', () => {
|
||||
it('refetches failed jobs count', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(defaultHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
await findFailedJobsButton().vm.$emit('click');
|
||||
|
||||
findFailedJobsList().vm.$emit('job-retried');
|
||||
|
||||
expect(defaultHandler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ describe('List Page', () => {
|
|||
beforeEach(() => {
|
||||
const config = {
|
||||
showContainerRegistrySettings: true,
|
||||
cleanupPoliciesSettingsPath: 'bar',
|
||||
settingsPath: 'bar',
|
||||
};
|
||||
mountComponent({ config });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,9 +37,10 @@ RSpec.describe ContainerRegistry::ContainerRegistryHelper, feature_category: :co
|
|||
gid_prefix: helper.container_repository_gid_prefix,
|
||||
is_admin: user.admin.to_s,
|
||||
show_cleanup_policy_link: helper.show_cleanup_policy_link(project).to_s,
|
||||
show_container_registry_settings: helper.show_container_registry_settings(project).to_s,
|
||||
cleanup_policies_settings_path:
|
||||
helper.cleanup_image_tags_project_settings_packages_and_registries_path(project),
|
||||
show_container_registry_settings: helper.show_container_registry_settings(project).to_s,
|
||||
settings_path: helper.project_settings_packages_and_registries_path(project),
|
||||
connection_error: (!!connection_error).to_s,
|
||||
invalid_path_error: (!!invalid_path_error).to_s,
|
||||
user_callouts_path: callouts_path,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::DeployTokens::RevokeService, feature_category: :deployment_management do
|
||||
let_it_be(:entity) { create(:project) }
|
||||
let_it_be(:deploy_token) { create(:deploy_token, projects: [entity]) }
|
||||
let_it_be(:deploy_token_params) { { id: deploy_token.id } }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:revoke_service) do
|
||||
described_class.new(project: entity, current_user: user, params: deploy_token_params).execute
|
||||
end
|
||||
|
||||
context 'as admin' do
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
context 'when admin mode enabled', :enable_admin_mode do
|
||||
it 'revokes a project deploy token' do
|
||||
expect(deploy_token.revoked).to be_falsey
|
||||
expect { revoke_service }.to change { deploy_token.reload.revoked }.to be_truthy
|
||||
end
|
||||
|
||||
context 'when the token id is invalid' do
|
||||
let(:deploy_token_params) { { token_id: non_existing_record_id } }
|
||||
|
||||
it 'returns an error' do
|
||||
expect(revoke_service.status).to eq(:error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin mode disabled' do
|
||||
it 'returns an error' do
|
||||
expect(revoke_service.status).to eq(:error)
|
||||
end
|
||||
|
||||
it 'does not revoke the token' do
|
||||
revoke_service
|
||||
expect(deploy_token.reload.revoked).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as a user' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'does not revoke a project deploy token' do
|
||||
expect(revoke_service.status).to eq(:error)
|
||||
end
|
||||
|
||||
it 'does not revoke the token' do
|
||||
revoke_service
|
||||
expect(deploy_token.reload.revoked).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -136,15 +136,24 @@ RSpec.describe WorkItems::DataSync::CloneService, feature_category: :team_planni
|
|||
end
|
||||
end
|
||||
|
||||
context 'when cloning work item with success' do
|
||||
context 'when cloning work item with success', :freeze_time do
|
||||
let(:expected_original_work_item_state) { Issue.available_states[:opened] }
|
||||
let!(:original_work_item_attrs) do
|
||||
{
|
||||
iid: original_work_item.iid,
|
||||
project: target_namespace.try(:project),
|
||||
namespace: target_namespace,
|
||||
work_item_type: original_work_item.work_item_type,
|
||||
author: current_user,
|
||||
title: original_work_item.title,
|
||||
description: original_work_item.description,
|
||||
author: current_user,
|
||||
work_item_type: original_work_item.work_item_type,
|
||||
state_id: Issue.available_states[:opened],
|
||||
created_at: Time.current,
|
||||
updated_at: Time.current,
|
||||
confidential: original_work_item.confidential,
|
||||
cached_markdown_version: original_work_item.cached_markdown_version,
|
||||
lock_version: original_work_item.lock_version,
|
||||
imported_from: "none",
|
||||
updated_by: current_user,
|
||||
last_edited_at: nil,
|
||||
last_edited_by: nil,
|
||||
|
|
@ -156,8 +165,7 @@ RSpec.describe WorkItems::DataSync::CloneService, feature_category: :team_planni
|
|||
external_key: nil,
|
||||
upvotes_count: 0,
|
||||
blocking_issues_count: 0,
|
||||
project: target_namespace.try(:project),
|
||||
namespace: target_namespace
|
||||
service_desk_reply_to: target_namespace.service_desk_alias_address
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -136,18 +136,25 @@ RSpec.describe WorkItems::DataSync::MoveService, feature_category: :team_plannin
|
|||
end
|
||||
end
|
||||
|
||||
context 'when moving work item with success' do
|
||||
context 'when moving work item with success', :freeze_time do
|
||||
let(:expected_original_work_item_state) { Issue.available_states[:closed] }
|
||||
let!(:original_work_item_attrs) do
|
||||
{
|
||||
iid: original_work_item.iid,
|
||||
project: target_namespace.try(:project),
|
||||
namespace: target_namespace,
|
||||
work_item_type: original_work_item.work_item_type,
|
||||
author: original_work_item.author,
|
||||
title: original_work_item.title,
|
||||
description: original_work_item.description,
|
||||
author: original_work_item.author,
|
||||
work_item_type: original_work_item.work_item_type,
|
||||
state_id: original_work_item.state_id,
|
||||
created_at: original_work_item.reload.created_at,
|
||||
updated_by: original_work_item.updated_by,
|
||||
updated_at: original_work_item.reload.updated_at,
|
||||
confidential: original_work_item.confidential,
|
||||
cached_markdown_version: original_work_item.cached_markdown_version,
|
||||
lock_version: original_work_item.lock_version,
|
||||
imported_from: "none",
|
||||
last_edited_at: original_work_item.last_edited_at,
|
||||
last_edited_by: original_work_item.last_edited_by,
|
||||
closed_at: original_work_item.closed_at,
|
||||
|
|
@ -158,8 +165,7 @@ RSpec.describe WorkItems::DataSync::MoveService, feature_category: :team_plannin
|
|||
external_key: original_work_item.external_key,
|
||||
upvotes_count: original_work_item.upvotes_count,
|
||||
blocking_issues_count: original_work_item.blocking_issues_count,
|
||||
project: target_namespace.try(:project),
|
||||
namespace: target_namespace
|
||||
service_desk_reply_to: target_namespace.service_desk_alias_address
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,15 +35,15 @@ RSpec.describe MergeRequests::ProcessScheduledMergeWorker, :sidekiq_inline, feat
|
|||
end
|
||||
end
|
||||
|
||||
it 'schedules AutoMergeProcessWorker for each batch with increasing delay' do
|
||||
it 'schedules AutoMergeProcessWorker for each batch with increasing delay', :aggregate_failures do
|
||||
expect(AutoMergeProcessWorker)
|
||||
.to receive(:bulk_perform_in)
|
||||
.with(1.second, [[scheduled_mr.id]])
|
||||
.with(1.second, [[{ 'merge_request_id' => scheduled_mr.id }]])
|
||||
.and_call_original
|
||||
|
||||
expect(AutoMergeProcessWorker)
|
||||
.to receive(:bulk_perform_in)
|
||||
.with(7.seconds, [[other_scheduled_mr.id]])
|
||||
.with(7.seconds, [[{ 'merge_request_id' => other_scheduled_mr.id }]])
|
||||
.and_call_original
|
||||
|
||||
perform
|
||||
|
|
|
|||
|
|
@ -1407,10 +1407,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.120.0.tgz#0f26908d4ded2e3f63b6e5028afbe8b59b7fe6dc"
|
||||
integrity sha512-lsMKCgg0o3FLbNzvNPahkDtKGOH4U99bHff2aGlyqYFIpPvGFes/DfGr0IXVtWdfavZyoT42zE0/rquG64bANw==
|
||||
|
||||
"@gitlab/ui@101.6.1":
|
||||
version "101.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-101.6.1.tgz#cf5046f3460fa3d181d6cff5bbb4bb036b95a489"
|
||||
integrity sha512-2FkvNWdt4a/KrRkMoFGMiKDVHkk6xRUx7ZFJJb3NTvZpLsn/kpbRAd+7vddwcywDpUkxSiD3TJJecwfVqrGDGQ==
|
||||
"@gitlab/ui@101.6.2":
|
||||
version "101.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-101.6.2.tgz#59de2626304814bd841257a3313a5ed6ee24ce73"
|
||||
integrity sha512-ecEW9weQcsZH+nDcewoJF0PK7EOR/Eju7xhHXh6v8gZqBwIQH9F+AW2ye6qTJkkLXN0E4kLJ57/D2anc6gpStg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
echarts "^5.3.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue