Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-16 18:14:47 +00:00
parent 5382b5cdc4
commit 5e65d4f6c6
39 changed files with 904 additions and 221 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
mutation CreateCommit($input: CommitCreateInput!) {
commitCreate(input: $input) {
commit {
id
}
content
errors
}
}

View File

@ -0,0 +1,12 @@
query GetFileMetadata($fullPath: ID!, $filePath: String!, $ref: String) {
project(fullPath: $fullPath) {
id
repository {
blobs(paths: [$filePath], ref: $ref) {
nodes {
id
}
}
}
}
}

View File

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

View File

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

View File

@ -5,6 +5,7 @@ query getSecurityTrainingProviders($fullPath: ID!) {
name
id
description
isPrimary
isEnabled
url
}

View File

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

View File

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

View File

@ -859,6 +859,8 @@ table.code {
}
.diff-files-changed {
background-color: $body-bg;
.inline-parallel-buttons {
@include gl-relative;
z-index: 1;

View File

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

View File

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

View File

@ -117,6 +117,7 @@ class Member < ApplicationRecord
# to projects/groups.
scope :authorizable, -> do
connected_to_user
.active_state
.non_request
.non_minimal_access
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
- 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`.
- 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)).
- 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.
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`.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

@ -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' }],
};

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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