Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-15 18:14:39 +00:00
parent 78cfc7cf4a
commit 0f50c47cd7
46 changed files with 1341 additions and 185 deletions

View file

@ -429,6 +429,13 @@ db:check-migrations-decomposed:
- .decomposed-database - .decomposed-database
- .rails:rules:decomposed-databases - .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: db:gitlabcom-database-testing:
extends: .rails:rules:db:gitlabcom-database-testing extends: .rails:rules:db:gitlabcom-database-testing
stage: test stage: test

View file

@ -230,6 +230,9 @@
.controllers-patterns: &controllers-patterns .controllers-patterns: &controllers-patterns
- "{,ee/,jh/}{app/controllers}/**/*" - "{,ee/,jh/}{app/controllers}/**/*"
.models-patterns: &models-patterns
- "{,ee/,jh/}{app/models}/**/*"
.startup-css-patterns: &startup-css-patterns .startup-css-patterns: &startup-css-patterns
- "{,ee/,jh/}app/assets/stylesheets/startup/**/*" - "{,ee/,jh/}app/assets/stylesheets/startup/**/*"
@ -1429,6 +1432,8 @@
changes: *frontend-patterns changes: *frontend-patterns
- <<: *if-dot-com-gitlab-org-merge-request - <<: *if-dot-com-gitlab-org-merge-request
changes: *controllers-patterns changes: *controllers-patterns
- <<: *if-dot-com-gitlab-org-merge-request
changes: *models-patterns
- <<: *if-dot-com-gitlab-org-merge-request - <<: *if-dot-com-gitlab-org-merge-request
changes: *qa-patterns changes: *qa-patterns
- <<: *if-dot-com-gitlab-org-merge-request - <<: *if-dot-com-gitlab-org-merge-request

View file

