Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Feature Flag Roll Out.md1
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue6
-rw-r--r--app/assets/javascripts/feature_flags/components/form.vue11
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js1
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue7
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue65
-rw-r--r--app/finders/issuable_finder.rb9
-rw-r--r--app/graphql/resolvers/ci/jobs_resolver.rb13
-rw-r--r--app/graphql/resolvers/ci/pipeline_stages_resolver.rb2
-rw-r--r--app/graphql/types/ci/job_status_enum.rb15
-rw-r--r--app/graphql/types/ci/job_type.rb57
-rw-r--r--app/graphql/types/ci/pipeline_type.rb29
-rw-r--r--app/graphql/types/ci/stage_type.rb7
-rw-r--r--app/graphql/types/global_id_type.rb11
-rw-r--r--app/models/application_record.rb6
-rw-r--r--app/models/ci/group.rb7
-rw-r--r--app/models/ci/stage.rb1
-rw-r--r--app/views/projects/_commit_button.html.haml2
-rw-r--r--app/views/projects/_customize_workflow.html.haml2
-rw-r--r--app/views/projects/_fork_suggestion.html.haml2
-rw-r--r--app/views/projects/_invite_members.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/_readme.html.haml2
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/settings/operations/_tracing.html.haml9
-rw-r--r--app/views/projects/snippets/index.html.haml2
-rw-r--r--app/views/projects/tracings/show.html.haml4
-rw-r--r--changelogs/unreleased/ajk-graphql-ci-jobs.yml5
-rw-r--r--changelogs/unreleased/btn-confirm-projects-snippets.yml5
-rw-r--r--changelogs/unreleased/btn-confirm-projects.yml5
-rw-r--r--changelogs/unreleased/cngo-add-gl-toggle-labels.yml5
-rw-r--r--changelogs/unreleased/eread-refactor-jaeger-tracing-configuration-ui.yml5
-rw-r--r--doc/administration/job_artifacts.md3
-rw-r--r--doc/api/graphql/reference/index.md33
-rw-r--r--doc/operations/tracing.md35
-rw-r--r--lib/api/helpers/graphql_helpers.rb4
-rw-r--r--lib/gitlab/issuables_count_for_state.rb2
-rw-r--r--locale/gitlab.pot17
-rw-r--r--spec/factories/ci/builds.rb15
-rw-r--r--spec/factories/sequences.rb1
-rw-r--r--spec/frontend/__mocks__/vue/index.js7
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js6
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js5
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js43
-rw-r--r--spec/frontend/pipelines/nav_controls_spec.js60
-rw-r--r--spec/graphql/types/ci/job_status_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb19
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/stage_type_spec.rb1
-rw-r--r--spec/models/application_record_spec.rb29
-rw-r--r--spec/models/ci/stage_spec.rb12
-rw-r--r--spec/requests/api/graphql/ci/groups_spec.rb23
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb100
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb193
-rw-r--r--spec/support/helpers/board_helpers.rb9
-rw-r--r--spec/support/matchers/graphql_matchers.rb14
58 files changed, 789 insertions, 169 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
index ea3bd07f228..4c144f06f67 100644
--- a/.gitlab/issue_templates/Feature Flag Roll Out.md
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -35,6 +35,7 @@ If applicable, any groups/projects that are happy to have this feature turned on
- [ ] Test on staging
- [ ] Ensure that documentation has been updated
- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`)
+- [ ] If it is possible to perform an incremental rollout, this should be preferred. Proposed increments are: `10%`, `50%`, `100%`. Proposed minimum time between increments is 15 minutes.
- [ ] Coordinate a time to enable the flag with the SRE oncall and release managers
- In `#production` mention `@sre-oncall` and `@release-managers`. Once an SRE on call and Release Manager on call confirm, you can proceed with the rollout
- [ ] Announce on the issue an estimated time this will be enabled on GitLab.com. **Note**: Once a feature rollout has started, it is not necessary to inform `@sre-oncall`/`@release-managers` at each stage of the gradual rollout.
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
index 8418c0f66ac..6cbe443062a 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
@@ -1,9 +1,13 @@
<script>
import { GlToggle } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
+import { __ } from '~/locale';
import { disableShortcuts, enableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
export default {
+ i18n: {
+ toggleLabel: __('Keyboard shortcuts'),
+ },
components: {
GlToggle,
},
@@ -31,7 +35,7 @@ export default {
<gl-toggle
v-model="shortcutsEnabled"
aria-describedby="shortcutsToggle"
- label="Keyboard shortcuts"
+ :label="$options.i18n.toggleLabel"
label-position="left"
@change="onChange"
/>
diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue
index 1f59b709aa2..7e3d3bc5203 100644
--- a/app/assets/javascripts/feature_flags/components/form.vue
+++ b/app/assets/javascripts/feature_flags/components/form.vue
@@ -29,6 +29,9 @@ import EnvironmentsDropdown from './environments_dropdown.vue';
import Strategy from './strategy.vue';
export default {
+ i18n: {
+ statusLabel: s__('FeatureFlags|Status'),
+ },
components: {
GlButton,
GlBadge,
@@ -396,12 +399,14 @@ export default {
<div class="table-section section-20 text-center" role="gridcell">
<div class="table-mobile-header" role="rowheader">
- {{ s__('FeatureFlags|Status') }}
+ {{ $options.i18n.statusLabel }}
</div>
<div class="table-mobile-content gl-display-flex gl-justify-content-center">
<gl-toggle
:value="scope.active"
:disabled="!active || !canUpdateScope(scope)"
+ :label="$options.i18n.statusLabel"
+ label-position="hidden"
@change="(status) => (scope.active = status)"
/>
</div>
@@ -529,11 +534,13 @@ export default {
<div class="table-section section-20 text-center" role="gridcell">
<div class="table-mobile-header" role="rowheader">
- {{ s__('FeatureFlags|Status') }}
+ {{ $options.i18n.statusLabel }}
</div>
<div class="table-mobile-content gl-display-flex gl-justify-content-center">
<gl-toggle
:disabled="!active"
+ :label="$options.i18n.statusLabel"
+ label-position="hidden"
:value="false"
@change="createNewScope({ active: true })"
/>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
index d4f51b83e1e..faacabb44ce 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
@@ -2,6 +2,7 @@
import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
import {
+ MAVEN_TOGGLE_LABEL,
MAVEN_TITLE,
MAVEN_SETTINGS_SUBTITLE,
MAVEN_DUPLICATES_ALLOWED_DISABLED,
@@ -15,6 +16,7 @@ import {
export default {
name: 'MavenSettings',
i18n: {
+ MAVEN_TOGGLE_LABEL,
MAVEN_TITLE,
MAVEN_SETTINGS_SUBTITLE,
MAVEN_SETTING_EXCEPTION_TITLE,
@@ -80,6 +82,8 @@ export default {
<div class="gl-display-flex">
<gl-toggle
data-qa-selector="allow_duplicates_toggle"
+ :label="$options.i18n.MAVEN_TOGGLE_LABEL"
+ label-position="hidden"
:value="mavenDuplicatesAllowed"
@change="update($options.modelNames.MAVEN_DUPLICATES_ALLOWED, $event)"
/>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index 72bec74060c..d52a6a626f9 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -8,6 +8,7 @@ export const PACKAGE_SETTINGS_DESCRIPTION = s__(
export const MAVEN_TITLE = s__('PackageRegistry|Maven');
export const MAVEN_SETTINGS_SUBTITLE = s__('PackageRegistry|Settings for Maven packages');
+export const MAVEN_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
export const MAVEN_DUPLICATES_ALLOWED_DISABLED = s__(
'PackageRegistry|%{boldStart}Do not allow duplicates%{boldEnd} - Packages with the same name and version are rejected.',
);
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index d62df77ad2c..c110c1d4d62 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -12,6 +12,11 @@ export default {
event: 'change',
},
props: {
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
name: {
type: String,
required: false,
@@ -82,6 +87,8 @@ export default {
class="gl-mr-3"
:value="featureEnabled"
:disabled="disabledInput"
+ :label="label"
+ label-position="hidden"
@change="toggleFeature"
/>
<div class="select-wrapper gl-flex-fill-1">
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 419e98dad50..0b7b4c0ded1 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -22,6 +22,21 @@ const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
export default {
i18n: {
...CVE_ID_REQUEST_BUTTON_I18N,
+ analyticsLabel: s__('ProjectSettings|Analytics'),
+ containerRegistryLabel: s__('ProjectSettings|Container registry'),
+ forksLabel: s__('ProjectSettings|Forks'),
+ issuesLabel: s__('ProjectSettings|Issues'),
+ lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
+ mergeRequestsLabel: s__('ProjectSettings|Merge requests'),
+ operationsLabel: s__('ProjectSettings|Operations'),
+ packagesLabel: s__('ProjectSettings|Packages'),
+ pagesLabel: s__('ProjectSettings|Pages'),
+ ciCdLabel: s__('CI/CD'),
+ repositoryLabel: s__('ProjectSettings|Repository'),
+ requirementsLabel: s__('ProjectSettings|Requirements'),
+ securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'),
+ snippetsLabel: s__('ProjectSettings|Snippets'),
+ wikiLabel: s__('ProjectSettings|Wiki'),
},
components: {
@@ -423,11 +438,12 @@ export default {
>
<project-setting-row
ref="issues-settings"
- :label="s__('ProjectSettings|Issues')"
+ :label="$options.i18n.issuesLabel"
:help-text="s__('ProjectSettings|Lightweight issue tracking system.')"
>
<project-feature-setting
v-model="issuesAccessLevel"
+ :label="$options.i18n.issuesLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][issues_access_level]"
/>
@@ -440,6 +456,8 @@ export default {
v-model="cveIdRequestEnabled"
class="gl-my-2"
:disabled="cveIdRequestIsDisabled"
+ :label="$options.i18n.cve_request_toggle_label"
+ label-position="hidden"
name="project[project_setting_attributes][cve_id_request_enabled]"
data-testid="cve_id_request_toggle"
/>
@@ -447,11 +465,12 @@ export default {
</project-setting-row>
<project-setting-row
ref="repository-settings"
- :label="s__('ProjectSettings|Repository')"
+ :label="$options.i18n.repositoryLabel"
:help-text="repositoryHelpText"
>
<project-feature-setting
v-model="repositoryAccessLevel"
+ :label="$options.i18n.repositoryLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][repository_access_level]"
/>
@@ -459,11 +478,12 @@ export default {
<div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5">
<project-setting-row
ref="merge-request-settings"
- :label="s__('ProjectSettings|Merge requests')"
+ :label="$options.i18n.mergeRequestsLabel"
:help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
>
<project-feature-setting
v-model="mergeRequestsAccessLevel"
+ :label="$options.i18n.mergeRequestsLabel"
:options="repoFeatureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][merge_requests_access_level]"
@@ -471,11 +491,12 @@ export default {
</project-setting-row>
<project-setting-row
ref="fork-settings"
- :label="s__('ProjectSettings|Forks')"
+ :label="$options.i18n.forksLabel"
:help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
>
<project-feature-setting
v-model="forkingAccessLevel"
+ :label="$options.i18n.forksLabel"
:options="featureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][forking_access_level]"
@@ -485,7 +506,7 @@ export default {
v-if="registryAvailable"
ref="container-registry-settings"
:help-path="registryHelpPath"
- :label="s__('ProjectSettings|Container registry')"
+ :label="$options.i18n.containerRegistryLabel"
:help-text="
s__('ProjectSettings|Every project can have its own space to store its Docker images')
"
@@ -501,6 +522,8 @@ export default {
v-model="containerRegistryEnabled"
class="gl-my-2"
:disabled="!repositoryEnabled"
+ :label="$options.i18n.containerRegistryLabel"
+ label-position="hidden"
name="project[container_registry_enabled]"
/>
</project-setting-row>
@@ -508,7 +531,7 @@ export default {
v-if="lfsAvailable"
ref="git-lfs-settings"
:help-path="lfsHelpPath"
- :label="s__('ProjectSettings|Git Large File Storage (LFS)')"
+ :label="$options.i18n.lfsLabel"
:help-text="
s__('ProjectSettings|Manages large files such as audio, video, and graphics files.')
"
@@ -517,6 +540,8 @@ export default {
v-model="lfsEnabled"
class="gl-my-2"
:disabled="!repositoryEnabled"
+ :label="$options.i18n.lfsLabel"
+ label-position="hidden"
name="project[lfs_enabled]"
/>
<p v-if="!lfsEnabled && lfsObjectsExist">
@@ -541,7 +566,7 @@ export default {
v-if="packagesAvailable"
ref="package-settings"
:help-path="packagesHelpPath"
- :label="s__('ProjectSettings|Packages')"
+ :label="$options.i18n.packagesLabel"
:help-text="
s__('ProjectSettings|Every project can have its own space to store its packages.')
"
@@ -550,17 +575,20 @@ export default {
v-model="packagesEnabled"
class="gl-my-2"
:disabled="!repositoryEnabled"
+ :label="$options.i18n.packagesLabel"
+ label-position="hidden"
name="project[packages_enabled]"
/>
</project-setting-row>
</div>
<project-setting-row
ref="pipeline-settings"
- :label="__('CI/CD')"
+ :label="$options.i18n.ciCdLabel"
:help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
>
<project-feature-setting
v-model="buildsAccessLevel"
+ :label="$options.i18n.ciCdLabel"
:options="repoFeatureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][builds_access_level]"
@@ -568,11 +596,12 @@ export default {
</project-setting-row>
<project-setting-row
ref="analytics-settings"
- :label="s__('ProjectSettings|Analytics')"
+ :label="$options.i18n.analyticsLabel"
:help-text="s__('ProjectSettings|View project analytics.')"
>
<project-feature-setting
v-model="analyticsAccessLevel"
+ :label="$options.i18n.analyticsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][analytics_access_level]"
/>
@@ -580,43 +609,47 @@ export default {
<project-setting-row
v-if="requirementsAvailable"
ref="requirements-settings"
- :label="s__('ProjectSettings|Requirements')"
+ :label="$options.i18n.requirementsLabel"
:help-text="s__('ProjectSettings|Requirements management system.')"
>
<project-feature-setting
v-model="requirementsAccessLevel"
+ :label="$options.i18n.requirementsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][requirements_access_level]"
/>
</project-setting-row>
<project-setting-row
- :label="s__('ProjectSettings|Security & Compliance')"
+ :label="$options.i18n.securityAndComplianceLabel"
:help-text="s__('ProjectSettings|Security & Compliance for this project')"
>
<project-feature-setting
v-model="securityAndComplianceAccessLevel"
+ :label="$options.i18n.securityAndComplianceLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][security_and_compliance_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="wiki-settings"
- :label="s__('ProjectSettings|Wiki')"
+ :label="$options.i18n.wikiLabel"
:help-text="s__('ProjectSettings|Pages for project documentation.')"
>
<project-feature-setting
v-model="wikiAccessLevel"
+ :label="$options.i18n.wikiLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][wiki_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="snippet-settings"
- :label="s__('ProjectSettings|Snippets')"
+ :label="$options.i18n.snippetsLabel"
:help-text="s__('ProjectSettings|Share code with others outside the project.')"
>
<project-feature-setting
v-model="snippetsAccessLevel"
+ :label="$options.i18n.snippetsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][snippets_access_level]"
/>
@@ -625,26 +658,28 @@ export default {
v-if="pagesAvailable && pagesAccessControlEnabled"
ref="pages-settings"
:help-path="pagesHelpPath"
- :label="s__('ProjectSettings|Pages')"
+ :label="$options.i18n.pagesLabel"
:help-text="
s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
"
>
<project-feature-setting
v-model="pagesAccessLevel"
+ :label="$options.i18n.pagesLabel"
:options="pagesFeatureAccessLevelOptions"
name="project[project_feature_attributes][pages_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="operations-settings"
- :label="s__('ProjectSettings|Operations')"
+ :label="$options.i18n.operationsLabel"
:help-text="
s__('ProjectSettings|Configure your project resources and monitor their health.')
"
>
<project-feature-setting
v-model="operationsAccessLevel"
+ :label="$options.i18n.operationsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][operations_access_level]"
/>
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 2409dc9d77d..8642ea11a57 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -125,14 +125,14 @@ class IssuableFinder
end
def filter_items(items)
+ # Selection by group is already covered by `by_project` and `projects` for project-based issuables
+ # Group-based issuables have their own group filter methods
items = by_project(items)
- items = by_group(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
items = by_closed_at(items)
items = by_state(items)
- items = by_group(items)
items = by_assignee(items)
items = by_author(items)
items = by_non_archived(items)
@@ -320,11 +320,6 @@ class IssuableFinder
end
end
- def by_group(items)
- # Selection by group is already covered by `by_project` and `projects`
- items
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def by_project(items)
if params.project?
diff --git a/app/graphql/resolvers/ci/jobs_resolver.rb b/app/graphql/resolvers/ci/jobs_resolver.rb
index dd565094017..5ae9e721cc8 100644
--- a/app/graphql/resolvers/ci/jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/jobs_resolver.rb
@@ -11,7 +11,18 @@ module Resolvers
required: false,
description: 'Filter jobs by the type of security report they produce.'
- def resolve(security_report_types: [])
+ argument :statuses, [::Types::Ci::JobStatusEnum],
+ required: false,
+ description: 'Filter jobs by status.'
+
+ def resolve(statuses: nil, security_report_types: [])
+ jobs = init_collection(security_report_types)
+ jobs = jobs.with_status(statuses) if statuses.present?
+
+ jobs
+ end
+
+ def init_collection(security_report_types)
if security_report_types.present?
::Security::SecurityJobsFinder.new(
pipeline: pipeline,
diff --git a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
index 98170e0cd2e..a458e873935 100644
--- a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
+++ b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
@@ -16,7 +16,7 @@ module Resolvers
def preloads
{
- statuses: [:needs]
+ jobs: { latest_statuses: [:needs] }
}
end
end
diff --git a/app/graphql/types/ci/job_status_enum.rb b/app/graphql/types/ci/job_status_enum.rb
new file mode 100644
index 00000000000..ec80b1f4776
--- /dev/null
+++ b/app/graphql/types/ci/job_status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class JobStatusEnum < BaseEnum
+ graphql_name 'CiJobStatus'
+
+ ::Ci::HasStatus::AVAILABLE_STATUSES.each do |status|
+ value status.upcase,
+ description: "A job that is #{status.tr('_', ' ')}.",
+ value: status
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index f2529b8e5c8..eb235e6ee73 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -6,7 +6,9 @@ module Types
graphql_name 'CiJob'
authorize :read_commit_status
- field :id, GraphQL::ID_TYPE, null: false,
+ connection_type_class(Types::CountableConnectionType)
+
+ field :id, ::Types::GlobalIDType[::CommitStatus].as('JobID'), null: true,
description: 'ID of the job.'
field :pipeline, Types::Ci::PipelineType, null: true,
description: 'Pipeline the job belongs to.'
@@ -14,16 +16,33 @@ module Types
description: 'Name of the job.'
field :needs, BuildNeedType.connection_type, null: true,
description: 'References to builds that must complete before the jobs run.'
- field :detailed_status, Types::Ci::DetailedStatusType, null: true,
- description: 'Detailed status of the job.'
+ field :status,
+ type: ::Types::Ci::JobStatusEnum,
+ null: true,
+ description: "Status of the job."
+ field :stage, Types::Ci::StageType, null: true,
+ description: 'Stage of the job.'
+ field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Whether this job is allowed to fail.'
+ field :duration, GraphQL::INT_TYPE, null: true,
+ description: 'Duration of the job in seconds.'
+
+ # Life-cycle timestamps:
+ field :created_at, Types::TimeType, null: false,
+ description: "When the job was created."
+ field :queued_at, Types::TimeType, null: true,
+ description: 'When the job was enqueued and marked as pending.'
+ field :started_at, Types::TimeType, null: true,
+ description: 'When the job was started.'
+ field :finished_at, Types::TimeType, null: true,
+ description: 'When a job has finished running.'
field :scheduled_at, Types::TimeType, null: true,
description: 'Schedule for the build.'
+
+ field :detailed_status, Types::Ci::DetailedStatusType, null: true,
+ description: 'Detailed status of the job.'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
description: 'Artifacts generated by the job.'
- field :finished_at, Types::TimeType, null: true,
- description: 'When a job has finished running.'
- field :duration, GraphQL::INT_TYPE, null: true,
- description: 'Duration of the job in seconds.'
field :short_sha, type: GraphQL::STRING_TYPE, null: false,
description: 'Short SHA1 ID of the commit.'
@@ -40,6 +59,30 @@ module Types
object.job_artifacts
end
end
+
+ def stage
+ ::Gitlab::Graphql::Lazy.with_value(pipeline) do |pl|
+ BatchLoader::GraphQL.for([pl, object.stage]).batch do |ids, loader|
+ by_pipeline = ids
+ .group_by(&:first)
+ .transform_values { |grp| grp.map(&:second) }
+
+ by_pipeline.each do |p, names|
+ p.stages.by_name(names).each { |s| loader.call([p, s.name], s) }
+ end
+ end
+ end
+ end
+
+ # This class is a secret union!
+ # TODO: turn this into an actual union, so that fields can be referenced safely!
+ def id
+ return unless object.id.present?
+
+ model_name = object.type || ::CommitStatus.name
+ id = object.id
+ Gitlab::GlobalId.build(model_name: model_name, id: id)
+ end
end
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 49be200a788..aaaa5041c5d 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -81,6 +81,20 @@ module Types
description: 'Jobs belonging to the pipeline.',
resolver: ::Resolvers::Ci::JobsResolver
+ field :job,
+ type: ::Types::Ci::JobType,
+ null: true,
+ description: 'A specific job in this pipeline, either by name or ID.' do
+ argument :id,
+ type: ::Types::GlobalIDType[::CommitStatus],
+ required: false,
+ description: 'ID of the job.'
+ argument :name,
+ type: ::GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Name of the job.'
+ end
+
field :source_job, Types::Ci::JobType, null: true,
description: 'Job where pipeline was triggered from.'
@@ -105,7 +119,7 @@ module Types
description: 'Indicates if the pipeline is active.'
def detailed_status
- object.detailed_status(context[:current_user])
+ object.detailed_status(current_user)
end
def user
@@ -119,6 +133,19 @@ module Types
def path
::Gitlab::Routing.url_helpers.project_pipeline_path(object.project, object)
end
+
+ def job(id: nil, name: nil)
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'One of id or name is required' unless id || name
+
+ if id
+ id = ::Types::GlobalIDType[::CommitStatus].coerce_isolated_input(id) if id
+ pipeline.statuses.id_in(id.model_id)
+ else
+ pipeline.statuses.by_name(name)
+ end.take # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ alias_method :pipeline, :object
end
end
end
diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb
index 836f2430890..04a7698e323 100644
--- a/app/graphql/types/ci/stage_type.rb
+++ b/app/graphql/types/ci/stage_type.rb
@@ -12,10 +12,13 @@ module Types
extras: [:lookahead],
description: 'Group of jobs for the stage.'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
- description: 'Detailed status of the stage.'
+ description: 'Detailed status of the stage.'
+ field :jobs, Ci::JobType.connection_type, null: true,
+ description: 'Jobs for the stage.',
+ method: 'latest_statuses'
def detailed_status
- object.detailed_status(context[:current_user])
+ object.detailed_status(current_user)
end
# Issues one query per pipeline
diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb
index 750bd1bfe8d..2b10edce108 100644
--- a/app/graphql/types/global_id_type.rb
+++ b/app/graphql/types/global_id_type.rb
@@ -67,6 +67,17 @@ module Types
graphql_name
end
+ define_singleton_method(:as) do |new_name|
+ if @renamed && graphql_name != new_name
+ raise "Conflicting names for ID of #{model_class.name}: " \
+ "#{graphql_name} and #{new_name}"
+ end
+
+ @renamed = true
+ graphql_name(new_name)
+ self
+ end
+
define_singleton_method(:coerce_result) do |gid, ctx|
global_id = ::Gitlab::GlobalId.as_global_id(gid, model_name: model_class.name)
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index c9ae185583d..1bbace791ed 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -52,9 +52,9 @@ class ApplicationRecord < ActiveRecord::Base
# Start a new transaction with a shorter-than-usual statement timeout. This is
# currently one third of the default 15-second timeout
- def self.with_fast_statement_timeout
+ def self.with_fast_read_statement_timeout(timeout_ms = 5000)
transaction(requires_new: true) do
- connection.exec_query("SET LOCAL statement_timeout = 5000")
+ connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
yield
end
@@ -79,3 +79,5 @@ class ApplicationRecord < ActiveRecord::Base
enum(enum_mod.key => values)
end
end
+
+ApplicationRecord.prepend_if_ee('EE::ApplicationRecordHelpers')
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index 4ba09fd8152..47b91fcf2ce 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -22,6 +22,13 @@ module Ci
@jobs = jobs
end
+ def ==(other)
+ other.present? && other.is_a?(self.class) &&
+ project == other.project &&
+ stage == other.stage &&
+ name == other.name
+ end
+
def status
strong_memoize(:status) do
status_struct.status
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index c49d088fe6b..e2103183247 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -20,6 +20,7 @@ module Ci
scope :ordered, -> { order(position: :asc) }
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
+ scope :by_name, ->(names) { where(name: names) }
with_options unless: :importing? do
validates :project, presence: true
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index 87c0933747d..4b41231ba20 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -1,5 +1,5 @@
.form-actions
- = button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-success js-commit-button qa-commit-button'
+ = button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button'
= link_to 'Cancel', cancel_path,
class: 'gl-button btn btn-default btn-cancel', data: {confirm: leave_edit_message}
diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml
index 8e4e5ca93e0..ded43a34b48 100644
--- a/app/views/projects/_customize_workflow.html.haml
+++ b/app/views/projects/_customize_workflow.html.haml
@@ -5,4 +5,4 @@
%p
Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production!
- if can?(current_user, :admin_project, @project)
- = link_to "Get started", edit_project_path(@project), class: "gl-button btn btn-success"
+ = link_to "Get started", edit_project_path(@project), class: "gl-button btn btn-confirm"
diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml
index 59c9c279a39..9888ce417f8 100644
--- a/app/views/projects/_fork_suggestion.html.haml
+++ b/app/views/projects/_fork_suggestion.html.haml
@@ -5,6 +5,6 @@
edit
files in this project directly. Please fork this project,
make your changes there, and submit a merge request.
- = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button gl-button btn btn-grouped btn-inverted btn-success'
+ = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button gl-button btn btn-grouped btn-confirm-secondary'
%button.js-cancel-fork-suggestion-button.gl-button.btn.btn-grouped{ type: 'button' }
Cancel
diff --git a/app/views/projects/_invite_members.html.haml b/app/views/projects/_invite_members.html.haml
index ef030cabc93..e3a512d6451 100644
--- a/app/views/projects/_invite_members.html.haml
+++ b/app/views/projects/_invite_members.html.haml
@@ -4,5 +4,5 @@
= s_('InviteMember|Invite your team')
%p= s_('InviteMember|Add members to this project and start collaborating with your team.')
= link_to s_('InviteMember|Invite members'), project_project_members_path(@project, sort: :access_level_desc),
- class: 'gl-button btn btn-success gl-mb-8 gl-xs-w-full',
+ class: 'gl-button btn btn-confirm gl-mb-8 gl-xs-w-full',
data: { track_event: 'click_button', track_label: 'invite_members_empty_project' }
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 8b1bf37ff10..f6adb213916 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -62,5 +62,5 @@
.option-description
= s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.')
-= f.submit _('Create project'), class: "btn gl-button btn-success", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
+= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel", track_value: "" }
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index da3133dfe15..85a53edc160 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -24,4 +24,4 @@
distributed with computer software, forming part of its documentation.
GitLab will render it here instead of this message.
%p
- = link_to "Add Readme", @project.add_readme_path, class: 'btn btn-success'
+ = link_to "Add Readme", @project.add_readme_path, class: 'gl-button btn btn-confirm'
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 12144f908dd..ecaf3467cd2 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -23,7 +23,7 @@
.js-project-permissions-form
- if show_visibility_confirm_modal?(@project)
= render "visibility_modal"
- = f.submit _('Save changes'), class: "btn gl-button btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
+ = f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
@@ -37,7 +37,7 @@
= form_for @project, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/merge_request_settings', form: f
- = f.submit _('Save changes'), class: "btn gl-button btn-success rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
+ = f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
diff --git a/app/views/projects/settings/operations/_tracing.html.haml b/app/views/projects/settings/operations/_tracing.html.haml
index 8bc9a383a73..218097cd163 100644
--- a/app/views/projects/settings/operations/_tracing.html.haml
+++ b/app/views/projects/settings/operations/_tracing.html.haml
@@ -17,17 +17,18 @@
- tracing_link = link_to project_tracing_path(@project) do
%span
= _('Tracing')
- = _("To open Jaeger and easily view tracing from GitLab, link the %{link} page to your server").html_safe % { link: tracing_link }
+ = _("To open Jaeger from GitLab to view tracing from the %{link} page, add a URL to your Jaeger server.").html_safe % { link: tracing_link }
+ = link_to _('Learn more.'), help_page_path('operations/tracing'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= form_for @project, url: project_settings_operations_path(@project), method: :patch do |f|
= form_errors(@project)
.form-group
= f.fields_for :tracing_setting_attributes, setting do |form|
= form.label :external_url, _('Jaeger URL'), class: 'label-bold'
- = form.url_field :external_url, class: 'form-control gl-form-input', placeholder: 'e.g. https://jaeger.mycompany.com'
+ = form.url_field :external_url, class: 'form-control gl-form-input', placeholder: 'https://jaeger.example.com'
%p.form-text.text-muted
- - jaeger_help_url = "https://www.jaegertracing.io/docs/1.7/getting-started/"
+ - jaeger_help_url = "https://www.jaegertracing.io/docs/getting-started/"
- link_start_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jaeger_help_url }
- link_end_tag = "#{sprite_icon('external-link', css_class: 'ml-1 vertical-align-middle')}</a>".html_safe
- = _("For more information, please review %{link_start_tag}Jaeger's configuration doc%{link_end_tag}").html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
+ = _("Learn more about %{link_start_tag}Jaeger configuration%{link_end_tag}.").html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index a505b34f46c..f53b2051835 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -9,7 +9,7 @@
- if new_project_snippet_link.present?
.nav-controls
- = link_to _("New snippet"), new_project_snippet_link, class: "btn btn-success", title: _("New snippet")
+ = link_to _("New snippet"), new_project_snippet_link, class: "gl-button btn btn-confirm", title: _("New snippet")
= render 'shared/snippets/list'
- else
diff --git a/app/views/projects/tracings/show.html.haml b/app/views/projects/tracings/show.html.haml
index 8c9bffc81bf..21c1d02d92e 100644
--- a/app/views/projects/tracings/show.html.haml
+++ b/app/views/projects/tracings/show.html.haml
@@ -24,10 +24,10 @@
.text-content
%h4.text-left= _('Troubleshoot and monitor your application with tracing')
%p
- - jaeger_help_url = "https://www.jaegertracing.io/docs/1.7/getting-started/"
+ - jaeger_help_url = "https://www.jaegertracing.io/docs/getting-started/"
- link_start_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jaeger_help_url }
- link_end_tag = "#{sprite_icon('external-link', css_class: 'ml-1 vertical-align-middle')}</a>".html_safe
- = _('To get started, link this page to your Jaeger server, or find out how to %{link_start_tag}install Jaeger%{link_end_tag}').html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
+ = _('Add a Jaeger URL to replace this page with a link to your Jaeger server. You first need to %{link_start_tag}install Jaeger%{link_end_tag}.').html_safe % { link_start_tag: link_start_tag, link_end_tag: link_end_tag }
.text-center
= render 'tracing_button'
diff --git a/changelogs/unreleased/ajk-graphql-ci-jobs.yml b/changelogs/unreleased/ajk-graphql-ci-jobs.yml
new file mode 100644
index 00000000000..396e06ae0ed
--- /dev/null
+++ b/changelogs/unreleased/ajk-graphql-ci-jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Adds CI pipeline and job features to GraphQL API
+merge_request: 44703
+author:
+type: changed
diff --git a/changelogs/unreleased/btn-confirm-projects-snippets.yml b/changelogs/unreleased/btn-confirm-projects-snippets.yml
new file mode 100644
index 00000000000..1766609abc5
--- /dev/null
+++ b/changelogs/unreleased/btn-confirm-projects-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Move from btn-success to btn-confirm in projects/snippets directory
+merge_request: 56939
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/btn-confirm-projects.yml b/changelogs/unreleased/btn-confirm-projects.yml
new file mode 100644
index 00000000000..983f72531be
--- /dev/null
+++ b/changelogs/unreleased/btn-confirm-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Move from btn-success to btn-confirm in projects directory
+merge_request: 56943
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/cngo-add-gl-toggle-labels.yml b/changelogs/unreleased/cngo-add-gl-toggle-labels.yml
new file mode 100644
index 00000000000..d553890901a
--- /dev/null
+++ b/changelogs/unreleased/cngo-add-gl-toggle-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Add labels to UI toggles
+merge_request: 56848
+author:
+type: fixed
diff --git a/changelogs/unreleased/eread-refactor-jaeger-tracing-configuration-ui.yml b/changelogs/unreleased/eread-refactor-jaeger-tracing-configuration-ui.yml
new file mode 100644
index 00000000000..cf891e28859
--- /dev/null
+++ b/changelogs/unreleased/eread-refactor-jaeger-tracing-configuration-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor docs and UI for Jaeger tracing
+merge_request: 56819
+author:
+type: other
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 3f8aca2f1ff..661fa67220d 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -526,6 +526,9 @@ If you need to manually remove job artifacts associated with multiple jobs while
- `3.months.ago`
- `1.year.ago`
+ `erase_erasable_artifacts!` is a synchronous method, and upon execution, the artifacts are removed immediately.
+ They are not scheduled via some background queue.
+
#### Delete job artifacts and logs from jobs completed before a specific date
WARNING:
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e54d5d80d3c..96cea368b04 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1194,16 +1194,22 @@ An edge in a connection.
| Field | Type | Description |
| ----- | ---- | ----------- |
+| `allowFailure` | [`Boolean!`](#boolean) | Whether this job is allowed to fail. |
| `artifacts` | [`CiJobArtifactConnection`](#cijobartifactconnection) | Artifacts generated by the job. |
+| `createdAt` | [`Time!`](#time) | When the job was created. |
| `detailedStatus` | [`DetailedStatus`](#detailedstatus) | Detailed status of the job. |
| `duration` | [`Int`](#int) | Duration of the job in seconds. |
| `finishedAt` | [`Time`](#time) | When a job has finished running. |
-| `id` | [`ID!`](#id) | ID of the job. |
+| `id` | [`JobID`](#jobid) | ID of the job. |
| `name` | [`String`](#string) | Name of the job. |
| `needs` | [`CiBuildNeedConnection`](#cibuildneedconnection) | References to builds that must complete before the jobs run. |
| `pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. |
+| `queuedAt` | [`Time`](#time) | When the job was enqueued and marked as pending. |
| `scheduledAt` | [`Time`](#time) | Schedule for the build. |
| `shortSha` | [`String!`](#string) | Short SHA1 ID of the commit. |
+| `stage` | [`CiStage`](#cistage) | Stage of the job. |
+| `startedAt` | [`Time`](#time) | When the job was started. |
+| `status` | [`CiJobStatus`](#cijobstatus) | Status of the job. |
### `CiJobArtifact`
@@ -1237,6 +1243,7 @@ The connection type for CiJob.
| Field | Type | Description |
| ----- | ---- | ----------- |
+| `count` | [`Int!`](#int) | Total count of collection. |
| `edges` | [`[CiJobEdge]`](#cijobedge) | A list of edges. |
| `nodes` | [`[CiJob]`](#cijob) | A list of nodes. |
| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
@@ -1256,6 +1263,7 @@ An edge in a connection.
| ----- | ---- | ----------- |
| `detailedStatus` | [`DetailedStatus`](#detailedstatus) | Detailed status of the stage. |
| `groups` | [`CiGroupConnection`](#cigroupconnection) | Group of jobs for the stage. |
+| `jobs` | [`CiJobConnection`](#cijobconnection) | Jobs for the stage. |
| `name` | [`String`](#string) | Name of the stage. |
### `CiStageConnection`
@@ -4582,6 +4590,7 @@ Information about pagination in a connection.
| `finishedAt` | [`Time`](#time) | Timestamp of the pipeline's completion. |
| `id` | [`ID!`](#id) | ID of the pipeline. |
| `iid` | [`String!`](#string) | Internal ID of the pipeline. |
+| `job` | [`CiJob`](#cijob) | A specific job in this pipeline, either by name or ID. |
| `jobs` | [`CiJobConnection`](#cijobconnection) | Jobs belonging to the pipeline. |
| `path` | [`String`](#string) | Relative path to the pipeline's page. |
| `project` | [`Project`](#project) | Project the pipeline belongs to. |
@@ -7274,6 +7283,22 @@ Values for YAML processor result.
| `INVALID` | The configuration file is not valid. |
| `VALID` | The configuration file is valid. |
+### `CiJobStatus`
+
+| Value | Description |
+| ----- | ----------- |
+| `CANCELED` | A job that is canceled. |
+| `CREATED` | A job that is created. |
+| `FAILED` | A job that is failed. |
+| `MANUAL` | A job that is manual. |
+| `PENDING` | A job that is pending. |
+| `PREPARING` | A job that is preparing. |
+| `RUNNING` | A job that is running. |
+| `SCHEDULED` | A job that is scheduled. |
+| `SKIPPED` | A job that is skipped. |
+| `SUCCESS` | A job that is success. |
+| `WAITING_FOR_RESOURCE` | A job that is waiting for resource. |
+
### `CommitActionMode`
Mode of a commit action.
@@ -8472,6 +8497,12 @@ An example `IterationsCadenceID` is: `"gid://gitlab/Iterations::Cadence/1"`.
Represents untyped JSON.
+### `JobID`
+
+A `CommitStatusID` is a global ID. It is encoded as a string.
+
+An example `CommitStatusID` is: `"gid://gitlab/CommitStatus/1"`.
+
### `JsonString`
JSON object as raw string.
diff --git a/doc/operations/tracing.md b/doc/operations/tracing.md
index bf9e0d2390e..a6647641527 100644
--- a/doc/operations/tracing.md
+++ b/doc/operations/tracing.md
@@ -9,27 +9,26 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7903) in GitLab Ultimate 11.5.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42645) to GitLab Free in 13.5.
-Tracing provides insight into the performance and health of a deployed application,
-tracking each function or microservice which handles a given request.
+Tracing provides insight into the performance and health of a deployed application, tracking each
+function or microservice that handles a given request. Tracing makes it easy to understand the
+end-to-end flow of a request, regardless of whether you are using a monolithic or distributed
+system.
-This makes it easy to
-understand the end-to-end flow of a request, regardless of whether you are using a monolithic or distributed system.
+## Install Jaeger
-## Jaeger tracing
-
-[Jaeger](https://www.jaegertracing.io/) is an open source, end-to-end distributed
-tracing system used for monitoring and troubleshooting microservices-based distributed
-systems.
+[Jaeger](https://www.jaegertracing.io/) is an open source, end-to-end distributed tracing system
+used for monitoring and troubleshooting microservices-based distributed systems. To learn more about
+installing Jaeger, read the official
+[Getting Started documentation](https://www.jaegertracing.io/docs/latest/getting-started/).
-### Deploying Jaeger
+See also:
-To learn more about deploying Jaeger, read the official
-[Getting Started documentation](https://www.jaegertracing.io/docs/latest/getting-started/).
-There is an easy to use [all-in-one Docker image](https://www.jaegertracing.io/docs/latest/getting-started/#AllinoneDockerimage),
-as well as deployment options for [Kubernetes](https://github.com/jaegertracing/jaeger-kubernetes)
-and [OpenShift](https://github.com/jaegertracing/jaeger-openshift).
+- An [all-in-one Docker image](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one).
+- Deployment options for:
+ - [Kubernetes](https://github.com/jaegertracing/jaeger-kubernetes).
+ - [OpenShift](https://github.com/jaegertracing/jaeger-openshift).
-### Enabling Jaeger
+## Link to Jaeger
GitLab provides an easy way to open the Jaeger UI from within your project:
@@ -37,5 +36,5 @@ GitLab provides an easy way to open the Jaeger UI from within your project:
[client libraries](https://www.jaegertracing.io/docs/latest/client-libraries/).
1. Navigate to your project's **Settings > Operations** and provide the Jaeger URL.
1. Click **Save changes** for the changes to take effect.
-1. You can now visit **Operations > Tracing** in your project's sidebar and
- GitLab redirects you to the configured Jaeger URL.
+1. You can now visit **Operations > Tracing** in your project's sidebar and GitLab redirects you to
+ the configured Jaeger URL.
diff --git a/lib/api/helpers/graphql_helpers.rb b/lib/api/helpers/graphql_helpers.rb
index 3ddef0c16b3..4f7f85bd69d 100644
--- a/lib/api/helpers/graphql_helpers.rb
+++ b/lib/api/helpers/graphql_helpers.rb
@@ -6,8 +6,8 @@ module API
# against the graphql API. Helper code for the graphql server implementation
# should be in app/graphql/ or lib/gitlab/graphql/
module GraphqlHelpers
- def run_graphql!(query:, context: {}, transform: nil)
- result = GitlabSchema.execute(query, context: context)
+ def run_graphql!(query:, context: {}, variables: nil, transform: nil)
+ result = GitlabSchema.execute(query, variables: variables, context: context)
if transform
transform.call(result)
diff --git a/lib/gitlab/issuables_count_for_state.rb b/lib/gitlab/issuables_count_for_state.rb
index 945ab7f40c2..6b33b60e850 100644
--- a/lib/gitlab/issuables_count_for_state.rb
+++ b/lib/gitlab/issuables_count_for_state.rb
@@ -78,7 +78,7 @@ module Gitlab
# to perform the calculation more efficiently. Until then, use a shorter
# timeout and return -1 as a sentinel value if it is triggered
begin
- ApplicationRecord.with_fast_statement_timeout do
+ ApplicationRecord.with_fast_read_statement_timeout do
finder.count_by_state
end
rescue ActiveRecord::QueryCanceled => err
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 71f859ec822..4e4e648e61a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1820,6 +1820,9 @@ msgstr ""
msgid "Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab."
msgstr ""
+msgid "Add a Jaeger URL to replace this page with a link to your Jaeger server. You first need to %{link_start_tag}install Jaeger%{link_end_tag}."
+msgstr ""
+
msgid "Add a bullet list"
msgstr ""
@@ -13488,9 +13491,6 @@ msgstr ""
msgid "For more information, go to the "
msgstr ""
-msgid "For more information, please review %{link_start_tag}Jaeger's configuration doc%{link_end_tag}"
-msgstr ""
-
msgid "For more information, see the File Hooks documentation."
msgstr ""
@@ -17951,6 +17951,9 @@ msgstr ""
msgid "Learn more"
msgstr ""
+msgid "Learn more about %{link_start_tag}Jaeger configuration%{link_end_tag}."
+msgstr ""
+
msgid "Learn more about %{username}"
msgstr ""
@@ -21795,6 +21798,9 @@ msgstr ""
msgid "PackageRegistry|Add composer registry"
msgstr ""
+msgid "PackageRegistry|Allow duplicates"
+msgstr ""
+
msgid "PackageRegistry|An error occurred while saving the settings"
msgstr ""
@@ -31680,9 +31686,6 @@ msgstr ""
msgid "To get started, click the link below to confirm your account."
msgstr ""
-msgid "To get started, link this page to your Jaeger server, or find out how to %{link_start_tag}install Jaeger%{link_end_tag}"
-msgstr ""
-
msgid "To get started, please enter your Gitea Host URL and a %{link_to_personal_token}."
msgstr ""
@@ -31710,7 +31713,7 @@ msgstr ""
msgid "To only use CI/CD features for an external repository, choose %{strong_open}CI/CD for external repo%{strong_close}."
msgstr ""
-msgid "To open Jaeger and easily view tracing from GitLab, link the %{link} page to your server"
+msgid "To open Jaeger from GitLab to view tracing from the %{link} page, add a URL to your Jaeger server."
msgstr ""
msgid "To personalize your GitLab experience, we'd like to know a bit more about you. We won't share this information with anyone."
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 886be520668..b06d581d2c0 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -30,6 +30,21 @@ FactoryBot.define do
yaml_variables { nil }
end
+ trait :unique_name do
+ name { generate(:job_name) }
+ end
+
+ trait :dependent do
+ transient do
+ sequence(:needed_name) { |n| "dependency #{n}" }
+ needed { association(:ci_build, name: needed_name, pipeline: pipeline) }
+ end
+
+ after(:create) do |build, evaluator|
+ build.needs << create(:ci_build_need, build: build, name: evaluator.needed.name)
+ end
+ end
+
trait :started do
started_at { 'Di 29. Okt 09:51:28 CET 2013' }
end
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index f9952cd9966..b276e6f8cfc 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -19,4 +19,5 @@ FactoryBot.define do
sequence(:wip_title) { |n| "WIP: #{n}" }
sequence(:jira_title) { |n| "[PROJ-#{n}]: fix bug" }
sequence(:jira_branch) { |n| "feature/PROJ-#{n}" }
+ sequence(:job_name) { |n| "job #{n}" }
end
diff --git a/spec/frontend/__mocks__/vue/index.js b/spec/frontend/__mocks__/vue/index.js
new file mode 100644
index 00000000000..52a5c6c5fcd
--- /dev/null
+++ b/spec/frontend/__mocks__/vue/index.js
@@ -0,0 +1,7 @@
+import Vue from 'vue';
+
+Vue.config.productionTip = false;
+Vue.config.devtools = false;
+
+export default Vue;
+export * from 'vue';
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
index 2e253d24125..635964b6b4a 100644
--- a/spec/frontend/boards/components/issue_time_estimate_spec.js
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { config as vueConfig } from 'vue';
+import Vue from 'vue';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
describe('Issue Time Estimate component', () => {
@@ -34,10 +34,10 @@ describe('Issue Time Estimate component', () => {
try {
// This will raise props validating warning by Vue, silencing it
- vueConfig.silent = true;
+ Vue.config.silent = true;
await wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
} finally {
- vueConfig.silent = false;
+ Vue.config.silent = false;
}
expect(alertSpy).not.toHaveBeenCalled();
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index a05e23a4250..00d557c11cf 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -123,6 +123,10 @@ describe('feature flag form', () => {
});
});
+ it('has label', () => {
+ expect(findGlToggle().props('label')).toBe(Form.i18n.statusLabel);
+ });
+
it('should be disabled if the feature flag is not active', (done) => {
wrapper.setProps({ active: false });
wrapper.vm.$nextTick(() => {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
index 2433c50ff24..859d3587223 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js
@@ -59,7 +59,10 @@ describe('Maven Settings', () => {
mountComponent();
expect(findToggle().exists()).toBe(true);
- expect(findToggle().props('value')).toBe(defaultProps.mavenDuplicatesAllowed);
+ expect(findToggle().props()).toMatchObject({
+ label: component.i18n.MAVEN_TOGGLE_LABEL,
+ value: defaultProps.mavenDuplicatesAllowed,
+ });
});
it('toggle emits an update event', () => {
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 0934dde8230..878721666ff 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -46,6 +46,7 @@ const defaultProps = {
pagesHelpPath: '/help/user/project/pages/introduction#gitlab-pages-access-control',
packagesAvailable: false,
packagesHelpPath: '/help/user/packages/index',
+ requestCveAvailable: true,
};
describe('Settings Panel', () => {
@@ -76,6 +77,7 @@ describe('Settings Panel', () => {
const findRepositoryFeatureSetting = () =>
findRepositoryFeatureProjectRow().find(projectFeatureSetting);
const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' });
+ const findIssuesSettingsRow = () => wrapper.find({ ref: 'issues-settings' });
const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' });
const findProjectVisibilityLevelInput = () => wrapper.find('[name="project[visibility_level]"]');
const findRequestAccessEnabledInput = () =>
@@ -174,6 +176,16 @@ describe('Settings Panel', () => {
});
});
+ describe('Issues settings', () => {
+ it('has label for CVE request toggle', () => {
+ wrapper = mountComponent();
+
+ expect(findIssuesSettingsRow().findComponent(GlToggle).props('label')).toBe(
+ settingsPanel.i18n.cve_request_toggle_label,
+ );
+ });
+ });
+
describe('Repository', () => {
it('should set the repository help text when the visibility level is set to private', () => {
wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } });
@@ -304,6 +316,17 @@ describe('Settings Panel', () => {
expect(findContainerRegistryEnabledInput().props('disabled')).toBe(true);
});
+
+ it('has label for the toggle', () => {
+ wrapper = mountComponent({
+ currentSettings: { visibilityLevel: visibilityOptions.PUBLIC },
+ registryAvailable: true,
+ });
+
+ expect(findContainerRegistrySettings().findComponent(GlToggle).props('label')).toBe(
+ settingsPanel.i18n.containerRegistryLabel,
+ );
+ });
});
describe('Git Large File Storage', () => {
@@ -342,6 +365,15 @@ describe('Settings Panel', () => {
expect(findLFSFeatureToggle().props('disabled')).toBe(true);
});
+ it('has label for toggle', () => {
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ lfsAvailable: true,
+ });
+
+ expect(findLFSFeatureToggle().props('label')).toBe(settingsPanel.i18n.lfsLabel);
+ });
+
it('should not change lfsEnabled when disabling the repository', async () => {
// mount over shallowMount, because we are aiming to test rendered state of toggle
wrapper = mountComponent({ currentSettings: { lfsEnabled: true } }, mount);
@@ -432,6 +464,17 @@ describe('Settings Panel', () => {
expect(findPackagesEnabledInput().props('disabled')).toBe(true);
});
+
+ it('has label for toggle', () => {
+ wrapper = mountComponent({
+ currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
+ packagesAvailable: true,
+ });
+
+ expect(findPackagesEnabledInput().findComponent(GlToggle).props('label')).toBe(
+ settingsPanel.i18n.packagesLabel,
+ );
+ });
});
describe('Pages', () => {
diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js
index 305dc557b39..40cfd785a20 100644
--- a/spec/frontend/pipelines/nav_controls_spec.js
+++ b/spec/frontend/pipelines/nav_controls_spec.js
@@ -1,17 +1,22 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import navControlsComp from '~/pipelines/components/pipelines_list/nav_controls.vue';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
describe('Pipelines Nav Controls', () => {
- let NavControlsComponent;
- let component;
+ let wrapper;
- beforeEach(() => {
- NavControlsComponent = Vue.extend(navControlsComp);
- });
+ const createComponent = (props) => {
+ wrapper = shallowMount(NavControls, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findRunPipeline = () => wrapper.find('.js-run-pipeline');
afterEach(() => {
- component.$destroy();
+ wrapper.destroy();
});
it('should render link to create a new pipeline', () => {
@@ -21,12 +26,11 @@ describe('Pipelines Nav Controls', () => {
resetCachePath: 'foo',
};
- component = mountComponent(NavControlsComponent, mockData);
+ createComponent(mockData);
- expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline');
- expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
- mockData.newPipelinePath,
- );
+ const runPipeline = findRunPipeline();
+ expect(runPipeline.text()).toContain('Run Pipeline');
+ expect(runPipeline.attributes('href')).toBe(mockData.newPipelinePath);
});
it('should not render link to create pipeline if no path is provided', () => {
@@ -36,9 +40,9 @@ describe('Pipelines Nav Controls', () => {
resetCachePath: 'foo',
};
- component = mountComponent(NavControlsComponent, mockData);
+ createComponent(mockData);
- expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null);
+ expect(findRunPipeline().exists()).toBe(false);
});
it('should render link for CI lint', () => {
@@ -49,12 +53,10 @@ describe('Pipelines Nav Controls', () => {
resetCachePath: 'foo',
};
- component = mountComponent(NavControlsComponent, mockData);
+ createComponent(mockData);
- expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint');
- expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(
- mockData.ciLintPath,
- );
+ expect(wrapper.find('.js-ci-lint').text().trim()).toContain('CI Lint');
+ expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(mockData.ciLintPath);
});
describe('Reset Runners Cache', () => {
@@ -64,22 +66,20 @@ describe('Pipelines Nav Controls', () => {
ciLintPath: 'foo',
resetCachePath: 'foo',
};
-
- component = mountComponent(NavControlsComponent, mockData);
+ createComponent(mockData);
});
it('should render button for resetting runner caches', () => {
- expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain(
- 'Clear Runner Caches',
- );
+ expect(wrapper.find('.js-clear-cache').text().trim()).toContain('Clear Runner Caches');
});
- it('should emit postAction event when reset runner cache button is clicked', () => {
- jest.spyOn(component, '$emit').mockImplementation(() => {});
+ it('should emit postAction event when reset runner cache button is clicked', async () => {
+ jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
- component.$el.querySelector('.js-clear-cache').click();
+ wrapper.find('.js-clear-cache').vm.$emit('click');
+ await nextTick();
- expect(component.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo');
});
});
});
diff --git a/spec/graphql/types/ci/job_status_enum_spec.rb b/spec/graphql/types/ci/job_status_enum_spec.rb
new file mode 100644
index 00000000000..e8a1a2e0aa8
--- /dev/null
+++ b/spec/graphql/types/ci/job_status_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiJobStatus'] do
+ it 'exposes all job status values' do
+ expect(described_class.values.values).to contain_exactly(
+ *::Ci::HasStatus::AVAILABLE_STATUSES.map do |status|
+ have_attributes(value: status, graphql_name: status.upcase)
+ end
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index c54137a1c3e..4654b55eea5 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -8,16 +8,23 @@ RSpec.describe Types::Ci::JobType do
it 'exposes the expected fields' do
expected_fields = %i[
+ allow_failure
+ artifacts
+ created_at
+ detailedStatus
+ duration
+ finished_at
id
- shortSha
- pipeline
name
needs
- detailedStatus
+ pipeline
+ queued_at
scheduledAt
- artifacts
- finished_at
- duration
+ scheduledAt
+ shortSha
+ stage
+ started_at
+ status
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index e0e84a1b635..13021c00aec 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Types::Ci::PipelineType do
expected_fields = %w[
id iid sha before_sha status detailed_status config_source duration
coverage created_at updated_at started_at finished_at committed_at
- stages user retryable cancelable jobs source_job downstream
+ stages user retryable cancelable jobs job source_job downstream
upstream path project active user_permissions warnings commit_path
]
diff --git a/spec/graphql/types/ci/stage_type_spec.rb b/spec/graphql/types/ci/stage_type_spec.rb
index 9a8d4fa96a3..cb8c1cb02cd 100644
--- a/spec/graphql/types/ci/stage_type_spec.rb
+++ b/spec/graphql/types/ci/stage_type_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Types::Ci::StageType do
name
groups
detailedStatus
+ jobs
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index 107f707ccd9..7e6ac351e68 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -100,4 +100,33 @@ RSpec.describe ApplicationRecord do
expect(User.where_exists(User.limit(1))).to eq([user])
end
end
+
+ describe '.with_fast_read_statement_timeout' do
+ context 'when the query runs faster than configured timeout' do
+ it 'executes the query without error' do
+ result = nil
+
+ expect do
+ described_class.with_fast_read_statement_timeout(100) do
+ result = described_class.connection.exec_query('SELECT 1')
+ end
+ end.not_to raise_error
+
+ expect(result).not_to be_nil
+ end
+ end
+
+ # This query hangs for 10ms and then gets cancelled. As there is no
+ # other way to test the timeout for sure, 10ms of waiting seems to be
+ # reasonable!
+ context 'when the query runs longer than configured timeout' do
+ it 'cancels the query and raises an exception' do
+ expect do
+ described_class.with_fast_read_statement_timeout(10) do
+ described_class.connection.exec_query('SELECT pg_sleep(0.1)')
+ end
+ end.to raise_error(ActiveRecord::QueryCanceled)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 0afc491dc73..677e4b34ecd 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -27,6 +27,18 @@ RSpec.describe Ci::Stage, :models do
end
end
+ describe '.by_name' do
+ it 'finds stages by name' do
+ a = create(:ci_stage_entity, name: 'a')
+ b = create(:ci_stage_entity, name: 'b')
+ c = create(:ci_stage_entity, name: 'c')
+
+ expect(described_class.by_name('a')).to contain_exactly(a)
+ expect(described_class.by_name('b')).to contain_exactly(b)
+ expect(described_class.by_name(%w[a c])).to contain_exactly(a, c)
+ end
+ end
+
describe '#status' do
context 'when stage is pending' do
let(:stage) { create(:ci_stage_entity, status: 'pending') }
diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb
index 9e81358a152..4c063d359a5 100644
--- a/spec/requests/api/graphql/ci/groups_spec.rb
+++ b/spec/requests/api/graphql/ci/groups_spec.rb
@@ -4,10 +4,14 @@ require 'spec_helper'
RSpec.describe 'Query.project.pipeline.stages.groups' do
include GraphqlHelpers
- let(:project) { create(:project, :repository, :public) }
- let(:user) { create(:user) }
- let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
- let(:group_graphql_data) { graphql_data.dig('project', 'pipeline', 'stages', 'nodes', 0, 'groups', 'nodes') }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let(:group_graphql_data) { graphql_data_at(:project, :pipeline, :stages, :nodes, 0, :groups, :nodes) }
+
+ let_it_be(:job_a) { create(:commit_status, pipeline: pipeline, name: 'rspec 0 2') }
+ let_it_be(:job_b) { create(:ci_build, pipeline: pipeline, name: 'rspec 0 1') }
+ let_it_be(:job_c) { create(:ci_bridge, pipeline: pipeline, name: 'spinach 0 1') }
let(:params) { {} }
@@ -38,18 +42,15 @@ RSpec.describe 'Query.project.pipeline.stages.groups' do
end
before do
- create(:commit_status, pipeline: pipeline, name: 'rspec 0 2')
- create(:commit_status, pipeline: pipeline, name: 'rspec 0 1')
- create(:commit_status, pipeline: pipeline, name: 'spinach 0 1')
post_graphql(query, current_user: user)
end
it_behaves_like 'a working graphql query'
it 'returns a array of jobs belonging to a pipeline' do
- expect(group_graphql_data.map { |g| g.slice('name', 'size') }).to eq([
- { 'name' => 'rspec', 'size' => 2 },
- { 'name' => 'spinach', 'size' => 1 }
- ])
+ expect(group_graphql_data).to contain_exactly(
+ a_hash_including('name' => 'rspec', 'size' => 2),
+ a_hash_including('name' => 'spinach', 'size' => 1)
+ )
end
end
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
new file mode 100644
index 00000000000..78f7d3e149b
--- /dev/null
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create_default(:user) }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let_it_be(:prepare_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'prepare') }
+ let_it_be(:test_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'test') }
+
+ let_it_be(:job_1) { create(:ci_build, pipeline: pipeline, stage: 'prepare', name: 'Job 1') }
+ let_it_be(:job_2) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 2') }
+ let_it_be(:job_3) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 3') }
+
+ let(:path_to_job) do
+ [
+ [:project, { full_path: project.full_path }],
+ [:pipelines, { first: 1 }],
+ [:nodes, nil],
+ [:job, { id: global_id_of(job_2) }]
+ ]
+ end
+
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, all_graphql_fields_for(terminal_type)))
+ end
+
+ describe 'scalar fields' do
+ let(:path) { [:project, :pipelines, :nodes, 0, :job] }
+ let(:query_path) { path_to_job }
+ let(:terminal_type) { 'CiJob' }
+
+ it 'retrieves scalar fields' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'id' => global_id_of(job_2),
+ 'name' => job_2.name,
+ 'allowFailure' => job_2.allow_failure,
+ 'duration' => job_2.duration,
+ 'status' => job_2.status.upcase
+ )
+ end
+
+ context 'when fetching by name' do
+ before do
+ query_path.last[1] = { name: job_2.name }
+ end
+
+ it 'retrieves scalar fields' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'id' => global_id_of(job_2),
+ 'name' => job_2.name
+ )
+ end
+ end
+ end
+
+ describe '.detailedStatus' do
+ let(:path) { [:project, :pipelines, :nodes, 0, :job, :detailed_status] }
+ let(:query_path) { path_to_job + [:detailed_status] }
+ let(:terminal_type) { 'DetailedStatus' }
+
+ it 'retrieves detailed status' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'text' => 'pending',
+ 'label' => 'pending',
+ 'action' => a_hash_including('buttonTitle' => 'Cancel this job', 'icon' => 'cancel')
+ )
+ end
+ end
+
+ describe '.stage' do
+ let(:path) { [:project, :pipelines, :nodes, 0, :job, :stage] }
+ let(:query_path) { path_to_job + [:stage] }
+ let(:terminal_type) { 'CiStage' }
+
+ it 'returns appropriate data' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'name' => test_stage.name,
+ 'jobs' => a_hash_including(
+ 'nodes' => contain_exactly(
+ a_hash_including('id' => global_id_of(job_2)),
+ a_hash_including('id' => global_id_of(job_3))
+ )
+ )
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index cc028ff2ff9..6436fe1e9ef 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -5,24 +5,28 @@ require 'spec_helper'
RSpec.describe 'getting pipeline information nested in a project' do
include GraphqlHelpers
- let!(:project) { create(:project, :repository, :public) }
- let!(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:current_user) { create(:user) }
- let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] }
-
- let!(:query) do
- %(
- query {
- project(fullPath: "#{project.full_path}") {
- pipeline(iid: "#{pipeline.iid}") {
- configSource
- }
- }
- }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:build_job) { create(:ci_build, :trace_with_sections, name: 'build-a', pipeline: pipeline) }
+ let_it_be(:failed_build) { create(:ci_build, :failed, name: 'failed-build', pipeline: pipeline) }
+ let_it_be(:bridge) { create(:ci_bridge, name: 'ci-bridge-example', pipeline: pipeline) }
+
+ let(:path) { %i[project pipeline] }
+ let(:pipeline_graphql_data) { graphql_data_at(*path) }
+ let(:depth) { 3 }
+ let(:excluded) { %w[job project] } # Project is very expensive, due to the number of fields
+ let(:fields) { all_graphql_fields_for('Pipeline', excluded: excluded, max_depth: depth) }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:pipeline, { iid: pipeline.iid.to_s }, fields)
)
end
- it_behaves_like 'a working graphql query' do
+ it_behaves_like 'a working graphql query', :use_clean_rails_memory_store_caching, :request_store do
before do
post_graphql(query, current_user: current_user)
end
@@ -37,14 +41,18 @@ RSpec.describe 'getting pipeline information nested in a project' do
it 'contains configSource' do
post_graphql(query, current_user: current_user)
- expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE')
+ expect(pipeline_graphql_data['configSource']).to eq('UNKNOWN_SOURCE')
end
- context 'batching' do
- let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
- let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
+ context 'when batching' do
+ let!(:pipeline2) { successful_pipeline }
+ let!(:pipeline3) { successful_pipeline }
let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
+ def successful_pipeline
+ create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)])
+ end
+
it 'executes the finder once' do
mock = double(Ci::PipelinesFinder)
opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
@@ -80,4 +88,151 @@ RSpec.describe 'getting pipeline information nested in a project' do
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
end
+
+ context 'when enough data is requested' do
+ let(:fields) do
+ query_graphql_field(:jobs, nil,
+ query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3)))
+ end
+
+ it 'contains jobs' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly(
+ a_hash_including(
+ 'name' => build_job.name,
+ 'status' => build_job.status.upcase,
+ 'duration' => build_job.duration
+ ),
+ a_hash_including(
+ 'id' => global_id_of(failed_build),
+ 'status' => failed_build.status.upcase
+ ),
+ a_hash_including(
+ 'id' => global_id_of(bridge),
+ 'status' => bridge.status.upcase
+ )
+ )
+ end
+ end
+
+ context 'when requesting only builds with certain statuses' do
+ let(:variables) do
+ {
+ path: project.full_path,
+ pipelineIID: pipeline.iid.to_s,
+ status: :FAILED
+ }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $pipelineIID: ID!, $status: CiJobStatus!) {
+ project(fullPath: $path) {
+ pipeline(iid: $pipelineIID) {
+ jobs(statuses: [$status]) {
+ nodes {
+ #{all_graphql_fields_for('CiJob', max_depth: 1)}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'can filter build jobs by status' do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ expect(graphql_data_at(*path, :jobs, :nodes))
+ .to contain_exactly(a_hash_including('id' => global_id_of(failed_build)))
+ end
+ end
+
+ context 'when requesting a specific job' do
+ let(:variables) do
+ {
+ path: project.full_path,
+ pipelineIID: pipeline.iid.to_s
+ }
+ end
+
+ let(:build_fields) do
+ all_graphql_fields_for('CiJob', max_depth: 1)
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $pipelineIID: ID!, $jobName: String, $jobID: JobID) {
+ project(fullPath: $path) {
+ pipeline(iid: $pipelineIID) {
+ job(id: $jobID, name: $jobName) {
+ #{build_fields}
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ let(:the_job) do
+ a_hash_including('name' => build_job.name, 'id' => global_id_of(build_job))
+ end
+
+ it 'can request a build by name' do
+ vars = variables.merge(jobName: build_job.name)
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :job)).to match(the_job)
+ end
+
+ it 'can request a build by ID' do
+ vars = variables.merge(jobID: global_id_of(build_job))
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :job)).to match(the_job)
+ end
+
+ context 'when we request nested fields of the build' do
+ let_it_be(:needy) { create(:ci_build, :dependent, pipeline: pipeline) }
+
+ let(:build_fields) { 'needs { nodes { name } }' }
+ let(:vars) { variables.merge(jobID: global_id_of(needy)) }
+
+ it 'returns the nested data' do
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :job, :needs, :nodes)).to contain_exactly(
+ a_hash_including('name' => needy.needs.first.name)
+ )
+ end
+
+ it 'requires a constant number of queries' do
+ fst_user = create(:user)
+ snd_user = create(:user)
+ path = %i[project pipeline job needs nodes name]
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: fst_user, variables: vars)
+ end
+
+ expect(baseline.count).to be > 0
+ dep_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
+
+ deps = create_list(:ci_build, 3, :unique_name, pipeline: pipeline)
+ deps.each { |d| create(:ci_build_need, build: needy, name: d.name) }
+
+ expect do
+ post_graphql(query, current_user: snd_user, variables: vars)
+ end.not_to exceed_query_limit(baseline)
+
+ more_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
+
+ expect(more_names).to include(*dep_names)
+ expect(more_names.count).to be > dep_names.count
+ end
+ end
+ end
end
diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb
index 683ee3e4bf2..6e145fed733 100644
--- a/spec/support/helpers/board_helpers.rb
+++ b/spec/support/helpers/board_helpers.rb
@@ -5,14 +5,5 @@ module BoardHelpers
within card do
first('.board-card-number').click
end
-
- wait_for_sidebar
- end
-
- def wait_for_sidebar
- # loop until the CSS transition is complete
- Timeout.timeout(0.5) do
- loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
- end
end
end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 565c21e0f85..904b7efdd7f 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -30,11 +30,13 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
end
match do |kls|
- if @allow_extra
- expect(kls.fields.keys).to include(*expected_field_names)
- else
- expect(kls.fields.keys).to contain_exactly(*expected_field_names)
- end
+ keys = kls.fields.keys.to_set
+ fields = expected_field_names.to_set
+
+ next true if fields == keys
+ next true if @allow_extra && fields.proper_subset?(keys)
+
+ false
end
failure_message do |kls|
@@ -108,7 +110,7 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected|
names = expected_names(field).inspect
args = field.arguments.keys.inspect
- "expected that #{field.name} would have the following arguments: #{names}, but it has #{args}."
+ "expected #{field.name} to have the following arguments: #{names}, but it has #{args}."
end
end