diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-16 21:14:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-16 21:14:47 +0300 |
commit | 5e65d4f6c601e6636d171dbf8586949fedc334d7 (patch) | |
tree | 68377df58b2318fedd44affaf29e59ec436e6a71 | |
parent | 5382b5cdc41d11fc50c47e226c48660aa0ddff55 (diff) |
Add latest changes from gitlab-org/gitlab@master
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 |