Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-15 12:14:49 +00:00
parent e7e44c0e4c
commit 524e972622
98 changed files with 1458 additions and 391 deletions

View File

@ -1,10 +1,51 @@
cloud-native-image-env:
extends:
- .default-retry
- .cng:rules
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13
stage: post-test
before_script:
- source ./scripts/utils.sh
- install_gitlab_gem
script:
- 'ruby -r./scripts/trigger-build.rb -e "puts Trigger.variables_for_env_file(Trigger::CNG.new.variables)" > build.env'
- cat build.env
artifacts:
reports:
dotenv: build.env
paths:
- build.env
expire_in: 7 days
when: always
cloud-native-image:
extends: .cng:rules
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine
dependencies: []
stage: post-test
needs: ["cloud-native-image-env"]
inherit:
variables: false
variables:
GIT_DEPTH: "1"
script:
- install_gitlab_gem
- ./scripts/trigger-build cng
TOP_UPSTREAM_SOURCE_PROJECT: "${TOP_UPSTREAM_SOURCE_PROJECT}"
TOP_UPSTREAM_SOURCE_REF: "${TOP_UPSTREAM_SOURCE_REF}"
TOP_UPSTREAM_SOURCE_JOB: "${TOP_UPSTREAM_SOURCE_JOB}"
TOP_UPSTREAM_SOURCE_SHA: "${TOP_UPSTREAM_SOURCE_SHA}"
TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID: "${TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID}"
TOP_UPSTREAM_MERGE_REQUEST_IID: "${TOP_UPSTREAM_MERGE_REQUEST_IID}"
GITLAB_REF_SLUG: "${GITLAB_REF_SLUG}"
# CNG pipeline specific variables
GITLAB_VERSION: "${GITLAB_VERSION}"
GITLAB_TAG: "${GITLAB_TAG}"
GITLAB_ASSETS_TAG: "${GITLAB_ASSETS_TAG}"
FORCE_RAILS_IMAGE_BUILDS: "${FORCE_RAILS_IMAGE_BUILDS}"
CE_PIPELINE: "${CE_PIPELINE}" # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$CE_PIPELINE'` will evaluate to `false` when this variable is empty
EE_PIPELINE: "${EE_PIPELINE}" # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$EE_PIPELINE'` will evaluate to `false` when this variable is empty
GITLAB_SHELL_VERSION: "${GITLAB_SHELL_VERSION}"
GITLAB_ELASTICSEARCH_INDEXER_VERSION: "${GITLAB_ELASTICSEARCH_INDEXER_VERSION}"
GITLAB_KAS_VERSION: "${GITLAB_KAS_VERSION}"
GITLAB_WORKHORSE_VERSION: "${GITLAB_WORKHORSE_VERSION}"
GITLAB_PAGES_VERSION: "${GITLAB_PAGES_VERSION}"
GITALY_SERVER_VERSION: "${GITALY_SERVER_VERSION}"
trigger:
project: gitlab-org/build/CNG
branch: $TRIGGER_BRANCH
strategy: depend

View File

@ -28,7 +28,7 @@
review-docs-deploy:
extends: .review-docs
script:
- ./scripts/trigger-build docs deploy
- ./scripts/trigger-build.rb docs deploy
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
@ -37,7 +37,7 @@ review-docs-cleanup:
name: review-docs/mr-${CI_MERGE_REQUEST_IID}
action: stop
script:
- ./scripts/trigger-build docs cleanup
- ./scripts/trigger-build.rb docs cleanup
docs-lint markdown:
extends:

View File

@ -73,7 +73,7 @@ update-qa-cache:
- echo $exit_code
- |
if [ $exit_code -eq 0 ]; then
./scripts/trigger-build omnibus
./scripts/trigger-build.rb omnibus
elif [ $exit_code -eq 1 ]; then
exit 1
else
@ -108,7 +108,7 @@ update-qa-cache:
if [[ $feature_flags ]]; then
export GITLAB_QA_OPTIONS="--set-feature-flags $feature_flags"
echo $GITLAB_QA_OPTIONS
./scripts/trigger-build omnibus
./scripts/trigger-build.rb omnibus
else
echo "No changed feature flag found to test. The tests are skipped if the flag was removed."
fi

View File

@ -438,7 +438,7 @@ db:gitlabcom-database-testing:
script:
- source scripts/utils.sh
- install_gitlab_gem
- ./scripts/trigger-build gitlab-com-database-testing
- ./scripts/trigger-build.rb gitlab-com-database-testing
gitlab:setup:
extends: .db-job-base

View File

@ -16,20 +16,58 @@ include:
- source ./scripts/review_apps/review-apps.sh
- install_api_client_dependencies_with_apk
review-build-cng:
review-build-cng-env:
extends:
- .default-retry
- .review:rules:review-build-cng
image: ${GITLAB_DEPENDENCY_PROXY}ruby:2.7-alpine3.13
stage: prepare
variables:
CNG_PROJECT_ACCESS_TOKEN: "${CNG_MIRROR_PROJECT_ACCESS_TOKEN}" # "Multi-pipeline (from 'gitlab-org/gitlab' 'review-build-cng' job)" at https://gitlab.com/gitlab-org/build/CNG-mirror/-/settings/access_tokens
CNG_PROJECT_PATH: "gitlab-org/build/CNG-mirror"
needs: []
before_script:
- source ./scripts/utils.sh
- install_gitlab_gem
script:
- ./scripts/trigger-build cng
- 'ruby -r./scripts/trigger-build.rb -e "puts Trigger.variables_for_env_file(Trigger::CNG.new.variables)" > build.env'
- cat build.env
artifacts:
reports:
dotenv: build.env
paths:
- build.env
expire_in: 7 days
when: always
review-build-cng:
extends: .review:rules:review-build-cng
stage: prepare
needs: ["review-build-cng-env"]
inherit:
variables: false
variables:
TOP_UPSTREAM_SOURCE_PROJECT: "${TOP_UPSTREAM_SOURCE_PROJECT}"
TOP_UPSTREAM_SOURCE_REF: "${TOP_UPSTREAM_SOURCE_REF}"
TOP_UPSTREAM_SOURCE_JOB: "${TOP_UPSTREAM_SOURCE_JOB}"
TOP_UPSTREAM_SOURCE_SHA: "${TOP_UPSTREAM_SOURCE_SHA}"
TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID: "${TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID}"
TOP_UPSTREAM_MERGE_REQUEST_IID: "${TOP_UPSTREAM_MERGE_REQUEST_IID}"
GITLAB_REF_SLUG: "${GITLAB_REF_SLUG}"
# CNG pipeline specific variables
GITLAB_VERSION: "${GITLAB_VERSION}"
GITLAB_TAG: "${GITLAB_TAG}"
GITLAB_ASSETS_TAG: "${GITLAB_ASSETS_TAG}"
FORCE_RAILS_IMAGE_BUILDS: "${FORCE_RAILS_IMAGE_BUILDS}"
CE_PIPELINE: "${CE_PIPELINE}" # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$CE_PIPELINE'` will evaluate to `false` when this variable is empty
EE_PIPELINE: "${EE_PIPELINE}" # Based on https://docs.gitlab.com/ee/ci/jobs/job_control.html#check-if-a-variable-exists, `if: '$EE_PIPELINE'` will evaluate to `false` when this variable is empty
GITLAB_SHELL_VERSION: "${GITLAB_SHELL_VERSION}"
GITLAB_ELASTICSEARCH_INDEXER_VERSION: "${GITLAB_ELASTICSEARCH_INDEXER_VERSION}"
GITLAB_KAS_VERSION: "${GITLAB_KAS_VERSION}"
GITLAB_WORKHORSE_VERSION: "${GITLAB_WORKHORSE_VERSION}"
GITLAB_PAGES_VERSION: "${GITLAB_PAGES_VERSION}"
GITALY_SERVER_VERSION: "${GITALY_SERVER_VERSION}"
trigger:
project: gitlab-org/build/CNG-mirror
branch: $TRIGGER_BRANCH
strategy: depend
.review-workflow-base:
extends:

View File

@ -141,7 +141,7 @@
- ".gitlab/ci/review-apps/**/*"
- "scripts/review_apps/base-config.yaml"
- "scripts/review_apps/review-apps.sh"
- "scripts/trigger-build"
- "scripts/trigger-build.rb"
- "{,ee/,jh/}{bin,config}/**/*.rb"
.ci-qa-patterns: &ci-qa-patterns

View File

@ -24,7 +24,6 @@ Database/MultipleDatabases:
- lib/gitlab/import_export/group/relation_tree_restorer.rb
- lib/gitlab/legacy_github_import/importer.rb
- lib/gitlab/seeder.rb
- lib/system_check/orphans/repository_check.rb
- spec/db/schema_spec.rb
- spec/initializers/database_config_spec.rb
- spec/lib/backup/manager_spec.rb

View File

@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 14.7.3 (2022-02-15)
### Fixed (2 changes)
- [Update GitHub PRs Importer to force update repository](gitlab-org/gitlab@33f12736b070362cb89e9bbb4b3aa7d86fc373c3) ([merge request](gitlab-org/gitlab!80595))
- [Fix Geo checksummable check failing when file is nil](gitlab-org/gitlab@f49e3ea3e4d4ca7a64607687f9aaa974801b6bf9) ([merge request](gitlab-org/gitlab!80595)) **GitLab Enterprise Edition**
### Changed (1 change)
- [Properly exclude pending_destruction packages when creating one](gitlab-org/gitlab@9fb9f1ca8a2342225b7017c211f85175a4ef56dd) ([merge request](gitlab-org/gitlab!80595))
## 14.7.2 (2022-02-08)
### Added (1 change)

View File

@ -1 +1 @@
d3ab199f7923a9d75516b8d1f1ea2f84b03190b1
a67a6fdd96ba690d57c919f9a042dceebab2832e

View File

