Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
78cfc7cf4a
commit
0f50c47cd7
46 changed files with 1341 additions and 185 deletions
|
@ -429,6 +429,13 @@ db:check-migrations-decomposed:
|
|||
- .decomposed-database
|
||||
- .rails:rules:decomposed-databases
|
||||
|
||||
db:migrate-non-superuser:
|
||||
extends:
|
||||
- .db-job-base
|
||||
- .rails:rules:ee-and-foss-mr-with-migration
|
||||
script:
|
||||
- bundle exec rake gitlab:db:reset_as_non_superuser
|
||||
|
||||
db:gitlabcom-database-testing:
|
||||
extends: .rails:rules:db:gitlabcom-database-testing
|
||||
stage: test
|
||||
|
|
|
@ -230,6 +230,9 @@
|
|||
.controllers-patterns: &controllers-patterns
|
||||
- "{,ee/,jh/}{app/controllers}/**/*"
|
||||
|
||||
.models-patterns: &models-patterns
|
||||
- "{,ee/,jh/}{app/models}/**/*"
|
||||
|
||||
.startup-css-patterns: &startup-css-patterns
|
||||
- "{,ee/,jh/}app/assets/stylesheets/startup/**/*"
|
||||
|
||||
|
@ -1429,6 +1432,8 @@
|
|||
changes: *frontend-patterns
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
changes: *controllers-patterns
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
changes: *models-patterns
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
changes: *qa-patterns
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
|
|
|
@ -6,10 +6,10 @@ import {
|
|||
GlIcon,
|
||||
GlLink,
|
||||
GlTooltipDirective as GlTooltip,
|
||||
GlTruncate,
|
||||
} from '@gitlab/ui';
|
||||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { truncate } from '~/lib/utils/text_utility';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import DeploymentStatusBadge from './deployment_status_badge.vue';
|
||||
|
@ -25,6 +25,7 @@ export default {
|
|||
GlCollapse,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlTruncate,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
directives: {
|
||||
|
@ -75,7 +76,7 @@ export default {
|
|||
return this.deployment?.user;
|
||||
},
|
||||
username() {
|
||||
return truncate(this.user?.username, 25);
|
||||
return `@${this.user.username}`;
|
||||
},
|
||||
userPath() {
|
||||
return this.user?.path;
|
||||
|
@ -84,11 +85,23 @@ export default {
|
|||
return this.deployment?.deployable;
|
||||
},
|
||||
jobName() {
|
||||
return truncate(this.deployable?.name ?? '', 25);
|
||||
return this.deployable?.name;
|
||||
},
|
||||
jobPath() {
|
||||
return this.deployable?.buildPath;
|
||||
},
|
||||
refLabel() {
|
||||
return this.deployment?.tag ? this.$options.i18n.tag : this.$options.i18n.branch;
|
||||
},
|
||||
ref() {
|
||||
return this.deployment?.ref;
|
||||
},
|
||||
refName() {
|
||||
return this.ref?.name;
|
||||
},
|
||||
refPath() {
|
||||
return this.ref?.refPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapse() {
|
||||
|
@ -105,6 +118,8 @@ export default {
|
|||
triggerer: s__('Deployment|Triggerer'),
|
||||
job: __('Job'),
|
||||
api: __('API'),
|
||||
branch: __('Branch'),
|
||||
tag: __('Tag'),
|
||||
},
|
||||
headerClasses: [
|
||||
'gl-display-flex',
|
||||
|
@ -144,10 +159,12 @@ export default {
|
|||
<div
|
||||
v-if="iid"
|
||||
v-gl-tooltip
|
||||
class="gl-display-flex"
|
||||
:title="$options.i18n.deploymentId"
|
||||
:aria-label="$options.i18n.deploymentId"
|
||||
>
|
||||
<gl-icon ref="deployment-iid-icon" name="deployments" /> #{{ iid }}
|
||||
<gl-icon ref="deployment-iid-icon" name="deployments" />
|
||||
<span class="gl-ml-2">#{{ iid }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="shortSha"
|
||||
|
@ -163,8 +180,11 @@ export default {
|
|||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<time-ago-tooltip v-if="createdAt" :time="createdAt">
|
||||
<template #default="{ timeAgo }"> <gl-icon name="calendar" /> {{ timeAgo }} </template>
|
||||
<time-ago-tooltip v-if="createdAt" :time="createdAt" class="gl-display-flex">
|
||||
<template #default="{ timeAgo }">
|
||||
<gl-icon name="calendar" />
|
||||
<span class="gl-mr-2 gl-white-space-nowrap">{{ timeAgo }}</span>
|
||||
</template>
|
||||
</time-ago-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -180,25 +200,40 @@ export default {
|
|||
</div>
|
||||
<commit v-if="commit" :commit="commit" class="gl-mt-3" />
|
||||
<gl-collapse :visible="visible">
|
||||
<div class="gl-display-flex gl-align-items-center gl-mt-5">
|
||||
<div v-if="user" class="gl-display-flex gl-flex-direction-column">
|
||||
<span class="gl-text-gray-500 gl-font-weight-bold">{{ $options.i18n.triggerer }}</span>
|
||||
<gl-link :href="userPath" class="gl-font-monospace gl-mt-3"> @{{ username }} </gl-link>
|
||||
<div
|
||||
class="gl-display-flex gl-md-align-items-center gl-mt-5 gl-flex-direction-column gl-md-flex-direction-row gl-pr-4 gl-md-pr-0"
|
||||
>
|
||||
<div v-if="user" class="gl-display-flex gl-flex-direction-column gl-md-max-w-15p">
|
||||
<span class="gl-text-gray-500">{{ $options.i18n.triggerer }}</span>
|
||||
<gl-link :href="userPath" class="gl-font-monospace gl-mt-3">
|
||||
<gl-truncate :text="username" with-tooltip />
|
||||
</gl-link>
|
||||
</div>
|
||||
<div class="gl-display-flex gl-flex-direction-column gl-ml-5">
|
||||
<span class="gl-text-gray-500 gl-font-weight-bold" :class="{ 'gl-ml-3': !deployable }">
|
||||
<div
|
||||
class="gl-display-flex gl-flex-direction-column gl-md-pl-7 gl-md-max-w-15p gl-mt-4 gl-md-mt-0"
|
||||
>
|
||||
<span class="gl-text-gray-500" :class="{ 'gl-ml-3': !deployable }">
|
||||
{{ $options.i18n.job }}
|
||||
</span>
|
||||
<gl-link v-if="jobPath" :href="jobPath" class="gl-font-monospace gl-mt-3">
|
||||
{{ jobName }}
|
||||
<gl-truncate :text="jobName" with-tooltip position="middle" />
|
||||
</gl-link>
|
||||
<span v-else-if="jobName" class="gl-font-monospace gl-mt-3">
|
||||
{{ jobName }}
|
||||
<gl-truncate :text="jobName" with-tooltip position="middle" />
|
||||
</span>
|
||||
<gl-badge v-else class="gl-font-monospace gl-mt-3" variant="info">
|
||||
{{ $options.i18n.api }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
<div
|
||||
v-if="ref"
|
||||
class="gl-display-flex gl-flex-direction-column gl-md-pl-7 gl-md-max-w-15p gl-mt-4 gl-md-mt-0"
|
||||
>
|
||||
<span class="gl-text-gray-500">{{ refLabel }}</span>
|
||||
<gl-link :href="refPath" class="gl-font-monospace gl-mt-3">
|
||||
<gl-truncate :text="refName" with-tooltip />
|
||||
</gl-link>
|
||||
</div>
|
||||
</div>
|
||||
</gl-collapse>
|
||||
</div>
|
||||
|
|
27
app/assets/javascripts/runner/components/cells/link_cell.vue
Normal file
27
app/assets/javascripts/runner/components/cells/link_cell.vue
Normal file
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
component() {
|
||||
if (this.href) {
|
||||
return GlLink;
|
||||
}
|
||||
return 'span';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="component" :href="href" v-bind="$attrs" v-on="$listeners">
|
||||
<slot></slot>
|
||||
</component>
|
||||
</template>
|
|
@ -1,22 +1,26 @@
|
|||
<script>
|
||||
import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
|
||||
import { GlBadge, GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
|
||||
import { formatJobCount } from '../utils';
|
||||
import RunnerDetail from './runner_detail.vue';
|
||||
import RunnerGroups from './runner_groups.vue';
|
||||
import RunnerProjects from './runner_projects.vue';
|
||||
import RunnerJobs from './runner_jobs.vue';
|
||||
import RunnerTags from './runner_tags.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
GlIntersperse,
|
||||
RunnerDetail,
|
||||
RunnerGroups,
|
||||
RunnerProjects,
|
||||
RunnerJobs,
|
||||
RunnerTags,
|
||||
TimeAgo,
|
||||
},
|
||||
|
@ -53,6 +57,9 @@ export default {
|
|||
isProjectRunner() {
|
||||
return this.runner?.runnerType === PROJECT_TYPE;
|
||||
},
|
||||
jobCount() {
|
||||
return formatJobCount(this.runner?.jobCount);
|
||||
},
|
||||
},
|
||||
ACCESS_LEVEL_REF_PROTECTED,
|
||||
};
|
||||
|
@ -65,7 +72,7 @@ export default {
|
|||
|
||||
<template v-if="runner">
|
||||
<div class="gl-pt-4">
|
||||
<dl class="gl-mb-0">
|
||||
<dl class="gl-mb-0" data-testid="runner-details-list">
|
||||
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
|
||||
<runner-detail
|
||||
:label="s__('Runners|Last contact')"
|
||||
|
@ -103,5 +110,15 @@ export default {
|
|||
<runner-projects v-if="isProjectRunner" :runner="runner" />
|
||||
</template>
|
||||
</gl-tab>
|
||||
<gl-tab>
|
||||
<template #title>
|
||||
{{ s__('Runners|Jobs') }}
|
||||
<gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm">
|
||||
{{ jobCount }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
||||
<runner-jobs v-if="runner" :runner="runner" />
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</template>
|
||||
|
|
82
app/assets/javascripts/runner/components/runner_jobs.vue
Normal file
82
app/assets/javascripts/runner/components/runner_jobs.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { createAlert } from '~/flash';
|
||||
import getRunnerJobsQuery from '../graphql/get_runner_jobs.query.graphql';
|
||||
import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants';
|
||||
import { captureException } from '../sentry_utils';
|
||||
import { getPaginationVariables } from '../utils';
|
||||
import RunnerJobsTable from './runner_jobs_table.vue';
|
||||
import RunnerPagination from './runner_pagination.vue';
|
||||
|
||||
export default {
|
||||
name: 'RunnerJobs',
|
||||
components: {
|
||||
GlSkeletonLoading,
|
||||
RunnerJobsTable,
|
||||
RunnerPagination,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
jobs: {
|
||||
items: [],
|
||||
pageInfo: {},
|
||||
},
|
||||
pagination: {
|
||||
page: 1,
|
||||
},
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
jobs: {
|
||||
query: getRunnerJobsQuery,
|
||||
variables() {
|
||||
return this.variables;
|
||||
},
|
||||
update({ runner }) {
|
||||
return {
|
||||
items: runner?.jobs?.nodes || [],
|
||||
pageInfo: runner?.jobs?.pageInfo || {},
|
||||
};
|
||||
},
|
||||
error(error) {
|
||||
createAlert({ message: I18N_FETCH_ERROR });
|
||||
this.reportToSentry(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
variables() {
|
||||
const { id } = this.runner;
|
||||
return {
|
||||
id,
|
||||
...getPaginationVariables(this.pagination, RUNNER_DETAILS_JOBS_PAGE_SIZE),
|
||||
};
|
||||
},
|
||||
loading() {
|
||||
return this.$apollo.queries.jobs.loading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
},
|
||||
},
|
||||
I18N_NO_JOBS_FOUND,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-pt-3">
|
||||
<gl-skeleton-loading v-if="loading" class="gl-py-5" />
|
||||
<runner-jobs-table v-else-if="jobs.items.length" :jobs="jobs.items" />
|
||||
<p v-else>{{ $options.I18N_NO_JOBS_FOUND }}</p>
|
||||
|
||||
<runner-pagination v-model="pagination" :disabled="loading" :page-info="jobs.pageInfo" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,95 @@
|
|||
<script>
|
||||
import { GlTableLite } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||
import RunnerTags from '~/runner/components/runner_tags.vue';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { tableField } from '../utils';
|
||||
import LinkCell from './cells/link_cell.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CiBadge,
|
||||
GlTableLite,
|
||||
LinkCell,
|
||||
RunnerTags,
|
||||
TimeAgo,
|
||||
},
|
||||
props: {
|
||||
jobs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
trAttr(job) {
|
||||
if (job?.id) {
|
||||
return { 'data-testid': `job-row-${getIdFromGraphQLId(job.id)}` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
jobId(job) {
|
||||
return getIdFromGraphQLId(job.id);
|
||||
},
|
||||
jobPath(job) {
|
||||
return job.detailedStatus?.detailsPath;
|
||||
},
|
||||
projectName(job) {
|
||||
return job.pipeline?.project?.name;
|
||||
},
|
||||
projectWebUrl(job) {
|
||||
return job.pipeline?.project?.webUrl;
|
||||
},
|
||||
commitShortSha(job) {
|
||||
return job.shortSha;
|
||||
},
|
||||
commitPath(job) {
|
||||
return job.commitPath;
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
tableField({ key: 'status', label: s__('Job|Status') }),
|
||||
tableField({ key: 'job', label: __('Job') }),
|
||||
tableField({ key: 'project', label: __('Project') }),
|
||||
tableField({ key: 'commit', label: __('Commit') }),
|
||||
tableField({ key: 'finished_at', label: s__('Job|Finished at') }),
|
||||
tableField({ key: 'tags', label: s__('Runners|Tags') }),
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-table-lite
|
||||
:items="jobs"
|
||||
:fields="$options.fields"
|
||||
:tbody-tr-attr="trAttr"
|
||||
primary-key="id"
|
||||
stacked="md"
|
||||
fixed
|
||||
>
|
||||
<template #cell(status)="{ item = {} }">
|
||||
<ci-badge v-if="item.detailedStatus" :status="item.detailedStatus" />
|
||||
</template>
|
||||
|
||||
<template #cell(job)="{ item = {} }">
|
||||
<link-cell :href="jobPath(item)"> #{{ jobId(item) }} </link-cell>
|
||||
</template>
|
||||
|
||||
<template #cell(project)="{ item = {} }">
|
||||
<link-cell :href="projectWebUrl(item)">{{ projectName(item) }}</link-cell>
|
||||
</template>
|
||||
|
||||
<template #cell(commit)="{ item = {} }">
|
||||
<link-cell :href="commitPath(item)"> {{ commitShortSha(item) }}</link-cell>
|
||||
</template>
|
||||
|
||||
<template #cell(tags)="{ item = {} }">
|
||||
<runner-tags :tag-list="item.tags" />
|
||||
</template>
|
||||
|
||||
<template #cell(finished_at)="{ item = {} }">
|
||||
<time-ago v-if="item.finishedAt" :time="item.finishedAt" />
|
||||
</template>
|
||||
</gl-table-lite>
|
||||
</template>
|
|
@ -4,6 +4,7 @@ export const RUNNER_PAGE_SIZE = 20;
|
|||
export const RUNNER_JOB_COUNT_LIMIT = 1000;
|
||||
|
||||
export const RUNNER_DETAILS_PROJECTS_PAGE_SIZE = 5;
|
||||
export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30;
|
||||
|
||||
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
|
||||
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
|
||||
|
@ -45,6 +46,7 @@ export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run
|
|||
|
||||
export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})');
|
||||
export const I18N_NONE = __('None');
|
||||
export const I18N_NO_JOBS_FOUND = s__('Runner|This runner has not run any jobs.');
|
||||
|
||||
// Styles
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
||||
|
||||
query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String, $after: String) {
|
||||
runner(id: $id) {
|
||||
id
|
||||
projectCount
|
||||
jobs(before: $before, after: $after, first: $first, last: $last) {
|
||||
nodes {
|
||||
id
|
||||
detailedStatus {
|
||||
# fields for `<ci-badge>`
|
||||
id
|
||||
detailsPath
|
||||
group
|
||||
icon
|
||||
text
|
||||
}
|
||||
pipeline {
|
||||
id
|
||||
project {
|
||||
id
|
||||
name
|
||||
webUrl
|
||||
}
|
||||
}
|
||||
shortSha
|
||||
commitPath
|
||||
tags
|
||||
finishedAt
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ fragment RunnerDetailsShared on CiRunner {
|
|||
ipAddress
|
||||
description
|
||||
maximumTimeout
|
||||
jobCount
|
||||
tagList
|
||||
createdAt
|
||||
status(legacyMode: null)
|
||||
|
|
|
@ -1,37 +1,36 @@
|
|||
# Make sure that this file has the keys sorted
|
||||
---
|
||||
dast_site_profiles_pipelines:
|
||||
- table: ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
vulnerability_feedback:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
ci_pipeline_chat_data:
|
||||
- table: chat_names
|
||||
column: chat_name_id
|
||||
on_delete: async_delete
|
||||
dast_scanner_profiles_builds:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_delete
|
||||
dast_site_profiles_builds:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_delete
|
||||
dast_profiles_pipelines:
|
||||
- table: ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
clusters_applications_runners:
|
||||
- table: ci_runners
|
||||
column: runner_id
|
||||
on_delete: async_nullify
|
||||
ci_variables:
|
||||
ci_build_report_results:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_runner_projects:
|
||||
ci_builds:
|
||||
- table: users
|
||||
column: user_id
|
||||
on_delete: async_nullify
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_builds_metadata:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_daily_build_group_report_results:
|
||||
- table: namespaces
|
||||
column: group_id
|
||||
on_delete: async_delete
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_freeze_periods:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_group_variables:
|
||||
- table: namespaces
|
||||
column: group_id
|
||||
on_delete: async_delete
|
||||
ci_job_artifacts:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
|
@ -45,20 +44,13 @@ ci_job_token_project_scope_links:
|
|||
- table: projects
|
||||
column: target_project_id
|
||||
on_delete: async_delete
|
||||
ci_daily_build_group_report_results:
|
||||
ci_minutes_additional_packs:
|
||||
- table: namespaces
|
||||
column: group_id
|
||||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
external_pull_requests:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_freeze_periods:
|
||||
- table: projects
|
||||
column: project_id
|
||||
ci_namespace_mirrors:
|
||||
- table: namespaces
|
||||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
ci_pending_builds:
|
||||
- table: namespaces
|
||||
|
@ -67,37 +59,17 @@ ci_pending_builds:
|
|||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_resource_groups:
|
||||
ci_pipeline_artifacts:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_runner_namespaces:
|
||||
- table: namespaces
|
||||
column: namespace_id
|
||||
ci_pipeline_chat_data:
|
||||
- table: chat_names
|
||||
column: chat_name_id
|
||||
on_delete: async_delete
|
||||
ci_running_builds:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_namespace_mirrors:
|
||||
- table: namespaces
|
||||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
ci_sources_projects:
|
||||
- table: projects
|
||||
column: source_project_id
|
||||
on_delete: async_delete
|
||||
ci_build_report_results:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_job_artifacts:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_builds:
|
||||
ci_pipeline_schedules:
|
||||
- table: users
|
||||
column: user_id
|
||||
column: owner_id
|
||||
on_delete: async_nullify
|
||||
- table: projects
|
||||
column: project_id
|
||||
|
@ -122,97 +94,31 @@ ci_project_mirrors:
|
|||
- table: namespaces
|
||||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
ci_unit_tests:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
merge_requests:
|
||||
- table: ci_pipelines
|
||||
column: head_pipeline_id
|
||||
on_delete: async_nullify
|
||||
vulnerability_statistics:
|
||||
- table: ci_pipelines
|
||||
column: latest_pipeline_id
|
||||
on_delete: async_nullify
|
||||
vulnerability_occurrence_pipelines:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_delete
|
||||
packages_build_infos:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
packages_package_file_build_infos:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
ci_project_monthly_usages:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
pages_deployments:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_nullify
|
||||
ci_builds_metadata:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
terraform_state_versions:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_nullify
|
||||
merge_request_metrics:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_delete
|
||||
project_pages_metadata:
|
||||
- table: ci_job_artifacts
|
||||
column: artifacts_archive_id
|
||||
on_delete: async_nullify
|
||||
ci_pipeline_schedules:
|
||||
- table: users
|
||||
column: owner_id
|
||||
on_delete: async_nullify
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
merge_trains:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
ci_refs:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_group_variables:
|
||||
- table: namespaces
|
||||
column: group_id
|
||||
on_delete: async_delete
|
||||
ci_minutes_additional_packs:
|
||||
- table: namespaces
|
||||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
requirements_management_test_reports:
|
||||
- table: ci_builds
|
||||
column: build_id
|
||||
on_delete: async_nullify
|
||||
ci_subscriptions_projects:
|
||||
- table: projects
|
||||
column: downstream_project_id
|
||||
on_delete: async_delete
|
||||
- table: projects
|
||||
column: upstream_project_id
|
||||
on_delete: async_delete
|
||||
security_scans:
|
||||
- table: ci_builds
|
||||
column: build_id
|
||||
on_delete: async_delete
|
||||
ci_secure_files:
|
||||
ci_resource_groups:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_pipeline_artifacts:
|
||||
ci_runner_namespaces:
|
||||
- table: namespaces
|
||||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
ci_runner_projects:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_running_builds:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_secure_files:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
|
@ -223,10 +129,21 @@ ci_sources_pipelines:
|
|||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_sources_projects:
|
||||
- table: projects
|
||||
column: source_project_id
|
||||
on_delete: async_delete
|
||||
ci_stages:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_subscriptions_projects:
|
||||
- table: projects
|
||||
column: downstream_project_id
|
||||
on_delete: async_delete
|
||||
- table: projects
|
||||
column: upstream_project_id
|
||||
on_delete: async_delete
|
||||
ci_triggers:
|
||||
- table: users
|
||||
column: owner_id
|
||||
|
@ -234,3 +151,87 @@ ci_triggers:
|
|||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_unit_tests:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
ci_variables:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
clusters_applications_runners:
|
||||
- table: ci_runners
|
||||
column: runner_id
|
||||
on_delete: async_nullify
|
||||
dast_profiles_pipelines:
|
||||
- table: ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
dast_scanner_profiles_builds:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_delete
|
||||
dast_site_profiles_builds:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_delete
|
||||
dast_site_profiles_pipelines:
|
||||
- table: ci_pipelines
|
||||
column: ci_pipeline_id
|
||||
on_delete: async_delete
|
||||
external_pull_requests:
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
merge_request_metrics:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_delete
|
||||
merge_requests:
|
||||
- table: ci_pipelines
|
||||
column: head_pipeline_id
|
||||
on_delete: async_nullify
|
||||
merge_trains:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
packages_build_infos:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
packages_package_file_build_infos:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
pages_deployments:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_nullify
|
||||
project_pages_metadata:
|
||||
- table: ci_job_artifacts
|
||||
column: artifacts_archive_id
|
||||
on_delete: async_nullify
|
||||
requirements_management_test_reports:
|
||||
- table: ci_builds
|
||||
column: build_id
|
||||
on_delete: async_nullify
|
||||
security_scans:
|
||||
- table: ci_builds
|
||||
column: build_id
|
||||
on_delete: async_delete
|
||||
terraform_state_versions:
|
||||
- table: ci_builds
|
||||
column: ci_build_id
|
||||
on_delete: async_nullify
|
||||
vulnerability_feedback:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_nullify
|
||||
vulnerability_occurrence_pipelines:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_delete
|
||||
vulnerability_statistics:
|
||||
- table: ci_pipelines
|
||||
column: latest_pipeline_id
|
||||
on_delete: async_nullify
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddScanMethodToDastSiteProfile < Gitlab::Database::Migration[1.0]
|
||||
def up
|
||||
add_column :dast_site_profiles, :scan_method, :integer, limit: 2, default: 0, null: false
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :dast_site_profiles, :scan_method
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class UpdateDefaultScanMethodOfDastSiteProfile < Gitlab::Database::Migration[1.0]
|
||||
BATCH_SIZE = 500
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
each_batch_range('dast_site_profiles', scope: ->(table) { table.where(target_type: 1) }, of: BATCH_SIZE) do |min, max|
|
||||
execute <<~SQL
|
||||
UPDATE dast_site_profiles
|
||||
SET scan_method = 1
|
||||
WHERE id BETWEEN #{min} AND #{max}
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# noop
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixApprovalRulesCodeOwnersRuleTypeIndex < Gitlab::Database::Migration[1.0]
|
||||
INDEX_NAME = 'index_approval_rules_code_owners_rule_type'
|
||||
OLD_INDEX_NAME = 'index_approval_rules_code_owners_rule_type_old'
|
||||
TABLE = :approval_merge_request_rules
|
||||
COLUMN = :merge_request_id
|
||||
WHERE_CONDITION = 'rule_type = 2'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
rename_index TABLE, INDEX_NAME, OLD_INDEX_NAME if index_exists_by_name?(TABLE, INDEX_NAME) && !index_exists_by_name?(TABLE, OLD_INDEX_NAME)
|
||||
|
||||
add_concurrent_index TABLE, COLUMN, where: WHERE_CONDITION, name: INDEX_NAME
|
||||
|
||||
remove_concurrent_index_by_name TABLE, OLD_INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
# No-op
|
||||
end
|
||||
end
|
1
db/schema_migrations/20220119220620
Normal file
1
db/schema_migrations/20220119220620
Normal file
|
@ -0,0 +1 @@
|
|||
535f476a358dcb3f3472f1e0ec1afef738f995197b5d1f4fcd61e58a9c9e8e75
|
1
db/schema_migrations/20220128155814
Normal file
1
db/schema_migrations/20220128155814
Normal file
|
@ -0,0 +1 @@
|
|||
77cc8fc86f2c6a5ed017dde40dd4db796821a35e6ce4d8dcbe24b2cdaccbb5d9
|
1
db/schema_migrations/20220208171826
Normal file
1
db/schema_migrations/20220208171826
Normal file
|
@ -0,0 +1 @@
|
|||
e48473172d7561fb7474e16e291e555843c0ec4543300b007f86cd4a5923db85
|
|
@ -13414,6 +13414,7 @@ CREATE TABLE dast_site_profiles (
|
|||
auth_password_field text,
|
||||
auth_username text,
|
||||
target_type smallint DEFAULT 0 NOT NULL,
|
||||
scan_method smallint DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT check_5203110fee CHECK ((char_length(auth_username_field) <= 255)),
|
||||
CONSTRAINT check_6cfab17b48 CHECK ((char_length(name) <= 255)),
|
||||
CONSTRAINT check_c329dffdba CHECK ((char_length(auth_password_field) <= 255)),
|
||||
|
|
|
@ -17,6 +17,7 @@ swap:
|
|||
e-mail: '"email"'
|
||||
GFM: '"GitLab Flavored Markdown"'
|
||||
it is recommended: '"we recommend"'
|
||||
navigate: go
|
||||
OAuth2: '"OAuth 2.0"'
|
||||
once that: '"after that"'
|
||||
once the: '"after the"'
|
||||
|
|
|
@ -561,6 +561,8 @@ Do not use **navigate**. Use **go** instead. For example:
|
|||
- Go to this webpage.
|
||||
- Open a terminal and go to the `runner` directory.
|
||||
|
||||
([Vale](../testing.md#vale) rule: [`SubstitutionSuggestions.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionSuggestions.yml))
|
||||
|
||||
## need to, should
|
||||
|
||||
Try to avoid **needs to**, because it's wordy. Avoid **should** when you can be more specific. If something is required, use **must**.
|
||||
|
|
|
@ -15,6 +15,7 @@ For any of the following scenarios, the `start-review-app-pipeline` job would be
|
|||
- for merge requests with CI config changes
|
||||
- for merge requests with frontend changes
|
||||
- for merge requests with changes to `{,ee/,jh/}{app/controllers}/**/*`
|
||||
- for merge requests with changes to `{,ee/,jh/}{app/models}/**/*`
|
||||
- for merge requests with QA changes
|
||||
- for scheduled pipelines
|
||||
- the MR has the `pipeline:run-review-app` label set
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
BIN
doc/user/analytics/img/issues_created_per_month_v14_8.png
Normal file
BIN
doc/user/analytics/img/issues_created_per_month_v14_8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -34,7 +34,7 @@ You can change the total number of months displayed by setting a URL parameter.
|
|||
For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
|
||||
shows a total of 15 months for the chart in the GitLab.org group.
|
||||
|
||||
![Issues created per month](img/issues_created_per_month_v13_11.png)
|
||||
![Issues created per month](img/issues_created_per_month_v14_8.png)
|
||||
|
||||
## Drill into the information
|
||||
|
||||
|
|
BIN
doc/user/profile/img/personal_readme_setup_v14_5.png
Normal file
BIN
doc/user/profile/img/personal_readme_setup_v14_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
|
@ -102,20 +102,36 @@ user profiles are only visible to signed-in users.
|
|||
|
||||
## Add details to your profile with a README
|
||||
|
||||
### *Add personal README to profile*
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
|
||||
|
||||
If you want to add more information to your profile page, you can create a README file. When you populate the README file with information, it's included on your profile page.
|
||||
You can add more information to your profile page with a README file. When you populate
|
||||
the README file with information, it's included on your profile page.
|
||||
|
||||
To add a README to your profile:
|
||||
### From a new project
|
||||
|
||||
1. Create a new public project with the same project path as your GitLab username.
|
||||
To create a new project and add its README to your profile:
|
||||
|
||||
1. On the top bar, select **Menu > Project**.
|
||||
1. Select **Create new project**.
|
||||
1. Select **Create blank project**.
|
||||
1. Enter the project details:
|
||||
- In the **Project name** field, enter the name for your new project.
|
||||
- In the **Project URL** field, select your GitLab username.
|
||||
- In the **Project slug** field, enter your GitLab username.
|
||||
1. For **Visibility Level**, select **Public**.
|
||||
![Proper project path for an individual on the hosted product](img/personal_readme_setup_v14_5.png)
|
||||
1. For **Project Configuration**, ensure **Initialize repository with a README** is selected.
|
||||
1. Select **Create project**.
|
||||
1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files).
|
||||
1. Populate the README file with [Markdown](../markdown.md).
|
||||
|
||||
To use an existing project, [update the path](../project/settings/index.md#renaming-a-repository) of the project to match
|
||||
your username.
|
||||
GitLab displays the contents of your README below your contribution graph.
|
||||
|
||||
### From an existing project
|
||||
|
||||
To add the README from an existing project to your profile,
|
||||
[update the path](../project/settings/index.md#renaming-a-repository) of the project
|
||||
to match your username.
|
||||
|
||||
## Add external accounts to your user profile page
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
LOG_MAX_DURATION_THRESHOLD = 2.seconds
|
||||
|
||||
def initialize(project:, current_user:, sha: nil)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
|
@ -49,12 +51,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def static_validation(content)
|
||||
result = Gitlab::Ci::YamlProcessor.new(
|
||||
content,
|
||||
project: @project,
|
||||
user: @current_user,
|
||||
sha: @sha
|
||||
).execute
|
||||
logger = build_logger
|
||||
|
||||
result = yaml_processor_result(content, logger)
|
||||
|
||||
Result.new(
|
||||
jobs: static_validation_convert_to_jobs(result),
|
||||
|
@ -62,6 +61,17 @@ module Gitlab
|
|||
errors: result.errors,
|
||||
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
|
||||
)
|
||||
ensure
|
||||
logger.commit(pipeline: ::Ci::Pipeline.new, caller: self.class.name)
|
||||
end
|
||||
|
||||
def yaml_processor_result(content, logger)
|
||||
logger.instrument(:yaml_process) do
|
||||
Gitlab::Ci::YamlProcessor.new(content, project: @project,
|
||||
user: @current_user,
|
||||
sha: @sha,
|
||||
logger: logger).execute
|
||||
end
|
||||
end
|
||||
|
||||
def dry_run_convert_to_jobs(stages)
|
||||
|
@ -109,6 +119,17 @@ module Gitlab
|
|||
|
||||
jobs
|
||||
end
|
||||
|
||||
def build_logger
|
||||
Gitlab::Ci::Pipeline::Logger.new(project: @project) do |l|
|
||||
l.log_when do |observations|
|
||||
values = observations['yaml_process_duration_s']
|
||||
next false if values.empty?
|
||||
|
||||
values.max >= LOG_MAX_DURATION_THRESHOLD
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -59,7 +59,7 @@ module Gitlab
|
|||
attributes = {
|
||||
class: self.class.name.to_s,
|
||||
pipeline_creation_caller: caller,
|
||||
project_id: project.id,
|
||||
project_id: project&.id, # project is not available when called from `/ci/lint`
|
||||
pipeline_persisted: pipeline.persisted?,
|
||||
pipeline_source: pipeline.source,
|
||||
pipeline_creation_service_duration_s: age
|
||||
|
|
|
@ -109,6 +109,26 @@ module Gitlab
|
|||
name.to_s == CI_DATABASE_NAME
|
||||
end
|
||||
|
||||
class PgUser < ApplicationRecord
|
||||
self.table_name = 'pg_user'
|
||||
self.primary_key = :usename
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def self.check_for_non_superuser
|
||||
user = PgUser.find_by('usename = CURRENT_USER')
|
||||
am_i_superuser = user.usesuper
|
||||
|
||||
Gitlab::AppLogger.info(
|
||||
"Account details: User: \"#{user.usename}\", UseSuper: (#{am_i_superuser})"
|
||||
)
|
||||
|
||||
raise 'Error: detected superuser' if am_i_superuser
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
raise 'User CURRENT_USER not found'
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def self.check_postgres_version_and_print_warning
|
||||
return if Gitlab::Runtime.rails_runner?
|
||||
|
||||
|
|
|
@ -270,6 +270,19 @@ namespace :gitlab do
|
|||
end
|
||||
end
|
||||
|
||||
desc 'Run migration as gitlab non-superuser'
|
||||
task :reset_as_non_superuser, [:username] => :environment do |_, args|
|
||||
username = args.fetch(:username, 'gitlab')
|
||||
puts "Migrate using username #{username}"
|
||||
Rake::Task['db:drop'].invoke
|
||||
Rake::Task['db:create'].invoke
|
||||
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
|
||||
ActiveRecord::Base.establish_connection(db_config.configuration_hash.merge(username: username)) # rubocop: disable Database/EstablishConnection
|
||||
Gitlab::Database.check_for_non_superuser
|
||||
Rake::Task['db:migrate'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
# Only for development environments,
|
||||
# we execute pending data migrations inline for convenience.
|
||||
Rake::Task['db:migrate'].enhance do
|
||||
|
|
|
@ -20374,6 +20374,9 @@ msgstr ""
|
|||
msgid "It looks like you have some draft commits in this branch."
|
||||
msgstr ""
|
||||
|
||||
msgid "It looks like you're attempting to activate your subscription. Use %{a_start}the Subscription page%{a_end} instead."
|
||||
msgstr ""
|
||||
|
||||
msgid "It may be several days before you see feature usage data."
|
||||
msgstr ""
|
||||
|
||||
|
@ -20878,6 +20881,9 @@ msgstr ""
|
|||
msgid "Job|Erase job log and artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Job|Finished at"
|
||||
msgstr ""
|
||||
|
||||
msgid "Job|Job artifacts"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20902,6 +20908,9 @@ msgstr ""
|
|||
msgid "Job|Show complete raw"
|
||||
msgstr ""
|
||||
|
||||
msgid "Job|Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Job|The artifacts were removed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27138,9 +27147,6 @@ msgstr ""
|
|||
msgid "Please enter a valid time interval"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please enter or upload a valid license."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please enter your current password."
|
||||
msgstr ""
|
||||
|
||||
|
@ -31321,6 +31327,9 @@ msgstr ""
|
|||
msgid "Runners|Instance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Last contact"
|
||||
msgstr ""
|
||||
|
||||
|
@ -31561,6 +31570,9 @@ msgstr ""
|
|||
msgid "Runners|stale"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runner|This runner has not run any jobs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36336,6 +36348,9 @@ msgstr ""
|
|||
msgid "The latest pipeline for this merge request has failed."
|
||||
msgstr ""
|
||||
|
||||
msgid "The license key is invalid."
|
||||
msgstr ""
|
||||
|
||||
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
|
||||
msgstr ""
|
||||
|
||||
|
@ -36351,6 +36366,9 @@ msgstr ""
|
|||
msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
|
||||
msgstr ""
|
||||
|
||||
msgid "The license you uploaded is invalid. If the issue persists, contact support at %{link}."
|
||||
msgstr ""
|
||||
|
||||
msgid "The list creation wizard is already open"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -213,8 +213,28 @@ describe('~/environments/components/deployment.vue', () => {
|
|||
expect(job.attributes('href')).toBe(deployment.deployable.buildPath);
|
||||
const apiBadge = wrapper.findByText(__('API'));
|
||||
expect(apiBadge.exists()).toBe(false);
|
||||
|
||||
const branchLabel = wrapper.findByText(__('Branch'));
|
||||
expect(branchLabel.exists()).toBe(true);
|
||||
const tagLabel = wrapper.findByText(__('Tag'));
|
||||
expect(tagLabel.exists()).toBe(false);
|
||||
const ref = wrapper.findByRole('link', { name: deployment.ref.name });
|
||||
expect(ref.attributes('href')).toBe(deployment.ref.refPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with tagged deployment', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } });
|
||||
await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
|
||||
});
|
||||
|
||||
it('shows tag instead of branch', () => {
|
||||
const refLabel = wrapper.findByText(__('Tag'));
|
||||
expect(refLabel.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with API deployment', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } });
|
||||
|
@ -237,7 +257,7 @@ describe('~/environments/components/deployment.vue', () => {
|
|||
});
|
||||
|
||||
it('shows a span instead of a link', () => {
|
||||
const job = wrapper.findByText(deployment.deployable.name);
|
||||
const job = wrapper.findByTitle(deployment.deployable.name);
|
||||
expect(job.attributes('href')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
|
|||
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
|
||||
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
|
||||
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
|
||||
let_it_be(:build) { create(:ci_build, runner: instance_runner) }
|
||||
|
||||
query_path = 'runner/graphql/'
|
||||
fixtures_path = 'graphql/runner/'
|
||||
|
@ -104,6 +105,22 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
|
|||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe GraphQL::Query, type: :request do
|
||||
get_runner_jobs_query_name = 'get_runner_jobs.query.graphql'
|
||||
|
||||
let_it_be(:query) do
|
||||
get_graphql_query_as_string("#{query_path}#{get_runner_jobs_query_name}")
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_runner_jobs_query_name}.json" do
|
||||
post_graphql(query, current_user: admin, variables: {
|
||||
id: instance_runner.to_global_id.to_s
|
||||
})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe do
|
||||
|
|
72
spec/frontend/runner/components/cells/link_cell_spec.js
Normal file
72
spec/frontend/runner/components/cells/link_cell_spec.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { GlLink } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import LinkCell from '~/runner/components/cells/link_cell.vue';
|
||||
|
||||
describe('LinkCell', () => {
|
||||
let wrapper;
|
||||
|
||||
const findGlLink = () => wrapper.find(GlLink);
|
||||
const findSpan = () => wrapper.find('span');
|
||||
|
||||
const createComponent = ({ props = {}, ...options } = {}) => {
|
||||
wrapper = shallowMountExtended(LinkCell, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
it('when an href is provided, renders a link', () => {
|
||||
createComponent({ props: { href: '/url' } });
|
||||
expect(findGlLink().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('when an href is not provided, renders no link', () => {
|
||||
createComponent();
|
||||
expect(findGlLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
href | findContent
|
||||
${null} | ${findSpan}
|
||||
${'/url'} | ${findGlLink}
|
||||
`('When href is $href', ({ href, findContent }) => {
|
||||
const content = 'My Text';
|
||||
const attrs = { foo: 'bar' };
|
||||
const listeners = {
|
||||
click: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: { href },
|
||||
slots: {
|
||||
default: content,
|
||||
},
|
||||
attrs,
|
||||
listeners,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
listeners.click.mockReset();
|
||||
});
|
||||
|
||||
it('Renders content', () => {
|
||||
expect(findContent().text()).toBe(content);
|
||||
});
|
||||
|
||||
it('Passes attributes', () => {
|
||||
expect(findContent().attributes()).toMatchObject(attrs);
|
||||
});
|
||||
|
||||
it('Passes event listeners', () => {
|
||||
expect(listeners.click).toHaveBeenCalledTimes(0);
|
||||
|
||||
findContent().vm.$emit('click');
|
||||
|
||||
expect(listeners.click).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import { GlSprintf, GlIntersperse } from '@gitlab/ui';
|
||||
import { GlSprintf, GlIntersperse, GlTab } from '@gitlab/ui';
|
||||
import { createWrapper, ErrorWrapper } from '@vue/test-utils';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
@ -8,6 +8,7 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner
|
|||
import RunnerDetails from '~/runner/components/runner_details.vue';
|
||||
import RunnerDetail from '~/runner/components/runner_detail.vue';
|
||||
import RunnerGroups from '~/runner/components/runner_groups.vue';
|
||||
import RunnersJobs from '~/runner/components/runner_jobs.vue';
|
||||
import RunnerTags from '~/runner/components/runner_tags.vue';
|
||||
import RunnerTag from '~/runner/components/runner_tag.vue';
|
||||
|
||||
|
@ -38,6 +39,8 @@ describe('RunnerDetails', () => {
|
|||
};
|
||||
|
||||
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
|
||||
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
|
||||
const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
|
||||
|
||||
const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
|
||||
wrapper = mountFn(RunnerDetails, {
|
||||
|
@ -146,4 +149,41 @@ describe('RunnerDetails', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Jobs tab', () => {
|
||||
const stubs = { GlTab };
|
||||
|
||||
it('without a runner, shows no jobs', () => {
|
||||
createComponent({
|
||||
props: { runner: null },
|
||||
stubs,
|
||||
});
|
||||
|
||||
expect(findJobCountBadge().exists()).toBe(false);
|
||||
expect(findRunnersJobs().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('without a job count, shows no jobs count', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
runner: { ...mockRunner, jobCount: undefined },
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
|
||||
expect(findJobCountBadge().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('with a job count, shows jobs count', () => {
|
||||
const runner = { ...mockRunner, jobCount: 3 };
|
||||
|
||||
createComponent({
|
||||
props: { runner },
|
||||
stubs,
|
||||
});
|
||||
|
||||
expect(findJobCountBadge().text()).toBe('3');
|
||||
expect(findRunnersJobs().props('runner')).toBe(runner);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
156
spec/frontend/runner/components/runner_jobs_spec.js
Normal file
156
spec/frontend/runner/components/runner_jobs_spec.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { createAlert } from '~/flash';
|
||||
import RunnerJobs from '~/runner/components/runner_jobs.vue';
|
||||
import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
|
||||
import RunnerPagination from '~/runner/components/runner_pagination.vue';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants';
|
||||
|
||||
import getRunnerJobsQuery from '~/runner/graphql/get_runner_jobs.query.graphql';
|
||||
|
||||
import { runnerData, runnerJobsData } from '../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/runner/sentry_utils');
|
||||
|
||||
const mockRunner = runnerData.data.runner;
|
||||
const mockRunnerWithJobs = runnerJobsData.data.runner;
|
||||
const mockJobs = mockRunnerWithJobs.jobs.nodes;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('RunnerJobs', () => {
|
||||
let wrapper;
|
||||
let mockRunnerJobsQuery;
|
||||
|
||||
const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading);
|
||||
const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable);
|
||||
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
|
||||
|
||||
const createComponent = ({ mountFn = shallowMountExtended } = {}) => {
|
||||
wrapper = mountFn(RunnerJobs, {
|
||||
apolloProvider: createMockApollo([[getRunnerJobsQuery, mockRunnerJobsQuery]]),
|
||||
propsData: {
|
||||
runner: mockRunner,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRunnerJobsQuery = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockRunnerJobsQuery.mockReset();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('Requests runner jobs', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(1);
|
||||
expect(mockRunnerJobsQuery).toHaveBeenCalledWith({
|
||||
id: mockRunner.id,
|
||||
first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
|
||||
});
|
||||
});
|
||||
|
||||
describe('When there are jobs assigned', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunnerJobsQuery.mockResolvedValueOnce(runnerJobsData);
|
||||
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('Shows jobs', () => {
|
||||
const jobs = findRunnerJobsTable().props('jobs');
|
||||
|
||||
expect(jobs).toHaveLength(mockJobs.length);
|
||||
expect(jobs[0]).toMatchObject(mockJobs[0]);
|
||||
});
|
||||
|
||||
describe('When "Next" page is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
findRunnerPagination().vm.$emit('input', { page: 2, after: 'AFTER_CURSOR' });
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('A new page is requested', () => {
|
||||
expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(2);
|
||||
expect(mockRunnerJobsQuery).toHaveBeenLastCalledWith({
|
||||
id: mockRunner.id,
|
||||
first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
|
||||
after: 'AFTER_CURSOR',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When loading', () => {
|
||||
it('shows loading indicator and no other content', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findGlSkeletonLoading().exists()).toBe(true);
|
||||
expect(findRunnerJobsTable().exists()).toBe(false);
|
||||
expect(findRunnerPagination().attributes('disabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When there are no jobs', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunnerJobsQuery.mockResolvedValueOnce({
|
||||
data: {
|
||||
runner: {
|
||||
id: mockRunner.id,
|
||||
projectCount: 0,
|
||||
jobs: {
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('Shows a "None" label', () => {
|
||||
expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When an error occurs', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunnerJobsQuery.mockRejectedValue(new Error('Error!'));
|
||||
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows an error', () => {
|
||||
expect(createAlert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports an error', () => {
|
||||
expect(captureException).toHaveBeenCalledWith({
|
||||
component: 'RunnerJobs',
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
119
spec/frontend/runner/components/runner_jobs_table_spec.js
Normal file
119
spec/frontend/runner/components/runner_jobs_table_spec.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { GlTableLite } from '@gitlab/ui';
|
||||
import {
|
||||
extendedWrapper,
|
||||
shallowMountExtended,
|
||||
mountExtended,
|
||||
} from 'helpers/vue_test_utils_helper';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { runnerJobsData } from '../mock_data';
|
||||
|
||||
const mockJobs = runnerJobsData.data.runner.jobs.nodes;
|
||||
|
||||
describe('RunnerJobsTable', () => {
|
||||
let wrapper;
|
||||
const mockNow = '2021-01-15T12:00:00Z';
|
||||
const mockOneHourAgo = '2021-01-15T11:00:00Z';
|
||||
|
||||
useFakeDate(mockNow);
|
||||
|
||||
const findTable = () => wrapper.findComponent(GlTableLite);
|
||||
const findHeaders = () => wrapper.findAll('th');
|
||||
const findRows = () => wrapper.findAll('[data-testid^="job-row-"]');
|
||||
const findCell = ({ field }) =>
|
||||
extendedWrapper(findRows().at(0).find(`[data-testid="td-${field}"]`));
|
||||
|
||||
const createComponent = ({ props = {} } = {}, mountFn = shallowMountExtended) => {
|
||||
wrapper = mountFn(RunnerJobsTable, {
|
||||
propsData: {
|
||||
jobs: mockJobs,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlTableLite,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('Sets job id as a row key', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findTable().attributes('primarykey')).toBe('id');
|
||||
});
|
||||
|
||||
describe('Table data', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({}, mountExtended);
|
||||
});
|
||||
|
||||
it('Displays headers', () => {
|
||||
const headerLabels = findHeaders().wrappers.map((w) => w.text());
|
||||
|
||||
expect(headerLabels).toEqual([
|
||||
s__('Job|Status'),
|
||||
__('Job'),
|
||||
__('Project'),
|
||||
__('Commit'),
|
||||
s__('Job|Finished at'),
|
||||
s__('Runners|Tags'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Displays a list of jobs', () => {
|
||||
expect(findRows()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Displays details of a job', () => {
|
||||
const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0];
|
||||
|
||||
expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text);
|
||||
|
||||
expect(findCell({ field: 'job' }).text()).toContain(`#${getIdFromGraphQLId(id)}`);
|
||||
expect(findCell({ field: 'job' }).find('a').attributes('href')).toBe(
|
||||
detailedStatus.detailsPath,
|
||||
);
|
||||
|
||||
expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name);
|
||||
expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(
|
||||
pipeline.project.webUrl,
|
||||
);
|
||||
|
||||
expect(findCell({ field: 'commit' }).text()).toBe(shortSha);
|
||||
expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Table data formatting', () => {
|
||||
let mockJobsCopy;
|
||||
|
||||
beforeEach(() => {
|
||||
mockJobsCopy = [
|
||||
{
|
||||
...mockJobs[0],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it('Formats finishedAt time', () => {
|
||||
mockJobsCopy[0].finishedAt = mockOneHourAgo;
|
||||
|
||||
createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
|
||||
|
||||
expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago');
|
||||
});
|
||||
|
||||
it('Formats tags', () => {
|
||||
mockJobsCopy[0].tags = ['tag-1', 'tag-2'];
|
||||
|
||||
createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
|
||||
|
||||
expect(findCell({ field: 'tags' }).text()).toMatchInterpolatedText('tag-1 tag-2');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -123,6 +123,7 @@ describe('RunnerUpdateForm', () => {
|
|||
|
||||
// Some read-only fields are not submitted
|
||||
const {
|
||||
__typename,
|
||||
ipAddress,
|
||||
runnerType,
|
||||
createdAt,
|
||||
|
@ -132,7 +133,7 @@ describe('RunnerUpdateForm', () => {
|
|||
userPermissions,
|
||||
version,
|
||||
groups,
|
||||
__typename,
|
||||
jobCount,
|
||||
...submitted
|
||||
} = mockRunner;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query
|
|||
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
|
||||
import runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json';
|
||||
import runnerProjectsData from 'test_fixtures/graphql/runner/get_runner_projects.query.graphql.json';
|
||||
import runnerJobsData from 'test_fixtures/graphql/runner/get_runner_jobs.query.graphql.json';
|
||||
|
||||
// Group queries
|
||||
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
|
||||
|
@ -20,6 +21,7 @@ export {
|
|||
runnerData,
|
||||
runnerWithGroupData,
|
||||
runnerProjectsData,
|
||||
runnerJobsData,
|
||||
groupRunnersData,
|
||||
groupRunnersCountData,
|
||||
groupRunnersDataPaginated,
|
||||
|
|
|
@ -322,4 +322,102 @@ RSpec.describe Gitlab::Ci::Lint do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'pipeline logger' do
|
||||
let(:counters) do
|
||||
{
|
||||
'count' => a_kind_of(Numeric),
|
||||
'avg' => a_kind_of(Numeric),
|
||||
'max' => a_kind_of(Numeric),
|
||||
'min' => a_kind_of(Numeric)
|
||||
}
|
||||
end
|
||||
|
||||
let(:loggable_data) do
|
||||
{
|
||||
'class' => 'Gitlab::Ci::Pipeline::Logger',
|
||||
'config_build_context_duration_s' => counters,
|
||||
'config_build_variables_duration_s' => counters,
|
||||
'config_compose_duration_s' => counters,
|
||||
'config_expand_duration_s' => counters,
|
||||
'config_external_process_duration_s' => counters,
|
||||
'config_stages_inject_duration_s' => counters,
|
||||
'config_tags_resolve_duration_s' => counters,
|
||||
'config_yaml_extend_duration_s' => counters,
|
||||
'config_yaml_load_duration_s' => counters,
|
||||
'pipeline_creation_caller' => 'Gitlab::Ci::Lint',
|
||||
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
|
||||
'pipeline_persisted' => false,
|
||||
'pipeline_source' => 'unknown',
|
||||
'project_id' => project&.id,
|
||||
'yaml_process_duration_s' => counters
|
||||
}
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
<<~YAML
|
||||
build:
|
||||
script: echo
|
||||
YAML
|
||||
end
|
||||
|
||||
subject(:validate) { lint.validate(content, dry_run: false) }
|
||||
|
||||
before do
|
||||
project&.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when the duration is under the threshold' do
|
||||
it 'does not create a log entry' do
|
||||
expect(Gitlab::AppJsonLogger).not_to receive(:info)
|
||||
|
||||
validate
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the durations exceeds the threshold' do
|
||||
let(:timer) do
|
||||
proc do
|
||||
@timer = @timer.to_i + 30
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab::Ci::Pipeline::Logger)
|
||||
.to receive(:current_monotonic_time) { timer.call }
|
||||
end
|
||||
|
||||
it 'creates a log entry' do
|
||||
expect(Gitlab::AppJsonLogger).to receive(:info).with(loggable_data)
|
||||
|
||||
validate
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_pipeline_creation_logger: false)
|
||||
end
|
||||
|
||||
it 'does not create a log entry' do
|
||||
expect(Gitlab::AppJsonLogger).not_to receive(:info)
|
||||
|
||||
validate
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is not provided' do
|
||||
let(:project) { nil }
|
||||
|
||||
let(:project_nil_loggable_data) do
|
||||
loggable_data.except('project_id')
|
||||
end
|
||||
|
||||
it 'creates a log entry without project_id' do
|
||||
expect(Gitlab::AppJsonLogger).to receive(:info).with(project_nil_loggable_data)
|
||||
|
||||
validate
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -203,6 +203,35 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
|
|||
expect(commit).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is not passed and pipeline is not persisted' do
|
||||
let(:project) {}
|
||||
let(:pipeline) { build(:ci_pipeline) }
|
||||
|
||||
let(:loggable_data) do
|
||||
{
|
||||
'class' => described_class.name.to_s,
|
||||
'pipeline_persisted' => false,
|
||||
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
|
||||
'pipeline_creation_caller' => 'source',
|
||||
'pipeline_save_duration_s' => {
|
||||
'avg' => 60, 'count' => 1, 'max' => 60, 'min' => 60
|
||||
},
|
||||
'pipeline_creation_duration_s' => {
|
||||
'avg' => 20, 'count' => 2, 'max' => 30, 'min' => 10
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'logs to application.json' do
|
||||
expect(Gitlab::AppJsonLogger)
|
||||
.to receive(:info)
|
||||
.with(a_hash_including(loggable_data))
|
||||
.and_call_original
|
||||
|
||||
expect(commit).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
|
|
|
@ -18,6 +18,15 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
|
|||
))
|
||||
end
|
||||
|
||||
context 'ensure keys are sorted' do
|
||||
it 'does not have any keys that are out of order' do
|
||||
parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path)
|
||||
mapping = parsed.children.first
|
||||
table_names = mapping.children.select(&:scalar?).map(&:value)
|
||||
expect(table_names).to eq(table_names.sort), "expected sorted table names in the YAML file"
|
||||
end
|
||||
end
|
||||
|
||||
context 'ensure no duplicates are found' do
|
||||
it 'does not have duplicate tables defined' do
|
||||
# since we use hash to detect duplicate hash keys we need to parse YAML document
|
||||
|
|
|
@ -104,6 +104,34 @@ RSpec.describe Gitlab::Database do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.check_for_non_superuser' do
|
||||
subject { described_class.check_for_non_superuser }
|
||||
|
||||
let(:non_superuser) { Gitlab::Database::PgUser.new(usename: 'foo', usesuper: false ) }
|
||||
let(:superuser) { Gitlab::Database::PgUser.new(usename: 'bar', usesuper: true) }
|
||||
|
||||
it 'prints user details if not superuser' do
|
||||
allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(non_superuser)
|
||||
|
||||
expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"foo\", UseSuper: (false)")
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'raises an exception if superuser' do
|
||||
allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(superuser)
|
||||
|
||||
expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"bar\", UseSuper: (true)")
|
||||
expect { subject }.to raise_error('Error: detected superuser')
|
||||
end
|
||||
|
||||
it 'catches exception if find_by fails' do
|
||||
allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_raise(ActiveRecord::StatementInvalid)
|
||||
|
||||
expect { subject }.to raise_error('User CURRENT_USER not found')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.check_postgres_version_and_print_warning' do
|
||||
let(:reflect) { instance_spy(Gitlab::Database::Reflection) }
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!('fix_approval_rules_code_owners_rule_type_index')
|
||||
|
||||
RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration do
|
||||
let(:table_name) { :approval_merge_request_rules }
|
||||
let(:index_name) { 'index_approval_rules_code_owners_rule_type' }
|
||||
|
||||
it 'correctly migrates up and down' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the index already exists' do
|
||||
before do
|
||||
subject.add_concurrent_index table_name, :merge_request_id, where: 'rule_type = 2', name: index_name
|
||||
end
|
||||
|
||||
it 'keeps the index' do
|
||||
migrate!
|
||||
|
||||
expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:dast_sites) { table(:dast_sites) }
|
||||
let(:dast_site_profiles) { table(:dast_site_profiles) }
|
||||
|
||||
before do
|
||||
namespace = namespaces.create!(name: 'test', path: 'test')
|
||||
project = projects.create!(id: 12, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab')
|
||||
dast_site = dast_sites.create!(id: 1, url: 'https://www.gitlab.com', project_id: project.id)
|
||||
|
||||
dast_site_profiles.create!(id: 1, project_id: project.id, dast_site_id: dast_site.id,
|
||||
name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 0",
|
||||
scan_method: 0, target_type: 0)
|
||||
|
||||
dast_site_profiles.create!(id: 2, project_id: project.id, dast_site_id: dast_site.id,
|
||||
name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 1",
|
||||
scan_method: 0, target_type: 1)
|
||||
end
|
||||
|
||||
it 'updates the scan_method to 1 for profiles with target_type 1' do
|
||||
migrate!
|
||||
|
||||
expect(dast_site_profiles.where(scan_method: 1).count).to eq 1
|
||||
expect(dast_site_profiles.where(scan_method: 0).count).to eq 1
|
||||
end
|
||||
end
|
|
@ -446,6 +446,44 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'gitlab:db:reset_as_non_superuser' do
|
||||
let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool ) }
|
||||
let(:connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
|
||||
let(:configurations) { double(ActiveRecord::DatabaseConfigurations) }
|
||||
let(:configuration) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig) }
|
||||
let(:config_hash) { { username: 'foo' } }
|
||||
|
||||
it 'migrate as nonsuperuser check with default username' do
|
||||
allow(Rake::Task['db:drop']).to receive(:invoke)
|
||||
allow(Rake::Task['db:create']).to receive(:invoke)
|
||||
allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
|
||||
allow(configurations).to receive(:configs_for).and_return([configuration])
|
||||
allow(configuration).to receive(:configuration_hash).and_return(config_hash)
|
||||
allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
|
||||
|
||||
expect(config_hash).to receive(:merge).with({ username: 'gitlab' })
|
||||
expect(Gitlab::Database).to receive(:check_for_non_superuser)
|
||||
expect(Rake::Task['db:migrate']).to receive(:invoke)
|
||||
|
||||
run_rake_task('gitlab:db:reset_as_non_superuser')
|
||||
end
|
||||
|
||||
it 'migrate as nonsuperuser check with specified username' do
|
||||
allow(Rake::Task['db:drop']).to receive(:invoke)
|
||||
allow(Rake::Task['db:create']).to receive(:invoke)
|
||||
allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
|
||||
allow(configurations).to receive(:configs_for).and_return([configuration])
|
||||
allow(configuration).to receive(:configuration_hash).and_return(config_hash)
|
||||
allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
|
||||
|
||||
expect(config_hash).to receive(:merge).with({ username: 'foo' })
|
||||
expect(Gitlab::Database).to receive(:check_for_non_superuser)
|
||||
expect(Rake::Task['db:migrate']).to receive(:invoke)
|
||||
|
||||
run_rake_task('gitlab:db:reset_as_non_superuser', '[foo]')
|
||||
end
|
||||
end
|
||||
|
||||
def run_rake_task(task_name, arguments = '')
|
||||
Rake::Task[task_name].reenable
|
||||
Rake.application.invoke_task("#{task_name}#{arguments}")
|
||||
|
|
Loading…
Reference in a new issue