@ -6,10 +6,10 @@ import {
GlIcon, GlIcon,
GlLink, GlLink,
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
GlTruncate,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { truncate } from '~/lib/utils/text_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeploymentStatusBadge from './deployment_status_badge.vue'; import DeploymentStatusBadge from './deployment_status_badge.vue';
@ -25,6 +25,7 @@ export default {
GlCollapse, GlCollapse,
GlIcon, GlIcon,
GlLink, GlLink,
GlTruncate,
TimeAgoTooltip, TimeAgoTooltip,
}, },
directives: { directives: {
@ -75,7 +76,7 @@ export default {
return this.deployment?.user; return this.deployment?.user;
}, },
username() { username() {
return truncate(this.user?.username, 25); return `@${this.user.username}`;
}, },
userPath() { userPath() {
return this.user?.path; return this.user?.path;
@ -84,11 +85,23 @@ export default {
return this.deployment?.deployable; return this.deployment?.deployable;
}, },
jobName() { jobName() {
return truncate(this.deployable?.name ?? '', 25); return this.deployable?.name;
}, },
jobPath() { jobPath() {
return this.deployable?.buildPath; 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: { methods: {
toggleCollapse() { toggleCollapse() {
@ -105,6 +118,8 @@ export default {
triggerer: s__('Deployment|Triggerer'), triggerer: s__('Deployment|Triggerer'),
job: __('Job'), job: __('Job'),
api: __('API'), api: __('API'),
branch: __('Branch'),
tag: __('Tag'),
}, },
headerClasses: [ headerClasses: [
'gl-display-flex', 'gl-display-flex',
@ -144,10 +159,12 @@ export default {
<div <div
v-if="iid" v-if="iid"
v-gl-tooltip v-gl-tooltip
class="gl-display-flex"
:title="$options.i18n.deploymentId" :title="$options.i18n.deploymentId"
:aria-label="$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>
<div <div
v-if="shortSha" v-if="shortSha"
@ -163,8 +180,11 @@ export default {
size="small" size="small"
/> />
</div> </div>
<time-ago-tooltip v-if="createdAt" :time="createdAt"> <time-ago-tooltip v-if="createdAt" :time="createdAt" class="gl-display-flex">
<template #default="{ timeAgo }"> <gl-icon name="calendar" /> {{ timeAgo }} </template> <template #default="{ timeAgo }">
<gl-icon name="calendar" />
<span class="gl-mr-2 gl-white-space-nowrap">{{ timeAgo }}</span>
</template>
</time-ago-tooltip> </time-ago-tooltip>
</div> </div>
</div> </div>
@ -180,25 +200,40 @@ export default {
</div> </div>
<commit v-if="commit" :commit="commit" class="gl-mt-3" /> <commit v-if="commit" :commit="commit" class="gl-mt-3" />
<gl-collapse :visible="visible"> <gl-collapse :visible="visible">
<div class="gl-display-flex gl-align-items-center gl-mt-5"> <div
<div v-if="user" class="gl-display-flex gl-flex-direction-column"> 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"
<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 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>
<div class="gl-display-flex gl-flex-direction-column gl-ml-5"> <div
<span class="gl-text-gray-500 gl-font-weight-bold" :class="{ 'gl-ml-3': !deployable }"> 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 }} {{ $options.i18n.job }}
</span> </span>
<gl-link v-if="jobPath" :href="jobPath" class="gl-font-monospace gl-mt-3"> <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> </gl-link>
<span v-else-if="jobName" class="gl-font-monospace gl-mt-3"> <span v-else-if="jobName" class="gl-font-monospace gl-mt-3">
{{ jobName }} <gl-truncate :text="jobName" with-tooltip position="middle" />
</span> </span>
<gl-badge v-else class="gl-font-monospace gl-mt-3" variant="info"> <gl-badge v-else class="gl-font-monospace gl-mt-3" variant="info">
{{ $options.i18n.api }} {{ $options.i18n.api }}
</gl-badge> </gl-badge>
</div> </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> </div>
</gl-collapse> </gl-collapse>
</div> </div>

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

View file

@ -1,22 +1,26 @@
<script> <script>
import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui'; import { GlBadge, GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import { formatJobCount } from '../utils';
import RunnerDetail from './runner_detail.vue'; import RunnerDetail from './runner_detail.vue';
import RunnerGroups from './runner_groups.vue'; import RunnerGroups from './runner_groups.vue';
import RunnerProjects from './runner_projects.vue'; import RunnerProjects from './runner_projects.vue';
import RunnerJobs from './runner_jobs.vue';
import RunnerTags from './runner_tags.vue'; import RunnerTags from './runner_tags.vue';
export default { export default {
components: { components: {
GlBadge,
GlTabs, GlTabs,
GlTab, GlTab,
GlIntersperse, GlIntersperse,
RunnerDetail, RunnerDetail,
RunnerGroups, RunnerGroups,
RunnerProjects, RunnerProjects,
RunnerJobs,
RunnerTags, RunnerTags,
TimeAgo, TimeAgo,
}, },
@ -53,6 +57,9 @@ export default {
isProjectRunner() { isProjectRunner() {
return this.runner?.runnerType === PROJECT_TYPE; return this.runner?.runnerType === PROJECT_TYPE;
}, },
jobCount() {
return formatJobCount(this.runner?.jobCount);
},
}, },
ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_REF_PROTECTED,
}; };
@ -65,7 +72,7 @@ export default {
<template v-if="runner"> <template v-if="runner">
<div class="gl-pt-4"> <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|Description')" :value="runner.description" />
<runner-detail <runner-detail
:label="s__('Runners|Last contact')" :label="s__('Runners|Last contact')"
@ -103,5 +110,15 @@ export default {
<runner-projects v-if="isProjectRunner" :runner="runner" /> <runner-projects v-if="isProjectRunner" :runner="runner" />
</template> </template>
</gl-tab> </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> </gl-tabs>
</template> </template>

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

View file

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

View file

@ -4,6 +4,7 @@ export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000; export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const RUNNER_DETAILS_PROJECTS_PAGE_SIZE = 5; 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_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}'); 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_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})');
export const I18N_NONE = __('None'); export const I18N_NONE = __('None');
export const I18N_NO_JOBS_FOUND = s__('Runner|This runner has not run any jobs.');
// Styles // Styles

View file

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

View file

@ -8,6 +8,7 @@ fragment RunnerDetailsShared on CiRunner {
ipAddress ipAddress
description description
maximumTimeout maximumTimeout
jobCount
tagList tagList
createdAt createdAt
status(legacyMode: null) status(legacyMode: null)

View file

@ -1,37 +1,36 @@
# Make sure that this file has the keys sorted
--- ---
dast_site_profiles_pipelines: ci_build_report_results:
- 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:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete 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 - table: projects
column: project_id column: project_id
on_delete: async_delete on_delete: async_delete
@ -45,20 +44,13 @@ ci_job_token_project_scope_links:
- table: projects - table: projects
column: target_project_id column: target_project_id
on_delete: async_delete on_delete: async_delete
ci_daily_build_group_report_results: ci_minutes_additional_packs:
- table: namespaces - table: namespaces
column: group_id column: namespace_id
on_delete: async_delete on_delete: async_delete
- table: projects ci_namespace_mirrors:
column: project_id - table: namespaces
on_delete: async_delete column: namespace_id
external_pull_requests:
- table: projects
column: project_id
on_delete: async_delete
ci_freeze_periods:
- table: projects
column: project_id
on_delete: async_delete on_delete: async_delete
ci_pending_builds: ci_pending_builds:
- table: namespaces - table: namespaces
@ -67,37 +59,17 @@ ci_pending_builds:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete on_delete: async_delete
ci_resource_groups: ci_pipeline_artifacts:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete on_delete: async_delete
ci_runner_namespaces: ci_pipeline_chat_data:
- table: namespaces - table: chat_names
column: namespace_id column: chat_name_id
on_delete: async_delete on_delete: async_delete
ci_running_builds: ci_pipeline_schedules:
- 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:
- table: users - table: users
column: user_id column: owner_id
on_delete: async_nullify on_delete: async_nullify
- table: projects - table: projects
column: project_id column: project_id
@ -122,97 +94,31 @@ ci_project_mirrors:
- table: namespaces - table: namespaces
column: namespace_id column: namespace_id
on_delete: async_delete 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: ci_project_monthly_usages:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete 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: ci_refs:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete on_delete: async_delete
ci_group_variables: ci_resource_groups:
- 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:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete 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 - table: projects
column: project_id column: project_id
on_delete: async_delete on_delete: async_delete
@ -223,10 +129,21 @@ ci_sources_pipelines:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete on_delete: async_delete
ci_sources_projects:
- table: projects
column: source_project_id
on_delete: async_delete
ci_stages: ci_stages:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete 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: ci_triggers:
- table: users - table: users
column: owner_id column: owner_id
@ -234,3 +151,87 @@ ci_triggers:
- table: projects - table: projects
column: project_id column: project_id
on_delete: async_delete 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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
535f476a358dcb3f3472f1e0ec1afef738f995197b5d1f4fcd61e58a9c9e8e75

View file

@ -0,0 +1 @@
77cc8fc86f2c6a5ed017dde40dd4db796821a35e6ce4d8dcbe24b2cdaccbb5d9

View file

@ -0,0 +1 @@
e48473172d7561fb7474e16e291e555843c0ec4543300b007f86cd4a5923db85

View file

@ -13414,6 +13414,7 @@ CREATE TABLE dast_site_profiles (
auth_password_field text, auth_password_field text,
auth_username text, auth_username text,
target_type smallint DEFAULT 0 NOT NULL, 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_5203110fee CHECK ((char_length(auth_username_field) <= 255)),
CONSTRAINT check_6cfab17b48 CHECK ((char_length(name) <= 255)), CONSTRAINT check_6cfab17b48 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_c329dffdba CHECK ((char_length(auth_password_field) <= 255)), CONSTRAINT check_c329dffdba CHECK ((char_length(auth_password_field) <= 255)),

View file

@ -17,6 +17,7 @@ swap:
e-mail: '"email"' e-mail: '"email"'
GFM: '"GitLab Flavored Markdown"' GFM: '"GitLab Flavored Markdown"'
it is recommended: '"we recommend"' it is recommended: '"we recommend"'
navigate: go
OAuth2: '"OAuth 2.0"' OAuth2: '"OAuth 2.0"'
once that: '"after that"' once that: '"after that"'
once the: '"after the"' once the: '"after the"'

View file

@ -561,6 +561,8 @@ Do not use **navigate**. Use **go** instead. For example:
- Go to this webpage. - Go to this webpage.
- Open a terminal and go to the `runner` directory. - 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 ## 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**. Try to avoid **needs to**, because it's wordy. Avoid **should** when you can be more specific. If something is required, use **must**.

View file

@ -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 CI config changes
- for merge requests with frontend 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/controllers}/**/*`
- for merge requests with changes to `{,ee/,jh/}{app/models}/**/*`
- for merge requests with QA changes - for merge requests with QA changes
- for scheduled pipelines - for scheduled pipelines
- the MR has the `pipeline:run-review-app` label set - the MR has the `pipeline:run-review-app` label set

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -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` 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. 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 ## Drill into the information

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -102,20 +102,36 @@ user profiles are only visible to signed-in users.
## Add details to your profile with a README ## 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. > [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. 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). 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 GitLab displays the contents of your README below your contribution graph.
your username.
### 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 ## Add external accounts to your user profile page

View file

@ -18,6 +18,8 @@ module Gitlab
end end
end end
LOG_MAX_DURATION_THRESHOLD = 2.seconds
def initialize(project:, current_user:, sha: nil) def initialize(project:, current_user:, sha: nil)
@project = project @project = project
@current_user = current_user @current_user = current_user
@ -49,12 +51,9 @@ module Gitlab
end end
def static_validation(content) def static_validation(content)
result = Gitlab::Ci::YamlProcessor.new( logger = build_logger
content,
project: @project, result = yaml_processor_result(content, logger)
user: @current_user,
sha: @sha
).execute
Result.new( Result.new(
jobs: static_validation_convert_to_jobs(result), jobs: static_validation_convert_to_jobs(result),
@ -62,6 +61,17 @@ module Gitlab
errors: result.errors, errors: result.errors,
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord 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 end
def dry_run_convert_to_jobs(stages) def dry_run_convert_to_jobs(stages)
@ -109,6 +119,17 @@ module Gitlab
jobs jobs
end 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 end
end end

View file

@ -59,7 +59,7 @@ module Gitlab
attributes = { attributes = {
class: self.class.name.to_s, class: self.class.name.to_s,
pipeline_creation_caller: caller, 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_persisted: pipeline.persisted?,
pipeline_source: pipeline.source, pipeline_source: pipeline.source,
pipeline_creation_service_duration_s: age pipeline_creation_service_duration_s: age

View file

@ -109,6 +109,26 @@ module Gitlab
name.to_s == CI_DATABASE_NAME name.to_s == CI_DATABASE_NAME
end 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 def self.check_postgres_version_and_print_warning
return if Gitlab::Runtime.rails_runner? return if Gitlab::Runtime.rails_runner?

View file

@ -270,6 +270,19 @@ namespace :gitlab do
end end
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, # Only for development environments,
# we execute pending data migrations inline for convenience. # we execute pending data migrations inline for convenience.
Rake::Task['db:migrate'].enhance do Rake::Task['db:migrate'].enhance do

View file

@ -20374,6 +20374,9 @@ msgstr ""
msgid "It looks like you have some draft commits in this branch." msgid "It looks like you have some draft commits in this branch."
msgstr "" 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." msgid "It may be several days before you see feature usage data."
msgstr "" msgstr ""
@ -20878,6 +20881,9 @@ msgstr ""
msgid "Job|Erase job log and artifacts" msgid "Job|Erase job log and artifacts"
msgstr "" msgstr ""
msgid "Job|Finished at"
msgstr ""
msgid "Job|Job artifacts" msgid "Job|Job artifacts"
msgstr "" msgstr ""
@ -20902,6 +20908,9 @@ msgstr ""
msgid "Job|Show complete raw" msgid "Job|Show complete raw"
msgstr "" msgstr ""
msgid "Job|Status"
msgstr ""
msgid "Job|The artifacts were removed" msgid "Job|The artifacts were removed"
msgstr "" msgstr ""
@ -27138,9 +27147,6 @@ msgstr ""
msgid "Please enter a valid time interval" msgid "Please enter a valid time interval"
msgstr "" msgstr ""
msgid "Please enter or upload a valid license."
msgstr ""
msgid "Please enter your current password." msgid "Please enter your current password."
msgstr "" msgstr ""
@ -31321,6 +31327,9 @@ msgstr ""
msgid "Runners|Instance" msgid "Runners|Instance"
msgstr "" msgstr ""
msgid "Runners|Jobs"
msgstr ""
msgid "Runners|Last contact" msgid "Runners|Last contact"
msgstr "" msgstr ""
@ -31561,6 +31570,9 @@ msgstr ""
msgid "Runners|stale" msgid "Runners|stale"
msgstr "" msgstr ""
msgid "Runner|This runner has not run any jobs."
msgstr ""
msgid "Running" msgid "Running"
msgstr "" msgstr ""
@ -36336,6 +36348,9 @@ msgstr ""
msgid "The latest pipeline for this merge request has failed." msgid "The latest pipeline for this merge request has failed."
msgstr "" 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." msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
msgstr "" 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." msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
msgstr "" 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" msgid "The list creation wizard is already open"
msgstr "" msgstr ""

View file

@ -213,8 +213,28 @@ describe('~/environments/components/deployment.vue', () => {
expect(job.attributes('href')).toBe(deployment.deployable.buildPath); expect(job.attributes('href')).toBe(deployment.deployable.buildPath);
const apiBadge = wrapper.findByText(__('API')); const apiBadge = wrapper.findByText(__('API'));
expect(apiBadge.exists()).toBe(false); 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', () => { describe('with API deployment', () => {
beforeEach(async () => { beforeEach(async () => {
wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } }); wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } });
@ -237,7 +257,7 @@ describe('~/environments/components/deployment.vue', () => {
}); });
it('shows a span instead of a link', () => { 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(); expect(job.attributes('href')).toBeUndefined();
}); });
}); });

View file

@ -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) { 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(: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(: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/' query_path = 'runner/graphql/'
fixtures_path = 'graphql/runner/' fixtures_path = 'graphql/runner/'
@ -104,6 +105,22 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
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 end
describe do describe do

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

View file

@ -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 { createWrapper, ErrorWrapper } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; 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 RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerDetail from '~/runner/components/runner_detail.vue'; import RunnerDetail from '~/runner/components/runner_detail.vue';
import RunnerGroups from '~/runner/components/runner_groups.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 RunnerTags from '~/runner/components/runner_tags.vue';
import RunnerTag from '~/runner/components/runner_tag.vue'; import RunnerTag from '~/runner/components/runner_tag.vue';
@ -38,6 +39,8 @@ describe('RunnerDetails', () => {
}; };
const findDetailGroups = () => wrapper.findComponent(RunnerGroups); const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => { const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
wrapper = mountFn(RunnerDetails, { 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);
});
});
}); });

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

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

View file

@ -123,6 +123,7 @@ describe('RunnerUpdateForm', () => {
// Some read-only fields are not submitted // Some read-only fields are not submitted
const { const {
__typename,
ipAddress, ipAddress,
runnerType, runnerType,
createdAt, createdAt,
@ -132,7 +133,7 @@ describe('RunnerUpdateForm', () => {
userPermissions, userPermissions,
version, version,
groups, groups,
__typename, jobCount,
...submitted ...submitted
} = mockRunner; } = mockRunner;

View file

@ -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 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 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 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 // Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json'; import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
@ -20,6 +21,7 @@ export {
runnerData, runnerData,
runnerWithGroupData, runnerWithGroupData,
runnerProjectsData, runnerProjectsData,
runnerJobsData,
groupRunnersData, groupRunnersData,
groupRunnersCountData, groupRunnersCountData,
groupRunnersDataPaginated, groupRunnersDataPaginated,

View file

@ -322,4 +322,102 @@ RSpec.describe Gitlab::Ci::Lint do
end end
end 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 end

View file

@ -203,6 +203,35 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
expect(commit).to be_truthy expect(commit).to be_truthy
end end
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 end
context 'when the feature flag is disabled' do context 'when the feature flag is disabled' do

View file

@ -18,6 +18,15 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
)) ))
end 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 context 'ensure no duplicates are found' do
it 'does not have duplicate tables defined' do it 'does not have duplicate tables defined' do
# since we use hash to detect duplicate hash keys we need to parse YAML document # since we use hash to detect duplicate hash keys we need to parse YAML document

View file

@ -104,6 +104,34 @@ RSpec.describe Gitlab::Database do
end end
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 describe '.check_postgres_version_and_print_warning' do
let(:reflect) { instance_spy(Gitlab::Database::Reflection) } let(:reflect) { instance_spy(Gitlab::Database::Reflection) }

View file

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

View file

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

View file

@ -446,6 +446,44 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
end end
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 = '') def run_rake_task(task_name, arguments = '')
Rake::Task[task_name].reenable Rake::Task[task_name].reenable
Rake.application.invoke_task("#{task_name}#{arguments}") Rake.application.invoke_task("#{task_name}#{arguments}")