@ -44,6 +44,9 @@ export const typePolicies = {
PipelinePermissions: {
merge: true,
},
DesignCollection: {
merge: true,
},
};
export const stripWhitespaceFromQuery = (url, path) => {

View File

@ -2,31 +2,14 @@
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatNumber, __, s__ } from '~/locale';
import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import { formatJobCount, tableField } from '../utils';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue';
const tableField = ({ key, label = '', thClasses = [] }) => {
return {
key,
label,
thClass: [
'gl-bg-transparent!',
'gl-border-b-solid!',
'gl-border-b-gray-100!',
'gl-border-b-1!',
...thClasses,
],
tdAttr: {
'data-testid': `td-${key}`,
},
};
};
export default {
components: {
GlTable,
@ -54,10 +37,7 @@ export default {
},
methods: {
formatJobCount(jobCount) {
if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
}
return formatNumber(jobCount);
return formatJobCount(jobCount);
},
runnerTrAttr(runner) {
if (runner) {

View File

@ -9,6 +9,7 @@ import {
I18N_FETCH_ERROR,
RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
} from '../constants';
import { getPaginationVariables } from '../utils';
import { captureException } from '../sentry_utils';
import RunnerAssignedItem from './runner_assigned_item.vue';
import RunnerPagination from './runner_pagination.vue';
@ -62,19 +63,9 @@ export default {
computed: {
variables() {
const { id } = this.runner;
const { before, after } = this.pagination;
if (before) {
return {
id,
before,
last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
};
}
return {
id,
after,
first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
...getPaginationVariables(this.pagination, RUNNER_DETAILS_PROJECTS_PAGE_SIZE),
};
},
loading() {

View File

@ -18,6 +18,7 @@ import {
RUNNER_PAGE_SIZE,
STATUS_NEVER_CONTACTED,
} from './constants';
import { getPaginationVariables } from './utils';
/**
* The filters and sorting of the runners are built around
@ -184,30 +185,27 @@ export const fromSearchToVariables = ({
sort = null,
pagination = {},
} = {}) => {
const variables = {};
const filterVariables = {};
const queryObj = filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
});
[variables.status] = queryObj[PARAM_KEY_STATUS] || [];
variables.search = queryObj[PARAM_KEY_SEARCH];
variables.tagList = queryObj[PARAM_KEY_TAG];
[filterVariables.status] = queryObj[PARAM_KEY_STATUS] || [];
filterVariables.search = queryObj[PARAM_KEY_SEARCH];
filterVariables.tagList = queryObj[PARAM_KEY_TAG];
if (runnerType) {
variables.type = runnerType;
filterVariables.type = runnerType;
}
if (sort) {
variables.sort = sort;
filterVariables.sort = sort;
}
if (pagination.before) {
variables.before = pagination.before;
variables.last = RUNNER_PAGE_SIZE;
} else {
variables.after = pagination.after;
variables.first = RUNNER_PAGE_SIZE;
}
const paginationVariables = getPaginationVariables(pagination, RUNNER_PAGE_SIZE);
return variables;
return {
...filterVariables,
...paginationVariables,
};
};

View File

@ -0,0 +1,72 @@
import { formatNumber } from '~/locale';
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
import { RUNNER_JOB_COUNT_LIMIT } from './constants';
/**
* Formats a job count, limited to a max number
*
* @param {Number} jobCount
* @returns Formatted string
*/
export const formatJobCount = (jobCount) => {
if (typeof jobCount !== 'number') {
return '';
}
if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
}
return formatNumber(jobCount);
};
/**
* Returns a GlTable fields with a given key and label
*
* @param {Object} options
* @returns Field object to add to GlTable fields
*/
export const tableField = ({ key, label = '', thClasses = [] }) => {
return {
key,
label,
thClass: [DEFAULT_TH_CLASSES, ...thClasses],
tdAttr: {
'data-testid': `td-${key}`,
},
};
};
/**
* Returns variables for a GraphQL query that uses keyset
* pagination.
*
* https://docs.gitlab.com/ee/development/graphql_guide/pagination.html#keyset-pagination
*
* @param {Object} pagination - Contains before, after, page
* @param {Number} pageSize
* @returns Variables
*/
export const getPaginationVariables = (pagination, pageSize = 10) => {
const { before, after } = pagination;
// first + after: Next page
// Get the first N items after item X
if (after) {
return {
after,
first: pageSize,
};
}
// last + before: Prev page
// Get the first N items before item X, when you click on Prev
if (before) {
return {
before,
last: pageSize,
};
}
// first page
// Get the first N items
return { first: pageSize };
};

View File

@ -10,6 +10,7 @@ import {
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
@ -221,6 +222,12 @@ export default {
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
return this.issuableAttribute === IssuableType.Epic;
},
formatIssuableAttribute() {
return {
kebab: kebabCase(this.issuableAttribute),
snake: snakeCase(this.issuableAttribute),
};
},
},
methods: {
updateAttribute(attributeId) {
@ -300,26 +307,28 @@ export default {
<sidebar-editable-item
ref="editable"
:title="attributeTypeTitle"
:data-testid="`${issuableAttribute}-edit`"
:data-testid="`${formatIssuableAttribute.kebab}-edit`"
:tracking="tracking"
:loading="updating || loading"
@open="handleOpen"
@close="handleClose"
>
<template #collapsed>
<slot name="value-collapsed" :current-attribute="currentAttribute">
<div
v-if="isClassicSidebar"
v-gl-tooltip.left.viewport
:title="attributeTypeTitle"
class="sidebar-collapsed-icon"
>
<gl-icon :aria-label="attributeTypeTitle" :name="attributeTypeIcon" />
<span class="collapse-truncated-title">
{{ attributeTitle }}
</span>
</div>
</slot>
<div
v-if="isClassicSidebar"
v-gl-tooltip.left.viewport
:title="attributeTypeTitle"
class="sidebar-collapsed-icon"
>
<gl-icon :size="16" :aria-label="attributeTypeTitle" :name="attributeTypeIcon" />
<span class="collapse-truncated-title">
{{ attributeTitle }}
</span>
</div>
<div
:data-testid="`select-${issuableAttribute}`"
:data-testid="`select-${formatIssuableAttribute.kebab}`"
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
>
<span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span>
@ -337,7 +346,7 @@ export default {
v-gl-tooltip="tooltipText"
class="gl-text-gray-900! gl-font-weight-bold"
:href="attributeUrl"
:data-qa-selector="`${issuableAttribute}_link`"
:data-qa-selector="`${formatIssuableAttribute.snake}_link`"
>
{{ attributeTitle }}
<span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span>
@ -359,7 +368,7 @@ export default {
>
<gl-search-box-by-type ref="search" v-model="searchTerm" />
<gl-dropdown-item
:data-testid="`no-${issuableAttribute}-item`"
:data-testid="`no-${formatIssuableAttribute.kebab}-item`"
:is-check-item="true"
:is-checked="isAttributeChecked($options.noAttributeId)"
@click="updateAttribute($options.noAttributeId)"
@ -389,7 +398,7 @@ export default {
:key="attrItem.id"
:is-check-item="true"
:is-checked="isAttributeChecked(attrItem.id)"
:data-testid="`${issuableAttribute}-items`"
:data-testid="`${formatIssuableAttribute.kebab}-items`"
@click="updateAttribute(attrItem.id)"
>
{{ attrItem.title }}

View File

@ -38,7 +38,10 @@ export default {
</script>
<template>
<div data-testid="helpPane" class="time-tracking-help-state">
<div
data-testid="helpPane"
class="sidebar-help-state gl-bg-white gl-border-gray-100 gl-border-t-solid gl-border-b-solid gl-border-1"
>
<div class="time-tracking-info">
<h4>{{ __('Track time with quick actions') }}</h4>
<p>{{ __('Quick actions can be used in description and comment boxes.') }}</p>

View File

@ -1,5 +1,5 @@
<script>
import { GlIcon, GlLink, GlModal, GlModalDirective, GlLoadingIcon } from '@gitlab/ui';
import { GlIcon, GlLink, GlModal, GlButton, GlModalDirective, GlLoadingIcon } from '@gitlab/ui';
import { IssuableType } from '~/issues/constants';
import { s__, __ } from '~/locale';
import { timeTrackingQueries } from '~/sidebar/constants';
@ -21,6 +21,7 @@ export default {
GlIcon,
GlLink,
GlModal,
GlButton,
GlLoadingIcon,
TimeTrackingCollapsedState,
TimeTrackingSpentOnlyPane,
@ -187,7 +188,11 @@ export default {
</script>
<template>
<div v-cloak class="time-tracker time-tracking-component-wrap" data-testid="time-tracker">
<div
v-cloak
class="time-tracker time-tracking-component-wrap sidebar-help-wrap"
data-testid="time-tracker"
>
<time-tracking-collapsed-state
v-if="showCollapsed"
:show-comparison-state="showComparisonState"
@ -198,25 +203,21 @@ export default {
:time-spent-human-readable="humanTotalTimeSpent"
:time-estimate-human-readable="humanTimeEstimate"
/>
<div class="hide-collapsed gl-line-height-20 gl-text-gray-900">
<div
class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center"
>
{{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" inline />
<div
v-if="!showHelpState"
data-testid="helpButton"
class="help-button float-right"
@click="toggleHelpState(true)"
<gl-button
:data-testid="showHelpState ? 'closeHelpButton' : 'helpButton'"
category="tertiary"
size="small"
variant="link"
class="gl-ml-auto"
@click="toggleHelpState(!showHelpState)"
>
<gl-icon name="question-o" />
</div>
<div
v-else
data-testid="closeHelpButton"
class="close-help-button float-right"
@click="toggleHelpState(false)"
>
<gl-icon name="close" />
</div>
<gl-icon :name="showHelpState ? 'close' : 'question-o'" class="gl-text-gray-900!" />
</gl-button>
</div>
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
<div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane">

View File

@ -0,0 +1,65 @@
import { kebabCase } from 'lodash';
import Vue from 'vue';
import { GlToggle } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
export const initToggle = (el) => {
if (!el) {
return false;
}
const {
name,
isChecked,
disabled,
isLoading,
label,
help,
labelPosition,
...dataset
} = el.dataset;
return new Vue({
el,
props: {
disabled: {
type: Boolean,
required: false,
default: parseBoolean(disabled),
},
isLoading: {
type: Boolean,
required: false,
default: parseBoolean(isLoading),
},
},
data() {
return {
value: parseBoolean(isChecked),
};
},
render(h) {
return h(GlToggle, {
props: {
name,
value: this.value,
disabled: this.disabled,
isLoading: this.isLoading,
label,
help,
labelPosition,
},
class: el.className,
attrs: Object.fromEntries(
Object.entries(dataset).map(([key, value]) => [`data-${kebabCase(key)}`, value]),
),
on: {
change: (newValue) => {
this.value = newValue;
this.$emit('change', newValue);
},
},
});
},
});
};

View File

@ -742,6 +742,26 @@
}
}
.sidebar-help-wrap {
.sidebar-help-state {
margin: 16px -20px -20px;
padding: 16px 20px;
}
.help-state-toggle-enter-active {
transition: all 0.8s ease;
}
.help-state-toggle-leave-active {
transition: all 0.5s ease;
}
.help-state-toggle-enter,
.help-state-toggle-leave-active {
opacity: 0;
}
}
.time-tracker {
.sidebar-collapsed-icon {
> .stopwatch-svg {
@ -759,11 +779,6 @@
}
}
.help-button,
.close-help-button {
cursor: pointer;
}
.compare-meter {
&.over_estimate {
.time-remaining,
@ -776,31 +791,6 @@
.compare-display-container {
font-size: 13px;
}
.time-tracking-help-state {
background: $white;
margin: 16px -20px -20px;
padding: 16px 20px;
border-top: 1px solid $border-gray-light;
border-bottom: 1px solid $border-gray-light;
a:hover {
color: $btn-white-active;
}
}
.help-state-toggle-enter-active {
transition: all 0.8s ease;
}
.help-state-toggle-leave-active {
transition: all 0.5s ease;
}
.help-state-toggle-enter,
.help-state-toggle-leave-active {
opacity: 0;
}
}
.issuable-todo-btn {

View File

@ -4,10 +4,10 @@ module Mutations
module AlertManagement
module HttpIntegration
class Create < HttpIntegrationBase
include FindsProject
graphql_name 'HttpIntegrationCreate'
include FindsProject
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project to create the integration in.'

View File

@ -4,10 +4,10 @@ module Mutations
module AlertManagement
module PrometheusIntegration
class Create < PrometheusIntegrationBase
include FindsProject
graphql_name 'PrometheusIntegrationCreate'
include FindsProject
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project to create the integration in.'

View File

@ -3,10 +3,9 @@
module Mutations
module Boards
class Create < ::Mutations::BaseMutation
include Mutations::ResolvesResourceParent
graphql_name 'CreateBoard'
include Mutations::ResolvesResourceParent
include Mutations::Boards::CommonMutationArguments
field :board,

View File

@ -3,10 +3,10 @@
module Mutations
module Branches
class Create < BaseMutation
include FindsProject
graphql_name 'CreateBranch'
include FindsProject
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the branch is associated with.'

View File

@ -3,10 +3,10 @@
module Mutations
module Ci
class CiCdSettingsUpdate < BaseMutation
include FindsProject
graphql_name 'CiCdSettingsUpdate'
include FindsProject
authorize :admin_project
argument :full_path, GraphQL::Types::ID,

View File

@ -4,10 +4,10 @@ module Mutations
module Ci
module JobTokenScope
class AddProject < BaseMutation
include FindsProject
graphql_name 'CiJobTokenScopeAddProject'
include FindsProject
authorize :admin_project
argument :project_path, GraphQL::Types::ID,

View File

@ -4,10 +4,10 @@ module Mutations
module Ci
module JobTokenScope
class RemoveProject < BaseMutation
include FindsProject
graphql_name 'CiJobTokenScopeRemoveProject'
include FindsProject
authorize :admin_project
argument :project_path, GraphQL::Types::ID,

View File

@ -4,12 +4,12 @@ module Mutations
module Clusters
module Agents
class Create < BaseMutation
graphql_name 'CreateClusterAgent'
include FindsProject
authorize :create_cluster
graphql_name 'CreateClusterAgent'
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the associated project for this cluster agent.'

View File

@ -3,6 +3,8 @@
module Mutations
module Commits
class Create < BaseMutation
graphql_name 'CommitCreate'
include FindsProject
class UrlHelpers
@ -10,8 +12,6 @@ module Mutations
include Gitlab::Routing
end
graphql_name 'CommitCreate'
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the branch is associated with.'

View File

@ -3,10 +3,10 @@
module Mutations
module ContainerExpirationPolicies
class Update < Mutations::BaseMutation
include FindsProject
graphql_name 'UpdateContainerExpirationPolicy'
include FindsProject
authorize :destroy_container_image
argument :project_path,

View File

@ -3,12 +3,11 @@
module Mutations
module ContainerRepositories
class DestroyTags < ::Mutations::ContainerRepositories::DestroyBase
LIMIT = 20
TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
graphql_name 'DestroyContainerRepositoryTags'
LIMIT = 20
TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
authorize :destroy_container_image
argument :id,

View File

@ -3,10 +3,10 @@
module Mutations
module CustomEmoji
class Create < BaseMutation
include Mutations::ResolvesGroup
graphql_name 'CreateCustomEmoji'
include Mutations::ResolvesGroup
authorize :create_custom_emoji
field :custom_emoji,

View File

@ -4,11 +4,11 @@ module Mutations
module CustomerRelations
module Contacts
class Create < BaseMutation
graphql_name 'CustomerRelationsContactCreate'
include ResolvesIds
include Gitlab::Graphql::Authorize::AuthorizeResource
graphql_name 'CustomerRelationsContactCreate'
field :contact,
Types::CustomerRelations::ContactType,
null: true,

View File

@ -4,10 +4,10 @@ module Mutations
module CustomerRelations
module Contacts
class Update < Mutations::BaseMutation
include ResolvesIds
graphql_name 'CustomerRelationsContactUpdate'
include ResolvesIds
authorize :admin_crm_contact
field :contact,

View File

@ -4,11 +4,11 @@ module Mutations
module CustomerRelations
module Organizations
class Create < BaseMutation
graphql_name 'CustomerRelationsOrganizationCreate'
include ResolvesIds
include Gitlab::Graphql::Authorize::AuthorizeResource
graphql_name 'CustomerRelationsOrganizationCreate'
field :organization,
Types::CustomerRelations::OrganizationType,
null: true,

View File

@ -4,10 +4,10 @@ module Mutations
module CustomerRelations
module Organizations
class Update < Mutations::BaseMutation
include ResolvesIds
graphql_name 'CustomerRelationsOrganizationUpdate'
include ResolvesIds
authorize :admin_crm_organization
field :organization,

View File

@ -4,10 +4,10 @@ module Mutations
module DependencyProxy
module GroupSettings
class Update < Mutations::BaseMutation
include Mutations::ResolvesGroup
graphql_name 'UpdateDependencyProxySettings'
include Mutations::ResolvesGroup
authorize :admin_dependency_proxy
argument :group_path,

View File

@ -4,10 +4,10 @@ module Mutations
module DependencyProxy
module ImageTtlGroupPolicy
class Update < Mutations::BaseMutation
include Mutations::ResolvesGroup
graphql_name 'UpdateDependencyProxyImageTtlGroupPolicy'
include Mutations::ResolvesGroup
authorize :admin_dependency_proxy
argument :group_path,

View File

@ -3,10 +3,10 @@
module Mutations
module DesignManagement
class Delete < Base
Errors = ::Gitlab::Graphql::Errors
graphql_name "DesignManagementDelete"
Errors = ::Gitlab::Graphql::Errors
argument :filenames, [GraphQL::Types::String],
required: true,
description: "Filenames of the designs to delete.",

View File

@ -3,10 +3,10 @@
module Mutations
module Groups
class Update < Mutations::BaseMutation
include Mutations::ResolvesGroup
graphql_name 'GroupUpdate'
include Mutations::ResolvesGroup
authorize :admin_group
field :group, Types::GroupType,

View File

@ -3,12 +3,12 @@
module Mutations
module Issues
class Create < BaseMutation
graphql_name 'CreateIssue'
include Mutations::SpamProtection
include FindsProject
include CommonMutationArguments
graphql_name 'CreateIssue'
authorize :create_issue
argument :project_path, GraphQL::Types::ID,

View File

@ -3,10 +3,10 @@
module Mutations
module Issues
class SetConfidential < Base
include Mutations::SpamProtection
graphql_name 'IssueSetConfidential'
include Mutations::SpamProtection
argument :confidential,
GraphQL::Types::Boolean,
required: true,

View File

@ -3,10 +3,10 @@
module Mutations
module JiraImport
class ImportUsers < BaseMutation
include FindsProject
graphql_name 'JiraImportUsers'
include FindsProject
authorize :admin_project
field :jira_users,

View File

@ -3,10 +3,10 @@
module Mutations
module JiraImport
class Start < BaseMutation
include FindsProject
graphql_name 'JiraImportStart'
include FindsProject
authorize :admin_project
field :jira_import,

View File

@ -3,10 +3,10 @@
module Mutations
module Labels
class Create < BaseMutation
include Mutations::ResolvesResourceParent
graphql_name 'LabelCreate'
include Mutations::ResolvesResourceParent
field :label,
Types::LabelType,
null: true,

View File

@ -3,12 +3,6 @@
module Mutations
module MergeRequests
class Accept < Base
NOT_MERGEABLE = 'This branch cannot be merged'
HOOKS_VALIDATION_ERROR = 'Pre-merge hooks failed'
SHA_MISMATCH = 'The merge-head is not at the anticipated SHA'
MERGE_FAILED = 'The merge failed'
ALREADY_SCHEDULED = 'The merge request is already scheduled to be merged'
graphql_name 'MergeRequestAccept'
authorize :accept_merge_request
description <<~DESC
@ -17,6 +11,12 @@ module Mutations
immediately if possible, or using one of the automatic merge strategies.
DESC
NOT_MERGEABLE = 'This branch cannot be merged'
HOOKS_VALIDATION_ERROR = 'Pre-merge hooks failed'
SHA_MISMATCH = 'The merge-head is not at the anticipated SHA'
MERGE_FAILED = 'The merge failed'
ALREADY_SCHEDULED = 'The merge request is already scheduled to be merged'
argument :strategy,
::Types::MergeStrategyEnum,
required: false,

View File

@ -3,10 +3,10 @@
module Mutations
module MergeRequests
class Create < BaseMutation
include FindsProject
graphql_name 'MergeRequestCreate'
include FindsProject
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the merge request is associated with.'

View File

@ -4,10 +4,10 @@ module Mutations
module Namespace
module PackageSettings
class Update < Mutations::BaseMutation
include Mutations::ResolvesNamespace
graphql_name 'UpdateNamespacePackageSettings'
include Mutations::ResolvesNamespace
authorize :create_package_settings
argument :namespace_path,

View File

@ -3,14 +3,13 @@
module Mutations
module ReleaseAssetLinks
class Create < BaseMutation
include FindsProject
graphql_name 'ReleaseAssetLinkCreate'
authorize :create_release
include FindsProject
include Types::ReleaseAssetLinkSharedInputArguments
authorize :create_release
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the project the asset link is associated with.'

View File

@ -3,14 +3,14 @@
module Mutations
module Snippets
class Create < BaseMutation
graphql_name 'CreateSnippet'
include ServiceCompatibility
include CanMutateSpammable
include Mutations::SpamProtection
authorize :create_snippet
graphql_name 'CreateSnippet'
field :snippet,
Types::SnippetType,
null: true,

View File

@ -3,12 +3,12 @@
module Mutations
module Snippets
class Update < Base
graphql_name 'UpdateSnippet'
include ServiceCompatibility
include CanMutateSpammable
include Mutations::SpamProtection
graphql_name 'UpdateSnippet'
argument :id, ::Types::GlobalIDType[::Snippet],
required: true,
description: 'Global ID of the snippet to update.'

View File

@ -3,11 +3,11 @@
module Mutations
module WorkItems
class Create < BaseMutation
graphql_name 'WorkItemCreate'
include Mutations::SpamProtection
include FindsProject
graphql_name 'WorkItemCreate'
authorize :create_work_item
argument :description, GraphQL::Types::String,

View File

@ -3,11 +3,10 @@
module Mutations
module WorkItems
class Delete < BaseMutation
graphql_name 'WorkItemDelete'
description "Deletes a work item." \
" Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
graphql_name 'WorkItemDelete'
authorize :delete_work_item
argument :id, ::Types::GlobalIDType[::WorkItem],

View File

@ -3,12 +3,11 @@
module Mutations
module WorkItems
class Update < BaseMutation
include Mutations::SpamProtection
graphql_name 'WorkItemUpdate'
description "Updates a work item by Global ID." \
" Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
graphql_name 'WorkItemUpdate'
include Mutations::SpamProtection
authorize :update_work_item

View File

@ -5,10 +5,11 @@ module Types
module Analytics
module UsageTrends
class MeasurementType < BaseObject
include Gitlab::Graphql::Authorize::AuthorizeResource
graphql_name 'UsageTrendsMeasurement'
description 'Represents a recorded measurement (object count) for the Admins'
include Gitlab::Graphql::Authorize::AuthorizeResource
authorize :read_usage_trends_measurement
field :recorded_at, Types::TimeType, null: true,

View File

@ -3,11 +3,11 @@
module Types
module AlertManagement
class PrometheusIntegrationType < ::Types::BaseObject
include ::Gitlab::Routing
graphql_name 'AlertManagementPrometheusIntegration'
description 'An endpoint and credentials used to accept Prometheus alerts for a project'
include ::Gitlab::Routing
implements(Types::AlertManagement::IntegrationType)
authorize :admin_project

View File

@ -3,11 +3,11 @@
module Types
# rubocop: disable Graphql/AuthorizeTypes
class BoardListType < BaseObject
include Gitlab::Utils::StrongMemoize
graphql_name 'BoardList'
description 'Represents a list for an issue board'
include Gitlab::Utils::StrongMemoize
alias_method :list, :object
field :id, GraphQL::Types::ID,

View File

@ -3,12 +3,13 @@
module Types
module Ci
class RunnerType < BaseObject
graphql_name 'CiRunner'
edge_type_class(RunnerWebUrlEdge)
connection_type_class(Types::CountableConnectionType)
graphql_name 'CiRunner'
authorize :read_runner
present_using ::Ci::RunnerPresenter
expose_permissions Types::PermissionTypes::Ci::Runner
JOB_COUNT_LIMIT = 1000

View File

@ -2,14 +2,14 @@
module Types
class GroupInvitationType < BaseObject
graphql_name 'GroupInvitation'
description 'Represents a Group Invitation'
expose_permissions Types::PermissionTypes::Group
authorize :admin_group
implements InvitationInterface
graphql_name 'GroupInvitation'
description 'Represents a Group Invitation'
field :group, Types::GroupType, null: true,
description: 'Group that a User is invited to.'

View File

@ -2,14 +2,14 @@
module Types
class GroupMemberType < BaseObject
graphql_name 'GroupMember'
description 'Represents a Group Membership'
expose_permissions Types::PermissionTypes::Group
authorize :read_group
implements MemberInterface
graphql_name 'GroupMember'
description 'Represents a Group Membership'
field :group, Types::GroupType, null: true,
description: 'Group that a User is a member of.'

View File

@ -3,11 +3,12 @@
module Types
module MergeRequests
class AssigneeType < ::Types::UserType
graphql_name 'MergeRequestAssignee'
description 'A user assigned to a merge request.'
include FindClosest
include ::Types::MergeRequests::InteractsWithMergeRequest
graphql_name 'MergeRequestAssignee'
description 'A user assigned to a merge request.'
authorize :read_user
end
end

View File

@ -3,11 +3,12 @@
module Types
module MergeRequests
class ReviewerType < ::Types::UserType
graphql_name 'MergeRequestReviewer'
description 'A user assigned to a merge request as a reviewer.'
include FindClosest
include ::Types::MergeRequests::InteractsWithMergeRequest
graphql_name 'MergeRequestReviewer'
description 'A user assigned to a merge request as a reviewer.'
authorize :read_user
end
end

View File

@ -4,8 +4,8 @@ module Types
module Metrics
module Dashboards
class AnnotationType < ::Types::BaseObject
authorize :read_metrics_dashboard_annotation
graphql_name 'MetricsDashboardAnnotation'
authorize :read_metrics_dashboard_annotation
field :description, GraphQL::Types::String, null: true,
description: 'Description of the annotation.'

View File

@ -2,10 +2,10 @@
module Types
class MutationType < BaseObject
include Gitlab::Graphql::MountMutation
graphql_name 'Mutation'
include Gitlab::Graphql::MountMutation
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
mount_mutation Mutations::AlertManagement::CreateAlertIssue
mount_mutation Mutations::AlertManagement::UpdateAlertStatus

View File

@ -3,10 +3,10 @@
module Types
module Notes
class DiscussionType < BaseObject
DiscussionID = ::Types::GlobalIDType[::Discussion]
graphql_name 'Discussion'
DiscussionID = ::Types::GlobalIDType[::Discussion]
authorize :read_note
implements(Types::ResolvableInterface)

View File

@ -3,10 +3,11 @@
module Types
module Packages
class PackageDetailsType < PackageType
include ::PackagesHelper
graphql_name 'PackageDetailsType'
description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes'
include ::PackagesHelper
authorize :read_package
field :versions, ::Types::Packages::PackageType.connection_type, null: true,

View File

@ -3,8 +3,8 @@
module Types
module PermissionTypes
class Issue < BasePermissionType
description 'Check permissions for the current user on a issue'
graphql_name 'IssuePermissions'
description 'Check permissions for the current user on a issue'
abilities :read_issue, :admin_issue, :update_issue, :reopen_issue,
:read_design, :create_design, :destroy_design,

View File

@ -3,15 +3,16 @@
module Types
module PermissionTypes
class MergeRequest < BasePermissionType
graphql_name 'MergeRequestPermissions'
description 'Check permissions for the current user on a merge request'
present_using MergeRequestPresenter
PERMISSION_FIELDS = %i[push_to_source_branch
remove_source_branch
cherry_pick_on_current_merge_request
revert_on_current_merge_request].freeze
present_using MergeRequestPresenter
description 'Check permissions for the current user on a merge request'
graphql_name 'MergeRequestPermissions'
abilities :read_merge_request, :admin_merge_request,
:update_merge_request, :create_note

View File

@ -3,10 +3,10 @@
module Types
# rubocop: disable Graphql/AuthorizeTypes
class QueryComplexityType < ::Types::BaseObject
ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity }
graphql_name 'QueryComplexity'
ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity }
alias_method :query, :object
field :limit, GraphQL::Types::Int,

View File

@ -4,10 +4,10 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class BlobType < BaseObject
present_using BlobPresenter
graphql_name 'RepositoryBlob'
present_using BlobPresenter
field :id, GraphQL::Types::ID, null: false,
description: 'ID of the blob.'

View File

@ -3,10 +3,10 @@
module Types
module Terraform
class StateVersionType < BaseObject
include ::API::Helpers::RelatedResourcesHelpers
graphql_name 'TerraformStateVersion'
include ::API::Helpers::RelatedResourcesHelpers
authorize :read_terraform_state
field :id, GraphQL::Types::ID,

View File

@ -4,12 +4,11 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class BlobType < BaseObject
implements Types::Tree::EntryType
present_using BlobPresenter
graphql_name 'Blob'
implements Types::Tree::EntryType
present_using BlobPresenter
field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the blob.'
field :web_path, GraphQL::Types::String, null: true,

View File

@ -4,10 +4,10 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class SubmoduleType < BaseObject
implements Types::Tree::EntryType
graphql_name 'Submodule'
implements Types::Tree::EntryType
field :web_url, type: GraphQL::Types::String, null: true,
description: 'Web URL for the sub-module.'
field :tree_url, type: GraphQL::Types::String, null: true,

View File

@ -4,13 +4,12 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes
# This is presented through `Repository` that has its own authorization
class TreeEntryType < BaseObject
implements Types::Tree::EntryType
present_using TreeEntryPresenter
graphql_name 'TreeEntry'
description 'Represents a directory'
implements Types::Tree::EntryType
present_using TreeEntryPresenter
field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL for the tree entry (directory).'
field :web_path, GraphQL::Types::String, null: true,

View File

@ -12,6 +12,8 @@ module Ci
initial_branch = params[:branch_name]
latest_commit = project.repository.commit(initial_branch) || project.commit
commit_sha = latest_commit ? latest_commit.sha : ''
total_branches = project.repository_exists? ? project.repository.branch_count : 0
{
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
@ -29,7 +31,7 @@ module Ci
"project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/index'),
"total-branches" => project.repository.branches.length,
"total-branches" => total_branches,
"yml-help-page-path" => help_page_path('ci/yaml/index')
}
end

View File

@ -0,0 +1,28 @@
-# This partial renders a GlToggle root element.
-# To actually initialize the component, make sure to call the initToggle helper from ~/toggles.
- classes = local_assigns.fetch(:classes)
- name = local_assigns.fetch(:name, nil)
- is_checked = local_assigns.fetch(:is_checked, false).to_s
- disabled = local_assigns.fetch(:disabled, false).to_s
- is_loading = local_assigns.fetch(:is_loading, false).to_s
- label = local_assigns.fetch(:label, nil)
- help = local_assigns.fetch(:help, nil)
- label_position = local_assigns.fetch(:label_position, nil)
- data = local_assigns.fetch(:data, {})
%span{ class: classes,
data: { name: name,
is_checked: is_checked,
disabled: disabled,
is_loading: is_loading,
label: label,
help: help,
label_position: label_position,
**data } }
-# Leverage this block to render a rich help text. To render a plain text help text,
-# prefer the `help` parameter.
- if yield.present?
.gl-text-secondary.gl-mt-1
= yield

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
class StartBackfillCiQueuingTables < Gitlab::Database::Migration[1.0]
MIGRATION = 'BackfillCiQueuingTables'
BATCH_SIZE = 500
DELAY_INTERVAL = 2.minutes
disable_ddl_transaction!
def up
return if Gitlab.com?
queue_background_migration_jobs_by_range_at_intervals(
Gitlab::BackgroundMigration::BackfillCiQueuingTables::Ci::Build.pending,
MIGRATION,
DELAY_INTERVAL,
batch_size: BATCH_SIZE,
track_jobs: true)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
dbe6760198b8fa068c30871a439298e56802867044a178baa6b8b009f8da13e6

View File

@ -528,7 +528,7 @@ You can use it either for personal or business websites, such as portfolios, doc
#### GitLab Runner
- [Project page](https://gitlab.com/gitlab-org/gitlab-runner/blob/master/README.md)
- [Project page](https://gitlab.com/gitlab-org/gitlab-runner/blob/main/README.md)
- Configuration:
- [Omnibus](https://docs.gitlab.com/runner/)
- [Charts](https://docs.gitlab.com/runner/install/kubernetes.html)

View File

@ -41,7 +41,7 @@ the GitLab team to run the job.
If you want to know the in-depth details, here's what's really happening:
1. You manually run the `review-docs-deploy` job in a merge request.
1. The job runs the [`scripts/trigger-build`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/trigger-build)
1. The job runs the [`scripts/trigger-build.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/trigger-build.rb)
script with the `docs deploy` flag, which triggers the "Triggered from `gitlab-org/gitlab` 'review-docs-deploy' job"
pipeline trigger in the `gitlab-org/gitlab-docs` project for the `$DOCS_BRANCH` (defaults to `main`).
1. The preview URL is shown both at the job output and in the merge request

View File

@ -99,7 +99,7 @@ The pipeline in the `gitlab-docs` project:
Once a week on Mondays, a scheduled pipeline runs and rebuilds the Docker images
used in various pipeline jobs, like `docs-lint`. The Docker image configuration files are
located in the [Dockerfiles directory](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/master/dockerfiles).
located in the [Dockerfiles directory](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/main/dockerfiles).
If you need to rebuild the Docker images immediately (must have maintainer level permissions):

View File

@ -199,7 +199,7 @@ You can find Vale configuration in the following projects:
- [`gitlab-runner`](https://gitlab.com/gitlab-org/gitlab-runner/-/tree/main/docs/.vale/gitlab)
- [`omnibus-gitlab`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/tree/master/doc/.vale/gitlab)
- [`charts`](https://gitlab.com/gitlab-org/charts/gitlab/-/tree/master/doc/.vale/gitlab)
- [`gitlab-development-kit`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/tree/master/doc/.vale/gitlab)
- [`gitlab-development-kit`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/tree/main/doc/.vale/gitlab)
This configuration is also used in build pipelines, where
[error-level rules](#vale-result-types) are enforced.

View File

@ -1389,7 +1389,7 @@ The JSON report artifacts are not a public API of DAST and their format is expec
The DAST tool always emits a JSON report file called `gl-dast-report.json` and
sample reports can be found in the
[DAST repository](https://gitlab.com/gitlab-org/security-products/dast/-/tree/master/test/end-to-end/expect).
[DAST repository](https://gitlab.com/gitlab-org/security-products/dast/-/tree/main/test/end-to-end/expect).
## Optimizing DAST

View File

@ -5,7 +5,7 @@ group: Product Planning
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Planning hierarchies **(PREMIUM)**
# Planning hierarchies **(FREE)**
Planning hierarchies are an integral part of breaking down your work in GitLab.
To understand how you can use epics and issues together in hierarchies, remember the following:
@ -22,7 +22,7 @@ portfolio management, see
## View planning hierarchies
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.8 and is behind the feature flag `work_items_hierarchy`.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.8.
To view the planning hierarchy in a project:
@ -34,7 +34,7 @@ The work items outside your subscription plan show up below **Unavailable struct
![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_8.png)
## Hierarchies with epics
## Hierarchies with epics **(PREMIUM)**
With epics, you can achieve the following hierarchy:
@ -68,14 +68,14 @@ Epic "1"*-- "0..*" Issue
![Diagram showing possible relationships of multi-level epics](img/hierarchy_with_multi_level_epics.png)
## View ancestry of an epic
In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**.
![epics state dropdown](img/epic-view-ancestors-in-sidebar_v14_6.png)
## View ancestry of an issue
In an issue, you can view the parented epic above the issue in the right sidebar under **Epic**.
![epics state dropdown](img/issue-view-parent-epic-in-sidebar_v14_6.png)
## View ancestry of an epic **(PREMIUM)**
In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**.
![epics state dropdown](img/epic-view-ancestors-in-sidebar_v14_6.png)

View File

@ -0,0 +1,153 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Ensure queuing entries are present even if admins skip upgrades.
class BackfillCiQueuingTables
class Namespace < ActiveRecord::Base # rubocop:disable Style/Documentation
self.table_name = 'namespaces'
self.inheritance_column = :_type_disabled
end
class Project < ActiveRecord::Base # rubocop:disable Style/Documentation
self.table_name = 'projects'
belongs_to :namespace
has_one :ci_cd_settings, class_name: 'Gitlab::BackgroundMigration::BackfillCiQueuingTables::ProjectCiCdSetting'
def group_runners_enabled?
return false unless ci_cd_settings
ci_cd_settings.group_runners_enabled?
end
end
class ProjectCiCdSetting < ActiveRecord::Base # rubocop:disable Style/Documentation
self.table_name = 'project_ci_cd_settings'
end
class Taggings < ActiveRecord::Base # rubocop:disable Style/Documentation
self.table_name = 'taggings'
end
module Ci
class Build < ActiveRecord::Base # rubocop:disable Style/Documentation
include EachBatch
self.table_name = 'ci_builds'
self.inheritance_column = :_type_disabled
belongs_to :project
scope :pending, -> do
where(status: :pending, type: 'Ci::Build', runner_id: nil)
end
def self.each_batch(of: 1000, column: :id, order: { runner_id: :asc, id: :asc }, order_hint: nil)
start = except(:select).select(column).reorder(order)
start = start.take
return unless start
start_id = start[column]
arel_table = self.arel_table
1.step do |index|
start_cond = arel_table[column].gteq(start_id)
stop = except(:select).select(column).where(start_cond).reorder(order)
stop = stop.offset(of).limit(1).take
relation = where(start_cond)
if stop
stop_id = stop[column]
start_id = stop_id
stop_cond = arel_table[column].lt(stop_id)
relation = relation.where(stop_cond)
end
# Any ORDER BYs are useless for this relation and can lead to less
# efficient UPDATE queries, hence we get rid of it.
relation = relation.except(:order)
# Using unscoped is necessary to prevent leaking the current scope used by
# ActiveRecord to chain `each_batch` method.
unscoped { yield relation, index }
break unless stop
end
end
def tags_ids
BackfillCiQueuingTables::Taggings
.where(taggable_id: id, taggable_type: 'CommitStatus')
.pluck(:tag_id)
end
end
class PendingBuild < ActiveRecord::Base # rubocop:disable Style/Documentation
self.table_name = 'ci_pending_builds'
class << self
def upsert_from_build!(build)
entry = self.new(args_from_build(build))
self.upsert(
entry.attributes.compact,
returning: %w[build_id],
unique_by: :build_id)
end
def args_from_build(build)
project = build.project
{
build_id: build.id,
project_id: build.project_id,
protected: build.protected?,
namespace_id: project.namespace_id,
tag_ids: build.tags_ids,
instance_runners_enabled: project.shared_runners_enabled?,
namespace_traversal_ids: namespace_traversal_ids(project)
}
end
def namespace_traversal_ids(project)
if project.group_runners_enabled?
project.namespace.traversal_ids
else
[]
end
end
end
end
end
BATCH_SIZE = 100
def perform(start_id, end_id)
scope = BackfillCiQueuingTables::Ci::Build.pending.where(id: start_id..end_id)
pending_builds_query = BackfillCiQueuingTables::Ci::PendingBuild
.where('ci_builds.id = ci_pending_builds.build_id')
.select(1)
scope.each_batch(of: BATCH_SIZE) do |builds|
builds = builds.where('NOT EXISTS (?)', pending_builds_query)
builds = builds.includes(:project, project: [:namespace, :ci_cd_settings])
builds.each do |build|
BackfillCiQueuingTables::Ci::PendingBuild.upsert_from_build!(build)
end
end
mark_job_as_succeeded(start_id, end_id)
end
private
def mark_job_as_succeeded(*arguments)
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
self.class.name.demodulize,
arguments)
end
end
end
end

View File

@ -57,8 +57,8 @@ module SystemCheck
WHERE (p.repository_storage LIKE ?)
"
query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) # rubocop:disable GitlabSecurity/PublicSend
ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) || []
query = ::Project.sanitize_sql_array([sql, storage_name])
::Project.connection.select_all(query).rows.try(:flatten!) || []
end
def fetch_disk_namespaces(storage_path)

View File

@ -14377,6 +14377,9 @@ msgstr ""
msgid "Escalation policies must have at least one rule"
msgstr ""
msgid "Escalation policy"
msgstr ""
msgid "Escalation policy:"
msgstr ""
@ -19112,6 +19115,12 @@ msgstr ""
msgid "IncidentManagement|Open"
msgstr ""
msgid "IncidentManagement|Page your team with escalation policies"
msgstr ""
msgid "IncidentManagement|Paged"
msgstr ""
msgid "IncidentManagement|Published"
msgstr ""
@ -19139,6 +19148,9 @@ msgstr ""
msgid "IncidentManagement|Unpublished"
msgstr ""
msgid "IncidentManagement|Use escalation policies to automatically page your team when incidents are created."
msgstr ""
msgid "IncidentSettings|Activate \"time to SLA\" countdown timer"
msgstr ""
@ -19184,7 +19196,10 @@ msgstr ""
msgid "Incidents"
msgstr ""
msgid "Incidents|Add a URL"
msgid "Incidents|Add image details"
msgstr ""
msgid "Incidents|Add text or a link to display with your image. If you don't add either, the file name displays instead."
msgstr ""
msgid "Incidents|Drop or %{linkStart}upload%{linkEnd} a metric screenshot to attach it to the incident"
@ -19199,10 +19214,10 @@ msgstr ""
msgid "Incidents|There was an issue loading metric images."
msgstr ""
msgid "Incidents|There was an issue uploading your image."
msgid "Incidents|There was an issue updating your image."
msgstr ""
msgid "Incidents|You can optionally add a URL to link users to the original graph."
msgid "Incidents|There was an issue uploading your image."
msgstr ""
msgid "Incident|Alert details"
@ -19211,9 +19226,18 @@ msgstr ""
msgid "Incident|Are you sure you wish to delete this image?"
msgstr ""
msgid "Incident|Delete image"
msgstr ""
msgid "Incident|Deleting %{filename}"
msgstr ""
msgid "Incident|Edit image text or link"
msgstr ""
msgid "Incident|Editing %{filename}"
msgstr ""
msgid "Incident|Metrics"
msgstr ""
@ -21740,6 +21764,9 @@ msgstr ""
msgid "Link"
msgstr ""
msgid "Link (optional)"
msgstr ""
msgid "Link Prometheus monitoring to GitLab."
msgstr ""
@ -35999,6 +36026,9 @@ msgstr ""
msgid "Tests"
msgstr ""
msgid "Text (optional)"
msgstr ""
msgid "Text added to the body of all email messages. %{character_limit} character limit"
msgstr ""

View File

@ -40,7 +40,7 @@ module QA
end
base.view 'app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue' do
element :milestone_link, 'data-qa-selector="`${issuableAttribute}_link`"' # rubocop:disable QA/ElementWithPattern
element :milestone_link, 'data-qa-selector="`${formatIssuableAttribute.snake}_link`"' # rubocop:disable QA/ElementWithPattern
end
base.view 'app/assets/javascripts/sidebar/components/sidebar_editable_item.vue' do

View File

@ -21,6 +21,12 @@ module Trigger
variable_value
end
def self.variables_for_env_file(variables)
variables.map do |key, value|
%Q(#{key}=#{value})
end.join("\n")
end
class Base
# Can be overridden
def self.access_token
@ -57,6 +63,21 @@ module Trigger
end
end
def variables
simple_forwarded_variables.merge(base_variables, extra_variables, version_file_variables)
end
def simple_forwarded_variables
{
'TRIGGER_SOURCE' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'],
'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME'],
'TOP_UPSTREAM_SOURCE_JOB' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID' => ENV['CI_MERGE_REQUEST_PROJECT_ID'],
'TOP_UPSTREAM_MERGE_REQUEST_IID' => ENV['CI_MERGE_REQUEST_IID']
}
end
private
# Override to trigger and work with pipeline on different GitLab instance
@ -95,23 +116,13 @@ module Trigger
ENV[version_file]&.strip || File.read(version_file).strip
end
def variables
base_variables.merge(extra_variables).merge(version_file_variables)
end
def base_variables
# Use CI_MERGE_REQUEST_SOURCE_BRANCH_SHA for omnibus checkouts due to pipeline for merged results,
# and fallback to CI_COMMIT_SHA for the `detached` pipelines.
{
'GITLAB_REF_SLUG' => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : ENV['CI_COMMIT_REF_SLUG'],
'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'],
'TRIGGER_SOURCE' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'],
'TOP_UPSTREAM_SOURCE_JOB' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_SOURCE_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA'],
'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME'],
'TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID' => ENV['CI_MERGE_REQUEST_PROJECT_ID'],
'TOP_UPSTREAM_MERGE_REQUEST_IID' => ENV['CI_MERGE_REQUEST_IID']
'TOP_UPSTREAM_SOURCE_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA']
}
end
@ -163,17 +174,16 @@ module Trigger
end
class CNG < Base
def self.access_token
# Default to "Multi-pipeline (from 'gitlab-org/gitlab' 'cloud-native-image' job)" at https://gitlab.com/gitlab-org/build/CNG/-/settings/access_tokens
ENV['CNG_PROJECT_ACCESS_TOKEN'] || super
def variables
# Delete variables that aren't useful when using native triggers.
super.tap do |hash|
hash.delete('TRIGGER_SOURCE')
hash.delete('TRIGGERED_USER')
end
end
private
def downstream_project_path
ENV.fetch('CNG_PROJECT_PATH', 'gitlab-org/build/CNG')
end
def ref
return ENV['CI_COMMIT_REF_NAME'] if ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/
@ -181,17 +191,17 @@ module Trigger
end
def extra_variables
edition = Trigger.ee? ? 'EE' : 'CE'
# Use CI_MERGE_REQUEST_SOURCE_BRANCH_SHA (MR HEAD commit) so that the image is in sync with the assets and QA images.
source_sha = Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA']
{
"ee" => Trigger.ee? ? "true" : "false",
"TRIGGER_BRANCH" => ref,
"GITLAB_VERSION" => source_sha,
"GITLAB_TAG" => ENV['CI_COMMIT_TAG'],
"GITLAB_TAG" => ENV['CI_COMMIT_TAG'], # Always set a value, even an empty string, so that the downstream pipeline can correctly check it.
"GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : source_sha,
"FORCE_RAILS_IMAGE_BUILDS" => 'true',
"#{edition}_PIPELINE" => 'true'
"CE_PIPELINE" => Trigger.ee? ? nil : "true", # Always set a value, even an empty string, so that the downstream pipeline can correctly check it.
"EE_PIPELINE" => Trigger.ee? ? "true" : nil # Always set a value, even an empty string, so that the downstream pipeline can correctly check it.
}
end
@ -445,28 +455,30 @@ module Trigger
Job = Class.new(Pipeline)
end
case ARGV[0]
when 'omnibus'
Trigger::Omnibus.new.invoke!(post_comment: true, downstream_job_name: 'Trigger:qa-test').wait!
when 'cng'
Trigger::CNG.new.invoke!.wait!
when 'gitlab-com-database-testing'
Trigger::DatabaseTesting.new.invoke!
when 'docs'
docs_trigger = Trigger::Docs.new
if $0 == __FILE__
case ARGV[0]
when 'omnibus'
Trigger::Omnibus.new.invoke!(post_comment: true, downstream_job_name: 'Trigger:qa-test').wait!
when 'cng'
Trigger::CNG.new.invoke!.wait!
when 'gitlab-com-database-testing'
Trigger::DatabaseTesting.new.invoke!
when 'docs'
docs_trigger = Trigger::Docs.new
case ARGV[1]
when 'deploy'
docs_trigger.deploy!
when 'cleanup'
docs_trigger.cleanup!
case ARGV[1]
when 'deploy'
docs_trigger.deploy!
when 'cleanup'
docs_trigger.cleanup!
else
puts 'usage: trigger-build docs <deploy|cleanup>'
exit 1
end
else
puts 'usage: trigger-build docs <deploy|cleanup>'
exit 1
puts "Please provide a valid option:
omnibus - Triggers a pipeline that builds the omnibus-gitlab package
cng - Triggers a pipeline that builds images used by the GitLab helm chart
gitlab-com-database-testing - Triggers a pipeline that tests database changes on GitLab.com data"
end
else
puts "Please provide a valid option:
omnibus - Triggers a pipeline that builds the omnibus-gitlab package
cng - Triggers a pipeline that builds images used by the GitLab helm chart
gitlab-com-database-testing - Triggers a pipeline that tests database changes on GitLab.com data"
end

View File

@ -61,6 +61,15 @@ FactoryBot.define do
factory :incident do
issue_type { :incident }
association :work_item_type, :default, :incident
# An escalation status record is created for all incidents
# in app code. This is a trait to avoid creating escalation
# status records in specs which do not need them.
trait :with_escalation_status do
after(:create) do |incident|
create(:incident_management_issuable_escalation_status, issue: incident)
end
end
end
end
end

View File

@ -7,7 +7,6 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
@ -30,7 +29,6 @@ describe('AdminRunnerShowApp', () => {
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
@ -80,8 +78,7 @@ describe('AdminRunnerShowApp', () => {
});
it('shows basic runner details', async () => {
const expected = `Details
Description Instance runner
const expected = `Description Instance runner
Last contact Never contacted
Version 1.0.0
IP Address 127.0.0.1
@ -89,7 +86,7 @@ describe('AdminRunnerShowApp', () => {
Maximum job timeout None
Tags None`.replace(/\s+/g, ' ');
expect(findRunnerDetails().text()).toMatchInterpolatedText(expected);
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
describe('when runner cannot be updated', () => {

View File

@ -1,6 +1,6 @@
import { GlSprintf, GlIntersperse } from '@gitlab/ui';
import { createWrapper, ErrorWrapper } from '@vue/test-utils';
import { mountExtended, 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 { useFakeDate } from 'helpers/fake_date';
import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
@ -8,6 +8,8 @@ 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 RunnerTags from '~/runner/components/runner_tags.vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
@ -37,16 +39,14 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
...props,
},
stubs: {
GlIntersperse,
GlSprintf,
TimeAgo,
RunnerDetail,
...stubs,
},
});
};
@ -65,76 +65,85 @@ describe('RunnerDetails', () => {
expect(wrapper.text()).toBe('');
});
describe.each`
field | runner | expectedValue
${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
${'Description'} | ${{ description: null }} | ${'None'}
${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
${'Version'} | ${{ version: null }} | ${'None'}
${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
`('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => {
createComponent({
props: {
runner: {
...mockRunner,
...runner,
describe('Details tab', () => {
describe.each`
field | runner | expectedValue
${'Description'} | ${{ description: 'My runner' }} | ${'My runner'}
${'Description'} | ${{ description: null }} | ${'None'}
${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
${'Version'} | ${{ version: null }} | ${'None'}
${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'}
${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'}
${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'}
`('"$field" field', ({ field, runner, expectedValue }) => {
beforeEach(() => {
createComponent({
props: {
runner: {
...mockRunner,
...runner,
},
},
},
stubs: {
GlIntersperse,
GlSprintf,
TimeAgo,
},
});
});
it(`displays expected value "${expectedValue}"`, () => {
expect(findDd(field).text()).toBe(expectedValue);
});
});
it(`displays expected value "${expectedValue}"`, () => {
expect(findDd(field).text()).toBe(expectedValue);
});
});
describe('"Tags" field', () => {
const stubs = { RunnerTags, RunnerTag };
describe('"Tags" field', () => {
it('displays expected value "tag-1 tag-2"', () => {
createComponent({
props: {
runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
},
mountFn: mountExtended,
it('displays expected value "tag-1 tag-2"', () => {
createComponent({
props: {
runner: { ...mockRunner, tagList: ['tag-1', 'tag-2'] },
},
stubs,
});
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
});
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2');
});
it('displays "None" when runner has no tags', () => {
createComponent({
props: {
runner: { ...mockRunner, tagList: [] },
},
stubs,
});
it('displays "None" when runner has no tags', () => {
createComponent({
props: {
runner: { ...mockRunner, tagList: [] },
},
mountFn: mountExtended,
});
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
});
});
describe('Group runners', () => {
beforeEach(() => {
createComponent({
props: {
runner: mockGroupRunner,
},
expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None');
});
});
it('Shows a group runner details', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
describe('Group runners', () => {
beforeEach(() => {
createComponent({
props: {
runner: mockGroupRunner,
},
});
});
it('Shows a group runner details', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
});
});
});

View File

@ -0,0 +1,65 @@
import { formatJobCount, tableField, getPaginationVariables } from '~/runner/utils';
describe('~/runner/utils', () => {
describe('formatJobCount', () => {
it('formats a number', () => {
expect(formatJobCount(1)).toBe('1');
expect(formatJobCount(99)).toBe('99');
});
it('formats a large count', () => {
expect(formatJobCount(1000)).toBe('1,000');
expect(formatJobCount(1001)).toBe('1,000+');
});
it('returns an empty string for non-numeric values', () => {
expect(formatJobCount(undefined)).toBe('');
expect(formatJobCount(null)).toBe('');
expect(formatJobCount('number')).toBe('');
});
});
describe('tableField', () => {
it('a field with options', () => {
expect(tableField({ key: 'name' })).toEqual({
key: 'name',
label: '',
tdAttr: { 'data-testid': 'td-name' },
thClass: expect.any(Array),
});
});
it('a field with a label', () => {
const label = 'A field name';
expect(tableField({ key: 'name', label })).toMatchObject({
label,
});
});
it('a field with custom classes', () => {
const mockClasses = ['foo', 'bar'];
expect(tableField({ thClasses: mockClasses })).toMatchObject({
thClass: expect.arrayContaining(mockClasses),
});
});
});
describe('getPaginationVariables', () => {
const after = 'AFTER_CURSOR';
const before = 'BEFORE_CURSOR';
it.each`
case | pagination | pageSize | variables
${'next page'} | ${{ after }} | ${undefined} | ${{ after, first: 10 }}
${'prev page'} | ${{ before }} | ${undefined} | ${{ before, last: 10 }}
${'first page'} | ${{}} | ${undefined} | ${{ first: 10 }}
${'next page with N items'} | ${{ after }} | ${20} | ${{ after, first: 20 }}
${'prev page with N items'} | ${{ before }} | ${20} | ${{ before, last: 20 }}
${'first page with N items'} | ${{}} | ${20} | ${{ first: 20 }}
`('navigates to $case', ({ pagination, pageSize, variables }) => {
expect(getPaginationVariables(pagination, pageSize)).toEqual(variables);
});
});
});

View File

@ -0,0 +1,149 @@
import { createWrapper } from '@vue/test-utils';
import { GlToggle } from '@gitlab/ui';
import { initToggle } from '~/toggles';
// Selectors
const TOGGLE_WRAPPER_CLASS = '.gl-toggle-wrapper';
const TOGGLE_LABEL_CLASS = '.gl-toggle-label';
const CHECKED_CLASS = '.is-checked';
const DISABLED_CLASS = '.is-disabled';
const LOADING_CLASS = '.toggle-loading';
const HELP_TEXT_SELECTOR = '[data-testid="toggle-help"]';
// Toggle settings
const toggleClassName = 'js-custom-toggle-class';
const toggleLabel = 'Toggle label';
describe('toggles/index.js', () => {
let instance;
let toggleWrapper;
const createRootEl = (dataAttrs) => {
const dataset = {
label: toggleLabel,
...dataAttrs,
};
const el = document.createElement('span');
el.classList.add(toggleClassName);
Object.entries(dataset).forEach(([key, value]) => {
el.dataset[key] = value;
});
document.body.appendChild(el);
return el;
};
const initToggleWithOptions = (options = {}) => {
const el = createRootEl(options);
instance = initToggle(el);
toggleWrapper = document.querySelector(TOGGLE_WRAPPER_CLASS);
};
afterEach(() => {
document.body.innerHTML = '';
instance = null;
toggleWrapper = null;
});
describe('initToggle', () => {
describe('default state', () => {
beforeEach(() => {
initToggleWithOptions();
});
it('attaches a GlToggle to the element', async () => {
expect(toggleWrapper).not.toBe(null);
expect(toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).textContent).toBe(toggleLabel);
});
it('passes CSS classes down to GlToggle', () => {
expect(toggleWrapper.className).toContain(toggleClassName);
});
it('is not checked', () => {
expect(toggleWrapper.querySelector(CHECKED_CLASS)).toBe(null);
});
it('is enabled', () => {
expect(toggleWrapper.querySelector(DISABLED_CLASS)).toBe(null);
});
it('is not loading', () => {
expect(toggleWrapper.querySelector(LOADING_CLASS)).toBe(null);
});
it('emits "change" event when value changes', () => {
const wrapper = createWrapper(instance);
const event = 'change';
const listener = jest.fn();
instance.$on(event, listener);
expect(listener).toHaveBeenCalledTimes(0);
wrapper.find(GlToggle).vm.$emit(event, true);
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenLastCalledWith(true);
wrapper.find(GlToggle).vm.$emit(event, false);
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenLastCalledWith(false);
});
});
describe('with custom options', () => {
const name = 'toggle-name';
const help = 'Help text';
const foo = 'bar';
beforeEach(() => {
initToggleWithOptions({
name,
isChecked: true,
disabled: true,
isLoading: true,
help,
labelPosition: 'hidden',
foo,
});
toggleWrapper = document.querySelector(TOGGLE_WRAPPER_CLASS);
});
it('sets the custom name', () => {
const input = toggleWrapper.querySelector('input[type="hidden"]');
expect(input.name).toBe(name);
});
it('is checked', () => {
expect(toggleWrapper.querySelector(CHECKED_CLASS)).not.toBe(null);
});
it('is disabled', () => {
expect(toggleWrapper.querySelector(DISABLED_CLASS)).not.toBe(null);
});
it('is loading', () => {
expect(toggleWrapper.querySelector(LOADING_CLASS)).not.toBe(null);
});
it('sets the custom help text', () => {
expect(toggleWrapper.querySelector(HELP_TEXT_SELECTOR).textContent).toBe(help);
});
it('hides the label', () => {
expect(
toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).classList.contains('gl-sr-only'),
).toBe(true);
});
it('passes custom dataset to the wrapper', () => {
expect(toggleWrapper.dataset.foo).toBe('bar');
});
});
});
});

View File

@ -88,6 +88,17 @@ RSpec.describe Ci::PipelineEditorHelper do
end
end
context 'with a project with no repository' do
let(:project) { create(:project) }
it 'returns pipeline editor data' do
expect(pipeline_editor_data).to include({
"pipeline_etag" => '',
"total-branches" => 0
})
end
end
context 'with a non-default branch name' do
let(:user) { create(:user) }

View File

@ -0,0 +1,244 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration, schema: 20220208115439 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:ci_cd_settings) { table(:project_ci_cd_settings) }
let(:builds) { table(:ci_builds) }
let(:queuing_entries) { table(:ci_pending_builds) }
let(:tags) { table(:tags) }
let(:taggings) { table(:taggings) }
subject { described_class.new }
describe '#perform' do
let!(:namespace) do
namespaces.create!(
id: 10,
name: 'namespace10',
path: 'namespace10',
traversal_ids: [10])
end
let!(:other_namespace) do
namespaces.create!(
id: 11,
name: 'namespace11',
path: 'namespace11',
traversal_ids: [11])
end
let!(:project) do
projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1')
end
let!(:ci_cd_setting) do
ci_cd_settings.create!(id: 5, project_id: 5, group_runners_enabled: true)
end
let!(:other_project) do
projects.create!(id: 7, namespace_id: 11, name: 'test2', path: 'test2')
end
let!(:other_ci_cd_setting) do
ci_cd_settings.create!(id: 7, project_id: 7, group_runners_enabled: false)
end
let!(:another_project) do
projects.create!(id: 9, namespace_id: 10, name: 'test3', path: 'test3', shared_runners_enabled: false)
end
let!(:ruby_tag) do
tags.create!(id: 22, name: 'ruby')
end
let!(:postgres_tag) do
tags.create!(id: 23, name: 'postgres')
end
it 'creates ci_pending_builds for all pending builds in range' do
builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build')
builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build')
taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 22)
taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23)
builds.create!(id: 60, status: :pending, name: 'test1', project_id: 7, type: 'Ci::Build')
builds.create!(id: 61, status: :running, name: 'test2', project_id: 7, protected: true, type: 'Ci::Build')
builds.create!(id: 62, status: :pending, name: 'test3', project_id: 7, type: 'Ci::Build')
taggings.create!(taggable_id: 60, taggable_type: 'CommitStatus', tag_id: 23)
taggings.create!(taggable_id: 62, taggable_type: 'CommitStatus', tag_id: 22)
builds.create!(id: 70, status: :pending, name: 'test1', project_id: 9, protected: true, type: 'Ci::Build')
builds.create!(id: 71, status: :failed, name: 'test2', project_id: 9, type: 'Ci::Build')
builds.create!(id: 72, status: :pending, name: 'test3', project_id: 9, type: 'Ci::Build')
taggings.create!(taggable_id: 71, taggable_type: 'CommitStatus', tag_id: 22)
subject.perform(1, 100)
expect(queuing_entries.all).to contain_exactly(
an_object_having_attributes(
build_id: 50,
project_id: 5,
namespace_id: 10,
protected: false,
instance_runners_enabled: true,
minutes_exceeded: false,
tag_ids: [],
namespace_traversal_ids: [10]),
an_object_having_attributes(
build_id: 52,
project_id: 5,
namespace_id: 10,
protected: true,
instance_runners_enabled: true,
minutes_exceeded: false,
tag_ids: [22, 23],
namespace_traversal_ids: [10]),
an_object_having_attributes(
build_id: 60,
project_id: 7,
namespace_id: 11,
protected: false,
instance_runners_enabled: true,
minutes_exceeded: false,
tag_ids: [23],
namespace_traversal_ids: []),
an_object_having_attributes(
build_id: 62,
project_id: 7,
namespace_id: 11,
protected: false,
instance_runners_enabled: true,
minutes_exceeded: false,
tag_ids: [22],
namespace_traversal_ids: []),
an_object_having_attributes(
build_id: 70,
project_id: 9,
namespace_id: 10,
protected: true,
instance_runners_enabled: false,
minutes_exceeded: false,
tag_ids: [],
namespace_traversal_ids: []),
an_object_having_attributes(
build_id: 72,
project_id: 9,
namespace_id: 10,
protected: false,
instance_runners_enabled: false,
minutes_exceeded: false,
tag_ids: [],
namespace_traversal_ids: [])
)
end
it 'skips builds that already have ci_pending_builds' do
builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build')
builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build')
taggings.create!(taggable_id: 50, taggable_type: 'CommitStatus', tag_id: 22)
taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23)
queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10)
subject.perform(1, 100)
expect(queuing_entries.all).to contain_exactly(
an_object_having_attributes(
build_id: 50,
project_id: 5,
namespace_id: 10,
protected: false,
instance_runners_enabled: false,
minutes_exceeded: false,
tag_ids: [],
namespace_traversal_ids: []),
an_object_having_attributes(
build_id: 52,
project_id: 5,
namespace_id: 10,
protected: true,
instance_runners_enabled: true,
minutes_exceeded: false,
tag_ids: [23],
namespace_traversal_ids: [10])
)
end
it 'upserts values in case of conflicts' do
builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10)
build = described_class::Ci::Build.find(50)
described_class::Ci::PendingBuild.upsert_from_build!(build)
expect(queuing_entries.all).to contain_exactly(
an_object_having_attributes(
build_id: 50,
project_id: 5,
namespace_id: 10,
protected: false,
instance_runners_enabled: true,
minutes_exceeded: false,
tag_ids: [],
namespace_traversal_ids: [10])
)
end
end
context 'Ci::Build' do
describe '.each_batch' do
let(:model) { described_class::Ci::Build }
before do
builds.create!(id: 1, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
builds.create!(id: 2, status: :pending, name: 'test2', project_id: 5, type: 'Ci::Build')
builds.create!(id: 3, status: :pending, name: 'test3', project_id: 5, type: 'Ci::Build')
builds.create!(id: 4, status: :pending, name: 'test4', project_id: 5, type: 'Ci::Build')
builds.create!(id: 5, status: :pending, name: 'test5', project_id: 5, type: 'Ci::Build')
end
it 'yields an ActiveRecord::Relation when a block is given' do
model.each_batch do |relation|
expect(relation).to be_a_kind_of(ActiveRecord::Relation)
end
end
it 'yields a batch index as the second argument' do
model.each_batch do |_, index|
expect(index).to eq(1)
end
end
it 'accepts a custom batch size' do
amount = 0
model.each_batch(of: 1) { amount += 1 }
expect(amount).to eq(5)
end
it 'does not include ORDER BYs in the yielded relations' do
model.each_batch do |relation|
expect(relation.to_sql).not_to include('ORDER BY')
end
end
it 'orders ascending' do
ids = []
model.each_batch(of: 1) { |rel| ids.concat(rel.ids) }
expect(ids).to eq(ids.sort)
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe StartBackfillCiQueuingTables do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:builds) { table(:ci_builds) }
let!(:namespace) do
namespaces.create!(name: 'namespace1', path: 'namespace1')
end
let!(:project) do
projects.create!(namespace_id: namespace.id, name: 'test1', path: 'test1')
end
let!(:pending_build_1) do
builds.create!(status: :pending, name: 'test1', type: 'Ci::Build', project_id: project.id)
end
let!(:running_build) do
builds.create!(status: :running, name: 'test2', type: 'Ci::Build', project_id: project.id)
end
let!(:pending_build_2) do
builds.create!(status: :pending, name: 'test3', type: 'Ci::Build', project_id: project.id)
end
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
end
it 'schedules jobs for builds that are pending' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
2.minutes, pending_build_1.id, pending_build_1.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
4.minutes, pending_build_2.id, pending_build_2.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end

View File

@ -66,7 +66,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
it 'shows the help state when icon is clicked' do
page.within '.time-tracking-component-wrap' do
find('.help-button').click
find('[data-testid="helpButton"]').click
expect(page).to have_content 'Track time with quick actions'
expect(page).to have_content 'Learn more'
end
@ -92,8 +92,8 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
it 'hides the help state when close icon is clicked' do
page.within '.time-tracking-component-wrap' do
find('.help-button').click
find('.close-help-button').click
find('[data-testid="helpButton"]').click
find('[data-testid="closeHelpButton"]').click
expect(page).not_to have_content 'Track time with quick actions'
expect(page).not_to have_content 'Learn more'
@ -102,7 +102,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
it 'displays the correct help url' do
page.within '.time-tracking-component-wrap' do
find('.help-button').click
find('[data-testid="helpButton"]').click
expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md')
end

View File

@ -0,0 +1,85 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'shared/_gl_toggle.html.haml' do
context 'defaults' do
before do
render partial: 'shared/gl_toggle', locals: {
classes: '.js-gl-toggle'
}
end
it 'does not set a name' do
expect(rendered).not_to have_selector('[data-name]')
end
it 'sets default is-checked attributes' do
expect(rendered).to have_selector('[data-is-checked="false"]')
end
it 'sets default disabled attributes' do
expect(rendered).to have_selector('[data-disabled="false"]')
end
it 'sets default is-loading attributes' do
expect(rendered).to have_selector('[data-is-loading="false"]')
end
it 'does not set a label' do
expect(rendered).not_to have_selector('[data-label]')
end
it 'does not set a label position' do
expect(rendered).not_to have_selector('[data-label-position]')
end
end
context 'with custom options' do
before do
render partial: 'shared/gl_toggle', locals: {
classes: 'js-custom-gl-toggle',
name: 'toggle-name',
is_checked: true,
disabled: true,
is_loading: true,
label: 'Custom label',
label_position: 'top',
data: {
foo: 'bar'
}
}
end
it 'sets the custom class' do
expect(rendered).to have_selector('.js-custom-gl-toggle')
end
it 'sets the custom name' do
expect(rendered).to have_selector('[data-name="toggle-name"]')
end
it 'sets the custom is-checked attributes' do
expect(rendered).to have_selector('[data-is-checked="true"]')
end
it 'sets the custom disabled attributes' do
expect(rendered).to have_selector('[data-disabled="true"]')
end
it 'sets the custom is-loading attributes' do
expect(rendered).to have_selector('[data-is-loading="true"]')
end
it 'sets the custom label' do
expect(rendered).to have_selector('[data-label="Custom label"]')
end
it 'sets the cutom label position' do
expect(rendered).to have_selector('[data-label-position="top"]')
end
it 'sets cutom data attributes' do
expect(rendered).to have_selector('[data-foo="bar"]')
end
end
end