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--app/assets/javascripts/clusters/agents/index.js2
-rw-r--r--app/assets/javascripts/clusters/agents/router.js22
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/commit.vue224
-rw-r--r--app/assets/javascripts/pipeline_wizard/queries/create_commit.graphql9
-rw-r--r--app/assets/javascripts/pipeline_wizard/queries/get_file_meta.graphql12
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue54
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql7
-rw-r--r--app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql1
-rw-r--r--app/assets/javascripts/security_configuration/index.js3
-rw-r--r--app/assets/javascripts/security_configuration/resolver.js60
-rw-r--r--app/assets/stylesheets/framework/diffs.scss2
-rw-r--r--app/graphql/mutations/work_items/create.rb8
-rw-r--r--app/graphql/types/mutation_type.rb2
-rw-r--r--app/models/member.rb1
-rw-r--r--app/views/groups/settings/_general.html.haml3
-rw-r--r--config/feature_flags/development/vulnerability_report_pagination.yml8
-rw-r--r--doc/api/container_registry.md2
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/integration/elasticsearch.md2
-rw-r--r--doc/user/clusters/migrating_from_gma_to_project_template.md76
-rw-r--r--doc/user/group/roadmap/index.md4
-rw-r--r--doc/user/group/value_stream_analytics/index.md15
-rw-r--r--doc/user/project/import/bitbucket.md2
-rw-r--r--doc/user/project/merge_requests/commits.md35
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb4
-rw-r--r--lib/gitlab/database/query_analyzers/base.rb8
-rw-r--r--locale/gitlab.pot66
-rw-r--r--scripts/rspec_helpers.sh5
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js282
-rw-r--r--spec/frontend/pipeline_wizard/mock/query_responses.js62
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js39
-rw-r--r--spec/frontend/security_configuration/mock_data.js25
-rw-r--r--spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/test/junit_spec.rb26
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb16
-rw-r--r--spec/models/member_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb9
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb5
-rw-r--r--spec/requests/api/internal/base_spec.rb24
39 files changed, 907 insertions, 224 deletions
diff --git a/app/assets/javascripts/clusters/agents/index.js b/app/assets/javascripts/clusters/agents/index.js
index 10055092c29..ba7b3edba72 100644
--- a/app/assets/javascripts/clusters/agents/index.js
+++ b/app/assets/javascripts/clusters/agents/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
import apolloProvider from './graphql/provider';
+import createRouter from './router';
export default () => {
const el = document.querySelector('#js-cluster-agent-details');
@@ -20,6 +21,7 @@ export default () => {
return new Vue({
el,
apolloProvider,
+ router: createRouter(),
provide: {
activityEmptyStateImage,
agentName,
diff --git a/app/assets/javascripts/clusters/agents/router.js b/app/assets/javascripts/clusters/agents/router.js
new file mode 100644
index 00000000000..162a91dc300
--- /dev/null
+++ b/app/assets/javascripts/clusters/agents/router.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+
+Vue.use(VueRouter);
+
+// Vue Router requires a component to render if the route matches, but since we're only using it for
+// querystring handling, we'll create an empty component.
+const EmptyRouterComponent = {
+ render(createElement) {
+ return createElement('div');
+ },
+};
+
+export default () => {
+ // Name and path here don't really matter since we're not rendering anything if the route matches.
+ const routes = [{ path: '/', name: 'cluster_agents', component: EmptyRouterComponent }];
+ return new VueRouter({
+ mode: 'history',
+ base: window.location.pathname,
+ routes,
+ });
+};
diff --git a/app/assets/javascripts/pipeline_wizard/components/commit.vue b/app/assets/javascripts/pipeline_wizard/components/commit.vue
new file mode 100644
index 00000000000..518b41c66b1
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/components/commit.vue
@@ -0,0 +1,224 @@
+<script>
+import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormTextarea } from '@gitlab/ui';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { __, s__, sprintf } from '~/locale';
+import createCommitMutation from '../queries/create_commit.graphql';
+import getFileMetaDataQuery from '../queries/get_file_meta.graphql';
+import StepNav from './step_nav.vue';
+
+export const i18n = {
+ updateFileHeading: s__('PipelineWizard|Commit changes to your file'),
+ createFileHeading: s__('PipelineWizard|Commit your new file'),
+ fieldRequiredFeedback: __('This field is required'),
+ commitMessageLabel: s__('PipelineWizard|Commit Message'),
+ branchSelectorLabel: s__('PipelineWizard|Commit file to Branch'),
+ defaultUpdateCommitMessage: s__('PipelineWizardDefaultCommitMessage|Update %{filename}'),
+ defaultCreateCommitMessage: s__('PipelineWizardDefaultCommitMessage|Add %{filename}'),
+ commitButtonLabel: s__('PipelineWizard|Commit'),
+ commitSuccessMessage: s__('PipelineWizard|The file has been committed.'),
+ errors: {
+ loadError: s__(
+ 'PipelineWizard|There was a problem while checking whether your file already exists in the specified branch.',
+ ),
+ commitError: s__('PipelineWizard|There was a problem committing the changes.'),
+ },
+};
+
+const COMMIT_ACTION = {
+ CREATE: 'CREATE',
+ UPDATE: 'UPDATE',
+};
+
+export default {
+ i18n,
+ name: 'PipelineWizardCommitStep',
+ components: {
+ RefSelector,
+ GlAlert,
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormTextarea,
+ StepNav,
+ },
+ props: {
+ prev: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ fileContent: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ filename: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ branch: this.defaultBranch,
+ loading: false,
+ loadError: null,
+ commitError: null,
+ message: null,
+ };
+ },
+ computed: {
+ fileExistsInRepo() {
+ return this.project?.repository?.blobs.nodes.length > 0;
+ },
+ commitAction() {
+ return this.fileExistsInRepo ? COMMIT_ACTION.UPDATE : COMMIT_ACTION.CREATE;
+ },
+ defaultMessage() {
+ return sprintf(
+ this.fileExistsInRepo
+ ? this.$options.i18n.defaultUpdateCommitMessage
+ : this.$options.i18n.defaultCreateCommitMessage,
+ { filename: this.filename },
+ );
+ },
+ isCommitButtonEnabled() {
+ return this.fileExistsCheckInProgress;
+ },
+ fileExistsCheckInProgress() {
+ return this.$apollo.queries.project.loading;
+ },
+ mutationPayload() {
+ return {
+ mutation: createCommitMutation,
+ variables: {
+ input: {
+ projectPath: this.projectPath,
+ branch: this.branch,
+ message: this.message || this.defaultMessage,
+ actions: [
+ {
+ action: this.commitAction,
+ filePath: `/${this.filename}`,
+ content: this.fileContent,
+ },
+ ],
+ },
+ },
+ };
+ },
+ },
+ apollo: {
+ project: {
+ query: getFileMetaDataQuery,
+ variables() {
+ this.loadError = null;
+ return {
+ fullPath: this.projectPath,
+ filePath: this.filename,
+ ref: this.branch,
+ };
+ },
+ error() {
+ this.loadError = this.$options.i18n.errors.loadError;
+ },
+ },
+ },
+ methods: {
+ async commit() {
+ this.loading = true;
+ try {
+ const { data } = await this.$apollo.mutate(this.mutationPayload);
+ const hasError = Boolean(data.commitCreate.errors?.length);
+ if (hasError) {
+ this.commitError = this.$options.i18n.errors.commitError;
+ } else {
+ this.handleCommitSuccess();
+ }
+ } catch (e) {
+ this.commitError = this.$options.i18n.errors.commitError;
+ } finally {
+ this.loading = false;
+ }
+ },
+ handleCommitSuccess() {
+ this.$toast.show(this.$options.i18n.commitSuccessMessage);
+ this.$emit('done');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h4 v-if="fileExistsInRepo" key="create-heading">
+ {{ $options.i18n.updateFileHeading }}
+ </h4>
+ <h4 v-else key="update-heading">
+ {{ $options.i18n.createFileHeading }}
+ </h4>
+ <gl-alert
+ v-if="!!loadError"
+ :dismissible="false"
+ class="gl-mb-5"
+ data-testid="load-error"
+ variant="danger"
+ >
+ {{ loadError }}
+ </gl-alert>
+ <gl-form class="gl-max-w-48">
+ <gl-form-group
+ :invalid-feedback="$options.i18n.fieldRequiredFeedback"
+ :label="$options.i18n.commitMessageLabel"
+ data-testid="commit_message_group"
+ label-for="commit_message"
+ >
+ <gl-form-textarea
+ id="commit_message"
+ v-model="message"
+ :placeholder="defaultMessage"
+ data-testid="commit_message"
+ size="md"
+ @input="(v) => $emit('update:message', v)"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :invalid-feedback="$options.i18n.fieldRequiredFeedback"
+ :label="$options.i18n.branchSelectorLabel"
+ data-testid="branch_selector_group"
+ label-for="branch"
+ >
+ <ref-selector id="branch" v-model="branch" data-testid="branch" :project-id="projectPath" />
+ </gl-form-group>
+ <gl-alert
+ v-if="!!commitError"
+ :dismissible="false"
+ class="gl-mb-5"
+ data-testid="commit-error"
+ variant="danger"
+ >
+ {{ commitError }}
+ </gl-alert>
+ <step-nav show-back-button v-bind="$props" @back="$emit('go-back')">
+ <template #after>
+ <gl-button
+ :disabled="isCommitButtonEnabled"
+ :loading="fileExistsCheckInProgress || loading"
+ category="primary"
+ variant="confirm"
+ @click="commit"
+ >
+ {{ $options.i18n.commitButtonLabel }}
+ </gl-button>
+ </template>
+ </step-nav>
+ </gl-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_wizard/queries/create_commit.graphql b/app/assets/javascripts/pipeline_wizard/queries/create_commit.graphql
new file mode 100644
index 00000000000..9abf8eff587
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/queries/create_commit.graphql
@@ -0,0 +1,9 @@
+mutation CreateCommit($input: CommitCreateInput!) {
+ commitCreate(input: $input) {
+ commit {
+ id
+ }
+ content
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipeline_wizard/queries/get_file_meta.graphql b/app/assets/javascripts/pipeline_wizard/queries/get_file_meta.graphql
new file mode 100644
index 00000000000..87f014fade6
--- /dev/null
+++ b/app/assets/javascripts/pipeline_wizard/queries/get_file_meta.graphql
@@ -0,0 +1,12 @@
+query GetFileMetadata($fullPath: ID!, $filePath: String!, $ref: String) {
+ project(fullPath: $fullPath) {
+ id
+ repository {
+ blobs(paths: [$filePath], ref: $ref) {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index dea94503e62..539e2bff17c 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -49,7 +49,7 @@ export default {
data() {
return {
errorMessage: '',
- toggleLoading: false,
+ providerLoadingId: null,
securityTrainingProviders: [],
hasTouchedConfiguration: false,
};
@@ -89,37 +89,29 @@ export default {
Sentry.captureException(e);
}
},
- toggleProvider(selectedProviderId) {
- const toggledProviders = this.securityTrainingProviders.map((provider) => ({
- ...provider,
- ...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }),
- }));
+ toggleProvider(provider) {
+ const { isEnabled } = provider;
+ const toggledIsEnabled = !isEnabled;
- const enabledProviderIds = toggledProviders
- .filter(({ isEnabled }) => isEnabled)
- .map(({ id }) => id);
-
- const { isEnabled: selectedProviderIsEnabled } = toggledProviders.find(
- (provider) => provider.id === selectedProviderId,
- );
-
- this.trackProviderToggle(selectedProviderId, selectedProviderIsEnabled);
- this.storeEnabledProviders(enabledProviderIds);
+ this.trackProviderToggle(provider.id, toggledIsEnabled);
+ this.storeProvider({ ...provider, isEnabled: toggledIsEnabled });
},
- async storeEnabledProviders(enabledProviderIds) {
- this.toggleLoading = true;
+ async storeProvider({ id, isEnabled, isPrimary }) {
+ this.providerLoadingId = id;
try {
const {
data: {
- configureSecurityTrainingProviders: { errors = [] },
+ securityTrainingUpdate: { errors = [] },
},
} = await this.$apollo.mutate({
mutation: configureSecurityTrainingProvidersMutation,
variables: {
input: {
- enabledProviders: enabledProviderIds,
- fullPath: this.projectFullPath,
+ projectPath: this.projectFullPath,
+ providerId: id,
+ isEnabled,
+ isPrimary,
},
},
});
@@ -133,7 +125,7 @@ export default {
} catch {
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
} finally {
- this.toggleLoading = false;
+ this.providerLoadingId = null;
}
},
trackProviderToggle(providerId, providerIsEnabled) {
@@ -166,25 +158,21 @@ export default {
</gl-skeleton-loader>
</div>
<ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
- <li
- v-for="{ id, isEnabled, name, description, url } in securityTrainingProviders"
- :key="id"
- class="gl-mb-6"
- >
+ <li v-for="provider in securityTrainingProviders" :key="provider.id" class="gl-mb-6">
<gl-card>
<div class="gl-display-flex">
<gl-toggle
- :value="isEnabled"
+ :value="provider.isEnabled"
:label="__('Training mode')"
label-position="hidden"
- :is-loading="toggleLoading"
- @change="toggleProvider(id)"
+ :is-loading="providerLoadingId === provider.id"
+ @change="toggleProvider(provider)"
/>
<div class="gl-ml-5">
- <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
+ <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3>
<p>
- {{ description }}
- <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
+ {{ provider.description }}
+ <gl-link :href="provider.url" target="_blank">{{ __('Learn more.') }}</gl-link>
</p>
</div>
</div>
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql
index 660e0fadafb..3528bfaf7b8 100644
--- a/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql
+++ b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql
@@ -1,9 +1,10 @@
-mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) {
- configureSecurityTrainingProviders(input: $input) @client {
+mutation updateSecurityTraining($input: SecurityTrainingUpdateInput!) {
+ securityTrainingUpdate(input: $input) {
errors
- securityTrainingProviders {
+ training {
id
isEnabled
+ isPrimary
}
}
}
diff --git a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
index a8326bb1968..2baeda318f3 100644
--- a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
+++ b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
@@ -5,6 +5,7 @@ query getSecurityTrainingProviders($fullPath: ID!) {
name
id
description
+ isPrimary
isEnabled
url
}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index c8255a010b2..8416692dd27 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -5,7 +5,6 @@ import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
import { augmentFeatures } from './utils';
-import tempResolvers from './resolver';
export const initSecurityConfiguration = (el) => {
if (!el) {
@@ -15,7 +14,7 @@ export const initSecurityConfiguration = (el) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(tempResolvers),
+ defaultClient: createDefaultClient(),
});
const {
diff --git a/app/assets/javascripts/security_configuration/resolver.js b/app/assets/javascripts/security_configuration/resolver.js
deleted file mode 100644
index 51c02839a47..00000000000
--- a/app/assets/javascripts/security_configuration/resolver.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import produce from 'immer';
-import { __ } from '~/locale';
-import securityTrainingProvidersQuery from './graphql/security_training_providers.query.graphql';
-
-// Note: this is behind a feature flag and only a placeholder
-// until the actual GraphQL fields have been added
-// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480
-export default {
- Query: {
- securityTrainingProviders() {
- return [
- {
- __typename: 'SecurityTrainingProvider',
- id: 101,
- name: __('Kontra'),
- description: __('Interactive developer security education.'),
- url: 'https://application.security/',
- isEnabled: false,
- },
- {
- __typename: 'SecurityTrainingProvider',
- id: 102,
- name: __('SecureCodeWarrior'),
- description: __('Security training with guide and learning pathways.'),
- url: 'https://www.securecodewarrior.com/',
- isEnabled: true,
- },
- ];
- },
- },
-
- Mutation: {
- configureSecurityTrainingProviders: (
- _,
- { input: { enabledProviders, primaryProvider, fullPath } },
- { cache },
- ) => {
- const sourceData = cache.readQuery({
- query: securityTrainingProvidersQuery,
- variables: {
- fullPath,
- },
- });
-
- const data = produce(sourceData.project, (draftData) => {
- /* eslint-disable no-param-reassign */
- draftData.securityTrainingProviders.forEach((provider) => {
- provider.isPrimary = provider.id === primaryProvider;
- provider.isEnabled =
- provider.id === primaryProvider || enabledProviders.includes(provider.id);
- });
- });
-
- return {
- __typename: 'configureSecurityTrainingProvidersPayload',
- securityTrainingProviders: data.securityTrainingProviders,
- };
- },
- },
-};
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 4f1e46259b0..f0495fdc94e 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -859,6 +859,8 @@ table.code {
}
.diff-files-changed {
+ background-color: $body-bg;
+
.inline-parallel-buttons {
@include gl-relative;
z-index: 1;
diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb
index a5d713d70b5..81454db62b1 100644
--- a/app/graphql/mutations/work_items/create.rb
+++ b/app/graphql/mutations/work_items/create.rb
@@ -8,6 +8,9 @@ module Mutations
include Mutations::SpamProtection
include FindsProject
+ description "Creates a work item." \
+ " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
+
authorize :create_work_item
argument :description, GraphQL::Types::String,
@@ -29,6 +32,11 @@ module Mutations
def resolve(project_path:, **attributes)
project = authorized_find!(project_path)
+
+ unless Feature.enabled?(:work_items, project)
+ return { errors: ['`work_items` feature flag disabled for this project'] }
+ end
+
params = global_id_compatibility_params(attributes).merge(author_id: current_user.id)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index d16a7c77320..3c735231595 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -125,7 +125,7 @@ module Types
mount_mutation Mutations::Packages::Destroy
mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Echo
- mount_mutation Mutations::WorkItems::Create, feature_flag: :work_items
+ mount_mutation Mutations::WorkItems::Create
mount_mutation Mutations::WorkItems::Delete
mount_mutation Mutations::WorkItems::Update
end
diff --git a/app/models/member.rb b/app/models/member.rb
index f01f34ad5b2..528c6855d9c 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -117,6 +117,7 @@ class Member < ApplicationRecord
# to projects/groups.
scope :authorizable, -> do
connected_to_user
+ .active_state
.non_request
.non_minimal_access
end
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index dfb7932a781..ad0780e869c 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -16,9 +16,8 @@
.row.gl-mt-3
.form-group.col-md-9
- = f.label :description, s_('Groups|Group description'), class: 'label-bold'
+ = f.label :description, s_('Groups|Group description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
- .form-text.text-muted= s_('Groups|Optional group description.')
= render 'shared/repository_size_limit_setting_registration_features_cta', form: f
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
diff --git a/config/feature_flags/development/vulnerability_report_pagination.yml b/config/feature_flags/development/vulnerability_report_pagination.yml
new file mode 100644
index 00000000000..71639f6790b
--- /dev/null
+++ b/config/feature_flags/development/vulnerability_report_pagination.yml
@@ -0,0 +1,8 @@
+---
+name: vulnerability_report_pagination
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79834
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351975
+milestone: '14.8'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md
index 8fc7c4dc4fe..b61ec34b882 100644
--- a/doc/api/container_registry.md
+++ b/doc/api/container_registry.md
@@ -441,7 +441,7 @@ These are different from project or personal access tokens in the GitLab applica
### Listing all container repositories
```plaintext
-GET /v2/_catalogue
+GET /v2/_catalog
```
To list all container repositories on your GitLab instance, admin credentials are required:
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6d3115e29af..11509e9f8a4 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -5168,7 +5168,7 @@ Input type: `VulnerabilityRevertToDetectedInput`
### `Mutation.workItemCreate`
-Available only when feature flag `work_items` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
+Creates a work item. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.
Input type: `WorkItemCreateInput`
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index f09f5ff0d60..4976f7c2664 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -473,7 +473,7 @@ The following are some available Rake tasks:
| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Enables Elasticsearch indexing and run `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, and `gitlab:elastic:index_snippets`. |
| [`sudo gitlab-rake gitlab:elastic:pause_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Pauses Elasticsearch indexing. Changes are still tracked. Useful for cluster/index migrations. |
| [`sudo gitlab-rake gitlab:elastic:resume_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Resumes Elasticsearch indexing. |
-| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects and queues Sidekiq jobs to index them in the background. |
+| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects, and queues Sidekiq jobs to index them in the background. It can only be used after the index is created. |
| [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. |
| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. Note that this command results in a complete wipe of the index, and it should be used with caution. |
| [`sudo gitlab-rake gitlab:elastic:create_empty_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Generates empty indexes (the default index and a separate issues index) and assigns an alias for each on the Elasticsearch side only if it doesn't already exist. |
diff --git a/doc/user/clusters/migrating_from_gma_to_project_template.md b/doc/user/clusters/migrating_from_gma_to_project_template.md
index 94e9d4af36a..b2ba1bef338 100644
--- a/doc/user/clusters/migrating_from_gma_to_project_template.md
+++ b/doc/user/clusters/migrating_from_gma_to_project_template.md
@@ -23,16 +23,16 @@ See also [video walk-throughs](#video-walk-throughs) with examples.
1. Create a new project based on the [Cluster Management Project template](management_project_template.md#create-a-new-project-based-on-the-cluster-management-template).
1. [Associate your new Cluster Management Project with your cluster](management_project.md#associate-the-cluster-management-project-with-the-cluster).
1. Detect apps deployed through Helm v2 releases by using the pre-configured [`.gitlab-ci.yml`](management_project_template.md#the-gitlab-ciyml-file) file:
- - In case you had overwritten the default GitLab Managed Apps namespace, edit `.gitlab-ci.yml`,
- and make sure the script is receiving the correct namespace as an argument:
+ - In case you had overwritten the default GitLab Managed Apps namespace, edit `.gitlab-ci.yml`,
+ and make sure the script is receiving the correct namespace as an argument:
- ```yaml
- script:
- - gl-fail-if-helm2-releases-exist <your_custom_namespace>
- ```
+ ```yaml
+ script:
+ - gl-fail-if-helm2-releases-exist <your_custom_namespace>
+ ```
- - If you kept the default name (`gitlab-managed-apps`), then the script is already
- set up.
+ - If you kept the default name (`gitlab-managed-apps`), then the script is already
+ set up.
Either way, [run a pipeline manually](../../ci/pipelines/index.md#run-a-pipeline-manually) and read the logs of the
`detect-helm2-releases` job to know if you have any Helm v2 releases and which are they.
@@ -53,7 +53,7 @@ See also [video walk-throughs](#video-walk-throughs) with examples.
```shell
helm ls -n gitlab-managed-apps
-
+
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
runner gitlab-managed-apps 1 2021-06-09 19:36:55.739141644 +0000 UTC deployed gitlab-runner-0.28.0 13.11.0
```
@@ -67,42 +67,42 @@ See also [video walk-throughs](#video-walk-throughs) with examples.
1. Edit the `applications/{app}/values.yaml` associated with your app to match the currently
deployed values. For example, for GitLab Runner:
- 1. Copy the output of the following command (it might be big):
+ 1. Copy the output of the following command (it might be big):
- ```shell
- helm get values runner -n gitlab-managed-apps -a --output yaml
- ```
+ ```shell
+ helm get values runner -n gitlab-managed-apps -a --output yaml
+ ```
- 1. Overwrite `applications/gitlab-runner/values.yaml` with the output of the previous command.
+ 1. Overwrite `applications/gitlab-runner/values.yaml` with the output of the previous command.
This safe step will guarantee that no unexpected default values overwrite your currently deployed values.
For instance, your GitLab Runner could have its `gitlabUrl` or `runnerRegistrationToken` overwritten by mistake.
1. Some apps require special attention:
- - Ingress: Due to an existing [chart issue](https://github.com/helm/charts/pull/13646), you might see
- `spec.clusterIP: Invalid value` when trying to run the [`./gl-helmfile`](management_project_template.md#the-gitlab-ciyml-file)
- command. To work around this, after overwriting the release values in `applications/ingress/values.yaml`,
- you might need to overwrite all the occurrences of `omitClusterIP: false`, setting it to `omitClusterIP: true`.
- Another approach,could be to collect these IPs by running `kubectl get services -n gitlab-managed-apps`
- and then overwriting each `ClusterIP` that it complains about with the value you got from that command.
-
- - Vault: This application introduces a breaking change from the chart we used in Helm v2 to the chart
- used in Helm v3. So, the only way to integrate it with this Cluster Management Project is to actually uninstall this app and accept the
- chart version proposed in `applications/vault/values.yaml`.
-
- - Cert-manager:
- - For users on Kubernetes version 1.20 or above, the deprecated cert-manager v0.10 is no longer valid
- and the upgrade includes a breaking change. So we suggest that you [backup and uninstall cert-manager v0.10](#backup-and-uninstall-cert-manager-v010),
- and install the latest cert-manager instead. To install this version, uncomment `applications/cert-manager/helmfile.yaml`
- from [`./helmfile.yaml`](management_project_template.md#the-main-helmfileyml-file).
- This triggers a pipeline to install the new version.
- - For users on Kubernetes versions lower than 1.20, you can stick to v0.10 by uncommenting
- `applications/cert-manager-legacy/helmfile.yaml`
- in your project's main Helmfile ([`./helmfile.yaml`](management_project_template.md#the-main-helmfileyml-file)).
-
- WARNING:
- Cert-manager v0.10 breaks when Kubernetes is upgraded to version 1.20 or later.
+ - Ingress: Due to an existing [chart issue](https://github.com/helm/charts/pull/13646), you might see
+ `spec.clusterIP: Invalid value` when trying to run the [`./gl-helmfile`](management_project_template.md#the-gitlab-ciyml-file)
+ command. To work around this, after overwriting the release values in `applications/ingress/values.yaml`,
+ you might need to overwrite all the occurrences of `omitClusterIP: false`, setting it to `omitClusterIP: true`.
+ Another approach,could be to collect these IPs by running `kubectl get services -n gitlab-managed-apps`
+ and then overwriting each `ClusterIP` that it complains about with the value you got from that command.
+
+ - Vault: This application introduces a breaking change from the chart we used in Helm v2 to the chart
+ used in Helm v3. So, the only way to integrate it with this Cluster Management Project is to actually uninstall this app and accept the
+ chart version proposed in `applications/vault/values.yaml`.
+
+ - Cert-manager:
+ - For users on Kubernetes version 1.20 or above, the deprecated cert-manager v0.10 is no longer valid
+ and the upgrade includes a breaking change. So we suggest that you [backup and uninstall cert-manager v0.10](#backup-and-uninstall-cert-manager-v010),
+ and install the latest cert-manager instead. To install this version, uncomment `applications/cert-manager/helmfile.yaml`
+ from [`./helmfile.yaml`](management_project_template.md#the-main-helmfileyml-file).
+ This triggers a pipeline to install the new version.
+ - For users on Kubernetes versions lower than 1.20, you can stick to v0.10 by uncommenting
+ `applications/cert-manager-legacy/helmfile.yaml`
+ in your project's main Helmfile ([`./helmfile.yaml`](management_project_template.md#the-main-helmfileyml-file)).
+
+ WARNING:
+ Cert-manager v0.10 breaks when Kubernetes is upgraded to version 1.20 or later.
1. After following all the previous steps, [run a pipeline manually](../../ci/pipelines/index.md#run-a-pipeline-manually)
and watch the `apply` job logs to see if any of your applications were successfully detected, installed, and whether they got any
@@ -121,7 +121,7 @@ you want to manage with the Cluster Management Project.
## Backup and uninstall cert-manager v0.10
1. Follow the [official docs](https://docs.cert-manager.io/en/release-0.10/tasks/backup-restore-crds.html) on how to
- backup your cert-manager v0.10 data.
+ backup your cert-manager v0.10 data.
1. Uninstall cert-manager by editing the setting all the occurrences of `installed: true` to `installed: false` in the
`applications/cert-manager/helmfile.yaml` file.
1. Search for any left-over resources by executing the following command `kubectl get Issuers,ClusterIssuers,Certificates,CertificateRequests,Orders,Challenges,Secrets,ConfigMaps -n gitlab-managed-apps | grep certmanager`.
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 641fc8a1509..f10dc3bc22a 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -85,8 +85,10 @@ When you enable the roadmap settings sidebar, you can use it to refine epics sho
You can configure the following:
- Select date range.
+- Turn milestones on or off and select whether to show all, group, sub-group, or project milestones.
- Show all, open, or closed epics.
-- Turn progress tracking on or off and select whether it uses issue weights or counts.
+- Turn progress tracking for child issues on or off and select whether
+ to use issue weights or counts.
The progress tracking setting is not saved in user preferences but is saved or shared using URL parameters.
diff --git a/doc/user/group/value_stream_analytics/index.md b/doc/user/group/value_stream_analytics/index.md
index 4663cfc8bfd..ccf77f79fd9 100644
--- a/doc/user/group/value_stream_analytics/index.md
+++ b/doc/user/group/value_stream_analytics/index.md
@@ -117,16 +117,15 @@ production environment for all merge requests deployed in the given time period.
The "Recent Activity" metrics near the top of the page are measured as follows:
- **New Issues:** the number of issues created in the date range.
-- **Deploys:** the number of deployments <sup>1</sup> to production <sup>2</sup> in the date range.
-- **Deployment Frequency:** the average number of deployments <sup>1</sup> to production <sup>2</sup>
+- **Deploys:** the number of deployments to production in the date range.
+- **Deployment Frequency:** the average number of deployments to production
per day in the date range.
-1. To give a more accurate representation of deployments that actually completed successfully,
- the calculation for these two metrics changed in GitLab 13.9 from using the time a deployment was
- created to the time a deployment finished. If you were referencing this metric prior to 13.9, please
- keep this slight change in mind.
-1. To see deployment metrics, you must have a
- [production environment configured](../../../ci/environments/index.md#deployment-tier-of-environments).
+To see deployment metrics, you must have a [production environment configured](../../../ci/environments/index.md#deployment-tier-of-environments).
+
+NOTE:
+In GitLab 13.9 and later, deployment metrics are calculated based on when the deployment was finished.
+In GitLab 13.8 and earlier, deployment metrics are calculated based on when the deployment was created.
You can learn more about these metrics in our [analytics definitions](../../analytics/index.md).
diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md
index 0c50fc77e33..62495872659 100644
--- a/doc/user/project/import/bitbucket.md
+++ b/doc/user/project/import/bitbucket.md
@@ -42,7 +42,7 @@ When issues/pull requests are being imported, the Bitbucket importer tries to fi
the Bitbucket author/assignee in the GitLab database using the Bitbucket `nickname`.
For this to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
and **associated their Bitbucket account**. Their `nickname` must also match their Bitbucket
-`username.`. If the user is not found in the GitLab database, the project creator
+`username`. If the user is not found in the GitLab database, the project creator
(most of the times the current user that started the import process) is set as the author,
but a reference on the issue about the original Bitbucket author is kept.
diff --git a/doc/user/project/merge_requests/commits.md b/doc/user/project/merge_requests/commits.md
index 8970f958603..0014c1ba994 100644
--- a/doc/user/project/merge_requests/commits.md
+++ b/doc/user/project/merge_requests/commits.md
@@ -29,21 +29,17 @@ To seamlessly navigate among commits in a merge request:
## View merge request commits in context
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29274) in GitLab 13.12.
-> - [Deployed behind a feature flag](../../feature_flags.md), enabled by default.
-> - Disabled on GitLab.com.
-> - Not recommended for production use.
-> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-viewing-merge-request-commits-in-context).
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29274) in GitLab 13.12 [with a flag](../../../administration/feature_flags.md) named `context_commits`. Enabled by default.
+> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/320757) in GitLab 14.8.
WARNING:
This feature is in [beta](../../../policy/alpha-beta-support.md#beta-features)
and is [incomplete](https://gitlab.com/groups/gitlab-org/-/epics/1192).
-Previously merged commits can be added, but they can't be removed due to
-[this bug](https://gitlab.com/gitlab-org/gitlab/-/issues/325538).
-This in-development feature might not be available for your use. There can be
-[risks when enabling features still in development](../../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
-Refer to this feature's version history for more details.
+FLAG:
+On self-managed GitLab, by default this feature is available. To hide the feature,
+ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `context_commits`.
+On GitLab.com, this feature is available.
When reviewing a merge request, it helps to have more context about the changes
made. That includes unchanged lines in unchanged files, and previous commits
@@ -66,22 +62,3 @@ To view the changes done on those previously merged commits:
1. Scroll to **(file-tree)** **Compare** and select **previously merged commits**:
![Previously merged commits](img/previously_merged_commits_v14_1.png)
-
-### Enable or disable viewing merge request commits in context **(FREE SELF)**
-
-Viewing merge request commits in context is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
-can enable it.
-
-To enable it:
-
-```ruby
-Feature.enable(:context_commits)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:context_commits)
-```
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 364ae66844e..999ffff85d2 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -64,11 +64,11 @@ module Gitlab
def create_test_case(data, test_suite, job)
if data.key?('failure')
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
- system_output = data['failure']
+ system_output = data['failure'] || data['system_err']
attachment = attachment_path(data['system_out'])
elsif data.key?('error')
status = ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR
- system_output = data['error']
+ system_output = data['error'] || data['system_err']
attachment = attachment_path(data['system_out'])
elsif data.key?('skipped')
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED
diff --git a/lib/gitlab/database/query_analyzers/base.rb b/lib/gitlab/database/query_analyzers/base.rb
index 0802d3c8013..5f321ece962 100644
--- a/lib/gitlab/database/query_analyzers/base.rb
+++ b/lib/gitlab/database/query_analyzers/base.rb
@@ -48,11 +48,15 @@ module Gitlab
end
def self.context_key
- "#{self.class.name}_context"
+ @context_key ||= "analyzer_#{self.analyzer_key}_context".to_sym
end
def self.suppress_key
- "#{self.class.name}_suppressed"
+ @suppress_key ||= "analyzer_#{self.analyzer_key}_suppressed".to_sym
+ end
+
+ def self.analyzer_key
+ @analyzer_key ||= self.name.demodulize.underscore.to_sym
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 86593f5ffcc..d12028ae4eb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -426,8 +426,23 @@ msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
msgstr[1] ""
-msgid "%d vulnerability updated"
-msgid_plural "%d vulnerabilities updated"
+msgid "%d vulnerability set to confirmed"
+msgid_plural "%d vulnerabilities set to confirmed"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d vulnerability set to dismissed"
+msgid_plural "%d vulnerabilities set to dismissed"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d vulnerability set to needs triage"
+msgid_plural "%d vulnerabilities set to needs triage"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d vulnerability set to resolved"
+msgid_plural "%d vulnerabilities set to resolved"
msgstr[0] ""
msgstr[1] ""
@@ -17779,7 +17794,7 @@ msgstr ""
msgid "Groups|Group avatar"
msgstr ""
-msgid "Groups|Group description"
+msgid "Groups|Group description (optional)"
msgstr ""
msgid "Groups|Group name"
@@ -17797,9 +17812,6 @@ msgstr ""
msgid "Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses."
msgstr ""
-msgid "Groups|Optional group description."
-msgstr ""
-
msgid "Groups|Remove avatar"
msgstr ""
@@ -19726,9 +19738,6 @@ msgstr ""
msgid "Integrations|can't exceed %{recipients_limit}"
msgstr ""
-msgid "Interactive developer security education."
-msgstr ""
-
msgid "Interactive mode"
msgstr ""
@@ -21067,9 +21076,6 @@ msgstr ""
msgid "Ki"
msgstr ""
-msgid "Kontra"
-msgstr ""
-
msgid "Kroki"
msgstr ""
@@ -26643,12 +26649,42 @@ msgstr ""
msgid "PipelineStatusTooltip|Pipeline: %{ci_status}"
msgstr ""
+msgid "PipelineWizardDefaultCommitMessage|Add %{filename}"
+msgstr ""
+
+msgid "PipelineWizardDefaultCommitMessage|Update %{filename}"
+msgstr ""
+
msgid "PipelineWizardInputValidation|This field is required"
msgstr ""
msgid "PipelineWizardInputValidation|This value is not valid"
msgstr ""
+msgid "PipelineWizard|Commit"
+msgstr ""
+
+msgid "PipelineWizard|Commit Message"
+msgstr ""
+
+msgid "PipelineWizard|Commit changes to your file"
+msgstr ""
+
+msgid "PipelineWizard|Commit file to Branch"
+msgstr ""
+
+msgid "PipelineWizard|Commit your new file"
+msgstr ""
+
+msgid "PipelineWizard|The file has been committed."
+msgstr ""
+
+msgid "PipelineWizard|There was a problem committing the changes."
+msgstr ""
+
+msgid "PipelineWizard|There was a problem while checking whether your file already exists in the specified branch."
+msgstr ""
+
msgid "Pipelines"
msgstr ""
@@ -32024,9 +32060,6 @@ msgstr ""
msgid "Secure token that identifies an external storage request."
msgstr ""
-msgid "SecureCodeWarrior"
-msgstr ""
-
msgid "Security"
msgstr ""
@@ -32051,9 +32084,6 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security training with guide and learning pathways."
-msgstr ""
-
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
msgstr ""
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 38952b7cadf..af09d6d0edd 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -279,7 +279,8 @@ function rspec_paralellized_job() {
# Experiment to retry failed examples in a new RSpec process: https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1148
if [[ $rspec_run_status -ne 0 ]]; then
if [[ "${RETRY_FAILED_TESTS_IN_NEW_PROCESS}" == "true" ]]; then
- $rspec_run_status=$(retry_failed_rspec_examples)
+ retry_failed_rspec_examples
+ rspec_run_status=$?
fi
else
echosuccess "No examples to retry, congrats!"
@@ -310,7 +311,7 @@ function retry_failed_rspec_examples() {
# Merge the JUnit report from retry into the first-try report
junit_merge "${JUNIT_RETRY_FILE}" "${JUNIT_RESULT_FILE}"
- return $rspec_run_status
+ exit $rspec_run_status
}
function rspec_rerun_previous_failed_tests() {
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
new file mode 100644
index 00000000000..6496850b028
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -0,0 +1,282 @@
+import { GlButton, GlFormGroup } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { __, s__, sprintf } from '~/locale';
+import { mountExtended } from 'jest/__helpers__/vue_test_utils_helper';
+import CommitStep, { i18n } from '~/pipeline_wizard/components/commit.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import createCommitMutation from '~/pipeline_wizard/queries/create_commit.graphql';
+import getFileMetadataQuery from '~/pipeline_wizard/queries/get_file_meta.graphql';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import flushPromises from 'helpers/flush_promises';
+import {
+ createCommitMutationErrorResult,
+ createCommitMutationResult,
+ fileQueryErrorResult,
+ fileQueryResult,
+ fileQueryEmptyResult,
+} from '../mock/query_responses';
+
+Vue.use(VueApollo);
+
+const COMMIT_MESSAGE_ADD_FILE = s__('PipelineWizardDefaultCommitMessage|Add %{filename}');
+const COMMIT_MESSAGE_UPDATE_FILE = s__('PipelineWizardDefaultCommitMessage|Update %{filename}');
+
+describe('Pipeline Wizard - Commit Page', () => {
+ const createCommitMutationHandler = jest.fn();
+ const $toast = {
+ show: jest.fn(),
+ };
+
+ let wrapper;
+
+ const getMockApollo = (scenario = {}) => {
+ return createMockApollo([
+ [
+ createCommitMutation,
+ createCommitMutationHandler.mockResolvedValue(
+ scenario.commitHasError ? createCommitMutationErrorResult : createCommitMutationResult,
+ ),
+ ],
+ [
+ getFileMetadataQuery,
+ (vars) => {
+ if (scenario.fileResultByRef) return scenario.fileResultByRef[vars.ref];
+ if (scenario.hasError) return fileQueryErrorResult;
+ return scenario.fileExists ? fileQueryResult : fileQueryEmptyResult;
+ },
+ ],
+ ]);
+ };
+ const createComponent = (props = {}, mockApollo = getMockApollo()) => {
+ wrapper = mountExtended(CommitStep, {
+ apolloProvider: mockApollo,
+ propsData: {
+ projectPath: 'some/path',
+ defaultBranch: 'main',
+ filename: 'newFile.yml',
+ ...props,
+ },
+ mocks: { $toast },
+ stubs: {
+ RefSelector: true,
+ GlFormGroup,
+ },
+ });
+ };
+
+ function getButtonWithLabel(label) {
+ return wrapper.findAllComponents(GlButton).filter((n) => n.text().match(label));
+ }
+
+ describe('ui setup', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows a commit message input with the correct label', () => {
+ expect(wrapper.findByTestId('commit_message').exists()).toBe(true);
+ expect(wrapper.find('label[for="commit_message"]').text()).toBe(i18n.commitMessageLabel);
+ });
+
+ it('shows a branch selector with the correct label', () => {
+ expect(wrapper.findByTestId('branch').exists()).toBe(true);
+ expect(wrapper.find('label[for="branch"]').text()).toBe(i18n.branchSelectorLabel);
+ });
+
+ it('shows a commit button', () => {
+ expect(getButtonWithLabel(i18n.commitButtonLabel).exists()).toBe(true);
+ });
+
+ it('shows a back button', () => {
+ expect(getButtonWithLabel(__('Back')).exists()).toBe(true);
+ });
+
+ it('does not show a next button', () => {
+ expect(getButtonWithLabel(__('Next')).exists()).toBe(false);
+ });
+ });
+
+ describe('loading the remote file', () => {
+ const projectPath = 'foo/bar';
+ const filename = 'foo.yml';
+
+ it('does not show a load error if call is successful', async () => {
+ createComponent({ projectPath, filename });
+ await flushPromises();
+ expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
+ });
+
+ it('shows a load error if call returns an unexpected error', async () => {
+ const branch = 'foo';
+ createComponent(
+ { defaultBranch: branch, projectPath, filename },
+ createMockApollo([[getFileMetadataQuery, () => fileQueryErrorResult]]),
+ );
+ await flushPromises();
+ expect(wrapper.findByTestId('load-error').exists()).toBe(true);
+ expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+ });
+
+ describe('commit result handling', () => {
+ describe('successful commit', () => {
+ beforeEach(async () => {
+ createComponent();
+ await flushPromises();
+ await getButtonWithLabel(__('Commit')).trigger('click');
+ await flushPromises();
+ });
+
+ it('will not show an error', async () => {
+ expect(wrapper.findByTestId('commit-error').exists()).not.toBe(true);
+ });
+
+ it('will show a toast message', () => {
+ expect($toast.show).toHaveBeenCalledWith(
+ s__('PipelineWizard|The file has been committed.'),
+ );
+ });
+
+ it('emits a done event', () => {
+ expect(wrapper.emitted().done.length).toBe(1);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ jest.clearAllMocks();
+ });
+ });
+
+ describe('failed commit', () => {
+ beforeEach(async () => {
+ createComponent({}, getMockApollo({ commitHasError: true }));
+ await flushPromises();
+ await getButtonWithLabel(__('Commit')).trigger('click');
+ await flushPromises();
+ });
+
+ it('will show an error', async () => {
+ expect(wrapper.findByTestId('commit-error').exists()).toBe(true);
+ expect(wrapper.findByTestId('commit-error').text()).toBe(i18n.errors.commitError);
+ });
+
+ it('will not show a toast message', () => {
+ expect($toast.show).not.toHaveBeenCalledWith(i18n.commitSuccessMessage);
+ });
+
+ it('will not emit a done event', () => {
+ expect(wrapper.emitted().done?.length).toBeFalsy();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ jest.clearAllMocks();
+ });
+ });
+ });
+
+ describe('modelling different input combinations', () => {
+ const projectPath = 'some/path';
+ const defaultBranch = 'foo';
+ const fileContent = 'foo: bar';
+
+ describe.each`
+ filename | fileExistsOnDefaultBranch | fileExistsOnInputtedBranch | fileLoadError | commitMessageInputValue | branchInputValue | expectedCommitBranch | expectedCommitMessage | expectedAction
+ ${'foo.yml'} | ${false} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'CREATE'}
+ ${'foo.yml'} | ${true} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'UPDATE'}
+ ${'foo.yml'} | ${false} | ${true} | ${false} | ${'foo'} | ${'dev'} | ${'dev'} | ${'foo'} | ${'UPDATE'}
+ ${'foo.yml'} | ${false} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_ADD_FILE} | ${'CREATE'}
+ ${'foo.yml'} | ${true} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'}
+ ${'foo.yml'} | ${false} | ${true} | ${false} | ${null} | ${'dev'} | ${'dev'} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'}
+ `(
+ 'Test with fileExistsOnDefaultBranch=$fileExistsOnDefaultBranch, fileExistsOnInputtedBranch=$fileExistsOnInputtedBranch, commitMessageInputValue=$commitMessageInputValue, branchInputValue=$branchInputValue, commitReturnsError=$commitReturnsError',
+ ({
+ filename,
+ fileExistsOnDefaultBranch,
+ fileExistsOnInputtedBranch,
+ commitMessageInputValue,
+ branchInputValue,
+ expectedCommitBranch,
+ expectedCommitMessage,
+ expectedAction,
+ }) => {
+ let consoleSpy;
+
+ beforeAll(async () => {
+ createComponent(
+ {
+ filename,
+ defaultBranch,
+ projectPath,
+ fileContent,
+ },
+ getMockApollo({
+ fileResultByRef: {
+ [defaultBranch]: fileExistsOnDefaultBranch ? fileQueryResult : fileQueryEmptyResult,
+ [branchInputValue]: fileExistsOnInputtedBranch
+ ? fileQueryResult
+ : fileQueryEmptyResult,
+ },
+ }),
+ );
+
+ await flushPromises();
+
+ consoleSpy = jest.spyOn(console, 'error');
+
+ await wrapper
+ .findByTestId('commit_message')
+ .get('textarea')
+ .setValue(commitMessageInputValue);
+
+ if (branchInputValue) {
+ await wrapper.getComponent(RefSelector).vm.$emit('input', branchInputValue);
+ }
+ await Vue.nextTick();
+
+ await flushPromises();
+ });
+
+ afterAll(() => {
+ wrapper.destroy();
+ });
+
+ it('sets up without error', async () => {
+ expect(consoleSpy).not.toHaveBeenCalled();
+ });
+
+ it('does not show a load error', async () => {
+ expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
+ });
+
+ it('sends the expected commit mutation', async () => {
+ await getButtonWithLabel(__('Commit')).trigger('click');
+
+ expect(createCommitMutationHandler).toHaveBeenCalledWith({
+ input: {
+ actions: [
+ {
+ action: expectedAction,
+ content: fileContent,
+ filePath: `/${filename}`,
+ },
+ ],
+ branch: expectedCommitBranch,
+ message: sprintf(expectedCommitMessage, { filename }),
+ projectPath,
+ },
+ });
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/pipeline_wizard/mock/query_responses.js b/spec/frontend/pipeline_wizard/mock/query_responses.js
new file mode 100644
index 00000000000..95dcb881a04
--- /dev/null
+++ b/spec/frontend/pipeline_wizard/mock/query_responses.js
@@ -0,0 +1,62 @@
+export const createCommitMutationResult = {
+ data: {
+ commitCreate: {
+ commit: {
+ id: '82a9df1',
+ },
+ content: 'foo: bar',
+ errors: null,
+ },
+ },
+};
+
+export const createCommitMutationErrorResult = {
+ data: {
+ commitCreate: {
+ commit: null,
+ content: null,
+ errors: ['Some Error Message'],
+ },
+ },
+};
+
+export const fileQueryResult = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ repository: {
+ blobs: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Blob/9ff96777b315cd37188f7194d8382c718cb2933c',
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const fileQueryEmptyResult = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/2',
+ repository: {
+ blobs: {
+ nodes: [],
+ },
+ },
+ },
+ },
+};
+
+export const fileQueryErrorResult = {
+ data: {
+ foo: 'bar',
+ project: {
+ id: null,
+ repository: null,
+ },
+ },
+ errors: [{ message: 'GraphQL Error' }],
+};
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index ea5df9ca550..18c9ada6bde 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -19,6 +19,8 @@ import {
dismissUserCalloutErrorResponse,
securityTrainingProviders,
securityTrainingProvidersResponse,
+ updateSecurityTrainingProvidersResponse,
+ updateSecurityTrainingProvidersErrorResponse,
testProjectPath,
textProviderIds,
} from '../mock_data';
@@ -29,18 +31,22 @@ describe('TrainingProviderList component', () => {
let wrapper;
let apolloProvider;
- const createApolloProvider = ({ resolvers, handlers = [] } = {}) => {
+ const createApolloProvider = ({ handlers = [] } = {}) => {
const defaultHandlers = [
[
securityTrainingProvidersQuery,
jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
],
+ [
+ configureSecurityTrainingProvidersMutation,
+ jest.fn().mockResolvedValue(updateSecurityTrainingProvidersResponse),
+ ],
];
// make sure we don't have any duplicate handlers to avoid 'Request handler already defined for query` errors
const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])];
- apolloProvider = createMockApollo(mergedHandlers, resolvers);
+ apolloProvider = createMockApollo(mergedHandlers);
};
const createComponent = () => {
@@ -62,7 +68,7 @@ describe('TrainingProviderList component', () => {
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findErrorAlert = () => wrapper.findComponent(GlAlert);
- const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
+ const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]);
afterEach(() => {
wrapper.destroy();
@@ -146,9 +152,9 @@ describe('TrainingProviderList component', () => {
beforeEach(async () => {
jest.spyOn(apolloProvider.defaultClient, 'mutate');
- await waitForMutationToBeLoaded();
+ await waitForQueryToBeLoaded();
- toggleFirstProvider();
+ await toggleFirstProvider();
});
it.each`
@@ -166,7 +172,14 @@ describe('TrainingProviderList component', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: configureSecurityTrainingProvidersMutation,
- variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
+ variables: {
+ input: {
+ providerId: textProviderIds[0],
+ isEnabled: true,
+ isPrimary: false,
+ projectPath: testProjectPath,
+ },
+ },
}),
);
});
@@ -264,14 +277,12 @@ describe('TrainingProviderList component', () => {
describe('when storing training provider configurations', () => {
beforeEach(async () => {
createApolloProvider({
- resolvers: {
- Mutation: {
- configureSecurityTrainingProviders: () => ({
- errors: ['something went wrong!'],
- securityTrainingProviders: [],
- }),
- },
- },
+ handlers: [
+ [
+ configureSecurityTrainingProvidersMutation,
+ jest.fn().mockReturnValue(updateSecurityTrainingProvidersErrorResponse),
+ ],
+ ],
});
createComponent();
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index e47a255fbac..b042e870467 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -9,6 +9,7 @@ export const securityTrainingProviders = [
description: 'Interactive developer security education',
url: 'https://www.example.org/security/training',
isEnabled: false,
+ isPrimary: false,
},
{
id: textProviderIds[1],
@@ -16,6 +17,7 @@ export const securityTrainingProviders = [
description: 'Security training with guide and learning pathways.',
url: 'https://www.vendornametwo.com/',
isEnabled: true,
+ isPrimary: false,
},
];
@@ -51,3 +53,26 @@ export const dismissUserCalloutErrorResponse = {
},
},
};
+
+export const updateSecurityTrainingProvidersResponse = {
+ data: {
+ securityTrainingUpdate: {
+ errors: [],
+ training: {
+ id: 101,
+ name: 'Acme',
+ isEnabled: true,
+ isPrimary: false,
+ },
+ },
+ },
+};
+
+export const updateSecurityTrainingProvidersErrorResponse = {
+ data: {
+ securityTrainingUpdate: {
+ errors: ['something went wrong!'],
+ training: null,
+ },
+ },
+};
diff --git a/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb
index 0f7f5c8122e..1aac5970a77 100644
--- a/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration,
protected: true,
instance_runners_enabled: true,
minutes_exceeded: false,
- tag_ids: [22, 23],
+ tag_ids: match_array([22, 23]),
namespace_traversal_ids: [10]),
an_object_having_attributes(
build_id: 60,
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index 4ca8f74e57f..82fa11d5f98 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -99,6 +99,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
'Some failure'
end
+ context 'and has failure with no message but has system-err' do
+ let(:testcase_content) do
+ <<-EOF.strip_heredoc
+ <failure></failure>
+ <system-err>Some failure</system-err>
+ EOF
+ end
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED,
+ 'Some failure'
+ end
+
context 'and has error' do
let(:testcase_content) { '<error>Some error</error>' }
@@ -107,6 +120,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
'Some error'
end
+ context 'and has error with no message but has system-err' do
+ let(:testcase_content) do
+ <<-EOF.strip_heredoc
+ <error></error>
+ <system-err>Some error</system-err>
+ EOF
+ end
+
+ it_behaves_like '<testcase> XML parser',
+ ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR,
+ 'Some error'
+ end
+
context 'and has skipped' do
let(:testcase_content) { '<skipped/>' }
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
index ebd3c5f6235..22a70dc7df0 100644
--- a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
@@ -14,6 +14,22 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
Gitlab::Database::QueryAnalyzer.instance.within { example.run }
end
+ describe 'context and suppress key names' do
+ describe '.context_key' do
+ it 'contains class name' do
+ expect(described_class.context_key)
+ .to eq 'analyzer_prevent_cross_database_modification_context'.to_sym
+ end
+ end
+
+ describe '.suppress_key' do
+ it 'contains class name' do
+ expect(described_class.suppress_key)
+ .to eq 'analyzer_prevent_cross_database_modification_suppressed'.to_sym
+ end
+ end
+ end
+
shared_examples 'successful examples' do |model:|
let(:model) { model }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6f271fea2c4..79491edba94 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -513,6 +513,8 @@ RSpec.describe Member do
it { is_expected.not_to include @invited_member }
it { is_expected.not_to include @requested_member }
it { is_expected.not_to include @member_with_minimal_access }
+ it { is_expected.not_to include awaiting_group_member }
+ it { is_expected.not_to include awaiting_project_member }
end
describe '.distinct_on_user_with_max_access_level' do
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index 006fe03378f..6abdaa2c850 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -68,8 +68,13 @@ RSpec.describe 'Create a work item' do
stub_feature_flags(work_items: false)
end
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ["Field 'workItemCreate' doesn't exist on type 'Mutation'", "Variable $workItemCreateInput is declared by anonymous mutation but not used"]
+ it 'does not create the work item and returns an error' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to not_change(WorkItem, :count)
+
+ expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index f3a9f8dd49c..71b03103115 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -71,10 +71,11 @@ RSpec.describe 'Update a work item' do
stub_feature_flags(work_items: false)
end
- it 'does nothing and returns and error' do
+ it 'does not update the work item and returns and error' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
- end.to not_change(WorkItem, :count)
+ work_item.reload
+ end.to not_change(work_item, :title)
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index b624b95c016..2b7963eadab 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -748,6 +748,30 @@ RSpec.describe API::Internal::Base do
end
end
+ context 'with a pending membership' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ before_all do
+ create(:project_member, :awaiting, :developer, source: project, user: user)
+ end
+
+ it 'returns not found for git pull' do
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response["status"]).to be_falsey
+ expect(user.reload.last_activity_on).to be_nil
+ end
+
+ it 'returns not found for git push' do
+ push(key, project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response["status"]).to be_falsey
+ expect(user.reload.last_activity_on).to be_nil
+ end
+ end
+
context "custom action" do
let(:access_checker) { double(Gitlab::GitAccess) }
let(:payload) do