Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-05 18:09:44 +00:00
parent 18e9429b63
commit 2f1a81fd16
92 changed files with 1542 additions and 392 deletions

View File

@ -103,7 +103,6 @@ proper-names:
"OAuth",
"OAuth 2",
"OmniAuth",
"Omnibus GitLab",
"OpenID",
"OpenShift",
"PgBouncer",

View File

@ -1065,7 +1065,6 @@ Layout/ArgumentAlignment:
- 'ee/spec/features/groups/settings/protected_environments_spec.rb'
- 'ee/spec/features/groups/usage_quotas/pipelines_tab_spec.rb'
- 'ee/spec/features/markdown/metrics_spec.rb'
- 'ee/spec/features/merge_request/user_merges_with_namespace_storage_limits_spec.rb'
- 'ee/spec/features/merge_request/user_sees_approval_widget_spec.rb'
- 'ee/spec/features/namespace_user_cap_reached_alert_spec.rb'
- 'ee/spec/features/projects/environments/environment_spec.rb'
@ -1665,30 +1664,6 @@ Layout/ArgumentAlignment:
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/purchase/user_registration_billing_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/saas_user_limit_experience_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/11_fulfillment/utilization/free_namespace_storage_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/database_delete_replication_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/geo_replication_ci_job_log_artifacts_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/geo_replication_maven_package_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/geo_replication_npm_registry_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/geo_replication_project_snippets_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/http_push_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/http_push_to_secondary_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/ssh_push_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/ssh_push_to_secondary_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/wiki_http_push_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/wiki_http_push_to_secondary_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/wiki_ssh_push_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/12_systems/geo/wiki_ssh_push_to_secondary_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/13_secure/merge_request_license_widget_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/15_growth/free_trial_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_git_access_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_non_enforced_sso_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/1_manage/group/restrict_by_ip_address_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/group_wiki/delete_group_wiki_page_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/file_locking_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/project_templates_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/push_rules_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/4_verify/job_trace_archival_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/5_package/dependency_proxy_sso_spec.rb'
- 'qa/qa/vendor/jira/jira_api.rb'
- 'qa/spec/support/loglinking_spec.rb'
- 'rubocop/cop/gitlab/finder_with_find_by.rb'

View File

@ -170,7 +170,7 @@ export default {
>
<div class="gl-pb-3 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<gl-icon name="search" class="gl-absolute gl-top-5 tree-list-icon" />
<gl-icon name="search" class="gl-absolute gl-top-3 gl-left-3 tree-list-icon" />
<label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label>
<input
id="diff-tree-search"
@ -189,7 +189,7 @@ export default {
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
<gl-icon name="close" />
<gl-icon name="close" class="gl-absolute gl-top-3 gl-right-1 tree-list-icon" />
</button>
</div>
</div>

View File

@ -6,6 +6,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import KubernetesAgentInfo from './kubernetes_agent_info.vue';
import KubernetesPods from './kubernetes_pods.vue';
import KubernetesTabs from './kubernetes_tabs.vue';
import KubernetesStatusBar from './kubernetes_status_bar.vue';
export default {
components: {
@ -15,6 +16,7 @@ export default {
KubernetesAgentInfo,
KubernetesPods,
KubernetesTabs,
KubernetesStatusBar,
},
inject: ['kasTunnelUrl'],
props: {
@ -32,6 +34,9 @@ export default {
return {
isVisible: false,
error: '',
hasFailedState: false,
podsLoading: false,
workloadTypesLoading: false,
};
},
computed: {
@ -52,6 +57,14 @@ export default {
},
};
},
clusterHealthStatus() {
const clusterDataLoading = this.podsLoading || this.workloadTypesLoading;
if (clusterDataLoading) {
return '';
}
return this.hasFailedState ? 'error' : 'success';
},
},
methods: {
toggleCollapse() {
@ -82,6 +95,7 @@ export default {
</p>
<gl-collapse :visible="isVisible" class="gl-md-pl-7 gl-md-pr-5 gl-mt-4">
<template v-if="isVisible">
<kubernetes-status-bar :cluster-health-status="clusterHealthStatus" class="gl-mb-4" />
<kubernetes-agent-info :cluster-agent="clusterAgent" class="gl-mb-5" />
<gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5">
@ -92,12 +106,16 @@ export default {
:configuration="k8sAccessConfiguration"
:namespace="namespace"
class="gl-mb-5"
@cluster-error="onClusterError" />
@cluster-error="onClusterError"
@loading="podsLoading = $event"
@failed="hasFailedState = true" />
<kubernetes-tabs
:configuration="k8sAccessConfiguration"
:namespace="namespace"
class="gl-mb-5"
@cluster-error="onClusterError"
@loading="workloadTypesLoading = $event"
@failed="hasFailedState = true"
/></template>
</gl-collapse>
</div>

View File

@ -3,6 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import k8sPodsQuery from '../graphql/queries/k8s_pods.query.graphql';
import { PHASE_RUNNING, PHASE_PENDING, PHASE_SUCCEEDED, PHASE_FAILED } from '../constants';
export default {
components: {
@ -25,6 +26,9 @@ export default {
this.error = error;
this.$emit('cluster-error', this.error);
},
watchLoading(isLoading) {
this.$emit('loading', isLoading);
},
},
},
props: {
@ -42,41 +46,39 @@ export default {
error: '',
};
},
computed: {
podStats() {
if (!this.k8sPods) return null;
return [
{
// eslint-disable-next-line @gitlab/require-i18n-strings
value: this.getPodsByPhase('Running'),
value: this.countPodsByPhase(PHASE_RUNNING),
title: this.$options.i18n.runningPods,
},
{
// eslint-disable-next-line @gitlab/require-i18n-strings
value: this.getPodsByPhase('Pending'),
value: this.countPodsByPhase(PHASE_PENDING),
title: this.$options.i18n.pendingPods,
},
{
// eslint-disable-next-line @gitlab/require-i18n-strings
value: this.getPodsByPhase('Succeeded'),
value: this.countPodsByPhase(PHASE_SUCCEEDED),
title: this.$options.i18n.succeededPods,
},
{
// eslint-disable-next-line @gitlab/require-i18n-strings
value: this.getPodsByPhase('Failed'),
value: this.countPodsByPhase(PHASE_FAILED),
title: this.$options.i18n.failedPods,
},
];
},
loading() {
return this.$apollo.queries.k8sPods.loading;
return this.$apollo?.queries?.k8sPods?.loading;
},
},
methods: {
getPodsByPhase(phase) {
countPodsByPhase(phase) {
const filteredPods = this.k8sPods.filter((item) => item.status.phase === phase);
if (phase === PHASE_FAILED && filteredPods.length) {
this.$emit('failed');
}
return filteredPods.length;
},
},

View File

@ -0,0 +1,39 @@
<script>
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import { HEALTH_BADGES } from '../constants';
export default {
components: {
GlLoadingIcon,
GlBadge,
},
props: {
clusterHealthStatus: {
required: false,
type: String,
default: '',
validator(val) {
return ['error', 'success', ''].includes(val);
},
},
},
computed: {
healthBadge() {
return HEALTH_BADGES[this.clusterHealthStatus];
},
},
i18n: {
healthLabel: s__('Environment|Environment health'),
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-mb-2">
<span class="gl-font-sm gl-font-monospace gl-mr-3">{{ $options.i18n.healthLabel }}</span>
<gl-loading-icon v-if="!clusterHealthStatus" size="sm" inline />
<gl-badge v-else-if="healthBadge" :variant="healthBadge.variant">
{{ healthBadge.text }}
</gl-badge>
</div>
</template>

View File

@ -32,6 +32,12 @@ export default {
error(error) {
this.$emit('cluster-error', error);
},
result() {
this.checkFailed();
},
watchLoading(isLoading) {
this.$emit('loading', isLoading);
},
},
},
props: {
@ -46,7 +52,7 @@ export default {
},
computed: {
summaryLoading() {
return this.$apollo.queries.k8sWorkloads.loading;
return this.$apollo?.queries?.k8sWorkloads?.loading;
},
summaryCount() {
return this.k8sWorkloads ? Object.values(this.k8sWorkloads).flat().length : 0;
@ -128,6 +134,17 @@ export default {
};
},
},
methods: {
checkFailed() {
const failed = this.summaryObjects.some((workloadType) => {
return workloadType.items?.failed?.length > 0;
});
if (failed) {
this.$emit('failed');
}
},
},
i18n: {
summaryTitle: s__('Environment|Summary'),
deployments: s__('Environment|Deployments'),

View File

@ -134,7 +134,12 @@ export default {
</script>
<template>
<gl-tabs>
<kubernetes-summary :namespace="namespace" :configuration="configuration" />
<kubernetes-summary
:namespace="namespace"
:configuration="configuration"
@loading="$emit('loading', $event)"
@failed="$emit('failed')"
/>
<gl-tab>
<template #title>

View File

@ -89,3 +89,22 @@ export const ENVIRONMENT_NEW_HELP_TEXT = __(
export const ENVIRONMENT_EDIT_HELP_TEXT = ENVIRONMENT_NEW_HELP_TEXT;
export const SERVICES_LIMIT_PER_PAGE = 10;
export const CLUSTER_STATUS_HEALTHY_TEXT = s__('Environment|Healthy');
export const CLUSTER_STATUS_UNHEALTHY_TEXT = s__('Environment|Unhealthy');
export const HEALTH_BADGES = {
success: {
variant: 'success',
text: CLUSTER_STATUS_HEALTHY_TEXT,
},
error: {
variant: 'danger',
text: CLUSTER_STATUS_UNHEALTHY_TEXT,
},
};
export const PHASE_RUNNING = 'Running';
export const PHASE_PENDING = 'Pending';
export const PHASE_SUCCEEDED = 'Succeeded';
export const PHASE_FAILED = 'Failed';

View File

@ -0,0 +1,165 @@
<script>
import { GlBadge, GlLink, GlLoadingIcon, GlModal, GlSprintf, GlToggle } from '@gitlab/ui';
import { createAlert, VARIANT_INFO } from '~/alert';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import getCiCatalogSettingsQuery from '../graphql/queries/get_ci_catalog_settings.query.graphql';
import catalogResourcesCreate from '../graphql/mutations/catalog_resources_create.mutation.graphql';
export const i18n = {
badgeText: __('Experiment'),
catalogResourceQueryError: s__(
'CiCatalog|There was a problem fetching the CI/CD Catalog setting.',
),
catalogResourceMutationError: s__(
'CiCatalog|There was a problem marking the project as a CI/CD Catalog resource.',
),
catalogResourceMutationSuccess: s__('CiCatalog|This project is now a CI/CD Catalog resource.'),
ciCatalogLabel: s__('CiCatalog|CI/CD Catalog resource'),
ciCatalogHelpText: s__(
'CiCatalog|Mark project as a CI/CD Catalog resource. %{linkStart}What is the CI/CD Catalog?%{linkEnd}',
),
modal: {
actionPrimary: {
text: s__('CiCatalog|Mark project as a CI/CD Catalog resource'),
},
actionCancel: {
text: __('Cancel'),
},
body: s__(
'CiCatalog|This project will be marked as a CI/CD Catalog resource and will be visible in the CI/CD Catalog. This action is not reversible.',
),
title: s__('CiCatalog|Mark project as a CI/CD Catalog resource'),
},
readMeHelpText: s__(
'CiCatalog|The project must contain a README.md file and a template.yml file. When enabled, the repository is available in the CI/CD Catalog.',
),
};
export const ciCatalogHelpPath = helpPagePath('ci/components/index', {
anchor: 'components-catalog',
});
export default {
i18n,
components: {
GlBadge,
GlLink,
GlLoadingIcon,
GlModal,
GlSprintf,
GlToggle,
},
props: {
fullPath: {
type: String,
required: true,
},
},
data() {
return {
ciCatalogHelpPath,
isCatalogResource: false,
showCatalogResourceModal: false,
};
},
apollo: {
isCatalogResource: {
query: getCiCatalogSettingsQuery,
variables() {
return {
fullPath: this.fullPath,
};
},
update({ project }) {
return project?.isCatalogResource || false;
},
error() {
createAlert({ message: this.$options.i18n.catalogResourceQueryError });
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.isCatalogResource.loading;
},
},
methods: {
async markProjectAsCatalogResource() {
try {
const {
data: {
catalogResourcesCreate: { errors },
},
} = await this.$apollo.mutate({
mutation: catalogResourcesCreate,
variables: { input: { projectPath: this.fullPath } },
});
if (errors.length) {
throw new Error(errors[0]);
}
this.isCatalogResource = true;
createAlert({
message: this.$options.i18n.catalogResourceMutationSuccess,
variant: VARIANT_INFO,
});
} catch (error) {
const message = error.message || this.$options.i18n.catalogResourceMutationError;
createAlert({ message });
}
},
onCatalogResourceEnabledToggled() {
this.showCatalogResourceModal = true;
},
onModalCanceled() {
this.showCatalogResourceModal = false;
},
},
};
</script>
<template>
<div>
<gl-loading-icon v-if="isLoading" />
<div v-else data-testid="ci-catalog-settings">
<div>
<label class="gl-mb-1 gl-mr-2">
{{ $options.i18n.ciCatalogLabel }}
</label>
<gl-badge size="sm" variant="info"> {{ $options.i18n.badgeText }} </gl-badge>
</div>
<gl-sprintf :message="$options.i18n.ciCatalogHelpText">
<template #link="{ content }">
<gl-link :href="ciCatalogHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<gl-toggle
class="gl-my-2"
:disabled="isCatalogResource"
:value="isCatalogResource"
:label="$options.i18n.ciCatalogLabel"
label-position="hidden"
name="ci_resource_enabled"
@change="onCatalogResourceEnabledToggled"
/>
<div class="gl-text-secondary">
{{ $options.i18n.readMeHelpText }}
</div>
<gl-modal
:visible="showCatalogResourceModal"
modal-id="mark-as-catalog-resource"
size="sm"
:title="$options.i18n.modal.title"
:action-cancel="$options.i18n.modal.actionCancel"
:action-primary="$options.i18n.modal.actionPrimary"
@canceled="onModalCanceled"
@primary="markProjectAsCatalogResource"
>
{{ $options.i18n.modal.body }}
</gl-modal>
</div>
</div>
</template>

View File

@ -20,6 +20,7 @@ import {
import { toggleHiddenClassBySelector } from '../external';
import ProjectFeatureSetting from './project_feature_setting.vue';
import ProjectSettingRow from './project_setting_row.vue';
import CiCatalogSettings from './ci_catalog_settings.vue';
const FEATURE_ACCESS_LEVEL_ANONYMOUS = [30, s__('ProjectSettings|Everyone')];
@ -34,6 +35,7 @@ export default {
...CVE_ID_REQUEST_BUTTON_I18N,
analyticsLabel: s__('ProjectSettings|Analytics'),
containerRegistryLabel: s__('ProjectSettings|Container registry'),
ciCdLabel: __('CI/CD'),
forksLabel: s__('ProjectSettings|Forks'),
issuesLabel: s__('ProjectSettings|Issues'),
lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
@ -62,7 +64,6 @@ export default {
'ProjectSettings|Track machine learning model experiments and artifacts.',
),
pagesLabel: s__('ProjectSettings|Pages'),
ciCdLabel: __('CI/CD'),
repositoryLabel: s__('ProjectSettings|Repository'),
requirementsLabel: s__('ProjectSettings|Requirements'),
releasesLabel: s__('ProjectSettings|Releases'),
@ -84,6 +85,7 @@ export default {
modelExperimentsHelpPath,
components: {
CiCatalogSettings,
ProjectFeatureSetting,
ProjectSettingRow,
GlButton,
@ -106,6 +108,11 @@ export default {
required: false,
default: false,
},
canAddCatalogResource: {
type: Boolean,
required: false,
default: false,
},
currentSettings: {
type: Object,
required: true,
@ -365,6 +372,9 @@ export default {
packageRegistryApiForEveryoneEnabledShown() {
return this.visibilityLevel !== VISIBILITY_LEVEL_PUBLIC_INTEGER;
},
monitorOperationsFeatureAccessLevelOptions() {
return this.featureAccessLevelOptions.filter(([value]) => value <= this.monitorAccessLevel);
},
},
watch: {
@ -985,6 +995,11 @@ export default {
/>
</project-setting-row>
</div>
<ci-catalog-settings
v-if="canAddCatalogResource"
class="gl-mb-5"
:full-path="confirmationPhrase"
/>
<project-setting-row v-if="canDisableEmails" ref="email-settings" class="mb-3">
<label class="js-emails-disabled">
<input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" />

View File

@ -0,0 +1,5 @@
mutation catalogResourcesCreate($input: CatalogResourcesCreateInput!) {
catalogResourcesCreate(input: $input) {
errors
}
}

View File

@ -0,0 +1,6 @@
query getCiCatalogSettings($fullPath: ID!) {
project(fullPath: $fullPath) {
id
isCatalogResource
}
}

View File

@ -1,8 +1,17 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import settingsPanel from './components/settings_panel.vue';
Vue.use(VueApollo);
export default function initProjectPermissionsSettings() {
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const mountPoint = document.querySelector('.js-project-permissions-form');
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
const componentProps = JSON.parse(componentPropsEl.innerHTML);
@ -19,6 +28,8 @@ export default function initProjectPermissionsSettings() {
return new Vue({
el: mountPoint,
name: 'ProjectPermissionsRoot',
apolloProvider,
provide: {
additionalInformation,
confirmDangerMessage,

View File

@ -259,6 +259,9 @@ export default {
commitPath() {
return this.pipeline?.commit?.webPath || '';
},
commitTitle() {
return this.pipeline?.commit?.title || '';
},
totalJobsText() {
return sprintf(__('%{jobs} Jobs'), {
jobs: this.totalJobs,
@ -371,6 +374,9 @@ export default {
<div v-else class="gl-display-flex gl-justify-content-space-between">
<div>
<h3 v-if="name" class="gl-mt-0 gl-mb-2" data-testid="pipeline-name">{{ name }}</h3>
<h3 v-else class="gl-mt-0 gl-mb-2" data-testid="pipeline-commit-title">
{{ commitTitle }}
</h3>
<div>
<ci-badge-link :status="detailedStatus" />
<div class="gl-ml-2 gl-mb-2 gl-display-inline-block gl-h-6">

View File

@ -36,38 +36,11 @@ export default {
finishedTime() {
return this.pipeline?.details?.finished_at || this.pipeline?.finishedAt;
},
showInProgress() {
return !this.duration && !this.finishedTime && !this.skipped;
},
showSkipped() {
return !this.duration && !this.finishedTime && this.skipped;
},
skipped() {
return this.pipeline?.details?.status?.label === 'skipped';
},
stuck() {
return this.pipeline?.flags?.stuck;
},
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column time-ago" :class="fontSize">
<span
v-if="showInProgress"
class="gl-display-inline-flex gl-align-items-center"
data-testid="pipeline-in-progress"
>
<gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" />
<gl-icon v-else name="hourglass" class="gl-mr-2" :size="12" data-testid="hourglass-icon" />
{{ s__('Pipeline|In progress') }}
</span>
<span v-if="showSkipped" data-testid="pipeline-skipped">
<gl-icon name="status_skipped_borderless" />
{{ s__('Pipeline|Skipped') }}
</span>
<p v-if="duration" class="duration gl-display-inline-flex gl-align-items-center">
<gl-icon name="timer" class="gl-mr-2" :size="12" />
{{ durationFormatted }}

View File

@ -35,6 +35,7 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
commit {
id
shortId
title
webPath
}
finishedAt

View File

@ -1,7 +1,6 @@
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { RUNNING } from './constants';
import { RUNNING, WILL_DEPLOY } from './constants';
export default {
name: 'DeploymentActionButton',
@ -42,40 +41,50 @@ export default {
},
computed: {
isActionInProgress() {
return Boolean(this.computedDeploymentStatus === RUNNING || this.actionInProgress);
},
actionInProgressTooltip() {
switch (this.actionInProgress) {
case this.actionsConfiguration.actionName:
return this.actionsConfiguration.busyText;
case null:
return '';
default:
return __('Another action is currently in progress');
}
return Boolean(
this.computedDeploymentStatus === RUNNING ||
this.computedDeploymentStatus === WILL_DEPLOY ||
this.actionInProgress,
);
},
isLoading() {
return this.actionInProgress === this.actionsConfiguration.actionName;
return (
this.actionInProgress === this.actionsConfiguration.actionName ||
this.computedDeploymentStatus === WILL_DEPLOY
);
},
},
};
</script>
<template>
<span v-gl-tooltip :title="actionInProgressTooltip" class="gl-display-inline-block" tabindex="0">
<gl-button
v-gl-tooltip
category="primary"
size="small"
:title="buttonTitle"
:aria-label="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
:class="`inline gl-ml-3 ${containerClasses}`"
:icon="icon"
@click="$emit('click')"
>
<slot> </slot>
</gl-button>
</span>
<gl-button
v-if="isLoading || isActionInProgress"
category="primary"
size="small"
:title="buttonTitle"
:aria-label="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
:class="`inline gl-ml-3 ${containerClasses}`"
:icon="icon"
@click="$emit('click')"
>
<slot> </slot>
</gl-button>
<gl-button
v-else
v-gl-tooltip.hover
category="primary"
size="small"
:title="buttonTitle"
:aria-label="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
:class="`inline gl-ml-3 ${containerClasses}`"
:icon="icon"
@click="$emit('click')"
>
<slot> </slot>
</gl-button>
</template>

View File

@ -71,11 +71,25 @@ export default {
return this.deployment.details?.playable_build?.play_path;
},
redeployPath() {
if (this.redeployMrWidgetFeatureFlagEnabled) {
return this.deployment.retry_url;
}
return this.deployment.details?.playable_build?.retry_path;
},
stopUrl() {
return this.deployment.stop_url;
},
environmentAvailable() {
return Boolean(this.deployment.environment_available);
},
redeployMrWidgetFeatureFlagEnabled() {
return this.glFeatures.reviewAppsRedeployMrWidget;
},
showDeploymentActionButton() {
return (
this.redeployPath && !this.environmentAvailable && this.redeployMrWidgetFeatureFlagEnabled
);
},
},
actionsConfiguration: {
[STOPPING]: {
@ -124,6 +138,10 @@ export default {
MRWidgetService.executeInlineAction(endpoint)
.then((resp) => {
if (this.redeployMrWidgetFeatureFlagEnabled) {
return;
}
const redirectUrl = resp?.data?.redirect_url;
if (redirectUrl) {
visitUrl(redirectUrl);
@ -167,7 +185,7 @@ export default {
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-action-button
v-if="canBeManuallyRedeployed"
v-if="canBeManuallyRedeployed && !redeployMrWidgetFeatureFlagEnabled"
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
@ -178,12 +196,12 @@ export default {
<span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-view-button
v-if="hasExternalUrls"
v-if="hasExternalUrls && environmentAvailable"
:app-button-text="appButtonText"
:deployment="deployment"
/>
<deployment-action-button
v-if="stopUrl"
v-if="stopUrl && environmentAvailable"
:action-in-progress="actionInProgress"
:computed-deployment-status="computedDeploymentStatus"
:actions-configuration="$options.actionsConfiguration[constants.STOPPING]"
@ -192,5 +210,15 @@ export default {
container-classes="js-stop-env"
@click="stopEnvironment"
/>
<deployment-action-button
v-if="showDeploymentActionButton"
:action-in-progress="actionInProgress"
:computed-deployment-status="computedDeploymentStatus"
:actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
:button-title="$options.actionsConfiguration[constants.REDEPLOYING].buttonText"
:icon="$options.btnIcons.repeat"
container-classes="js-redeploy-action"
@click="redeploy"
/>
</div>
</template>

View File

@ -2,6 +2,7 @@
import { GlDrawer, GlInfiniteScroll, GlResizeObserverDirective } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import Tracking from '~/tracking';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
import Feature from './feature.vue';
import SkeletonLoader from './skeleton_loader.vue';
@ -28,6 +29,9 @@ export default {
},
computed: {
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']),
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
},
mounted() {
this.openDrawer(this.versionDigest);
@ -69,6 +73,7 @@ export default {
ref="drawer"
v-gl-resize-observer="handleResize"
class="whats-new-drawer gl-reset-line-height"
:header-height="getDrawerHeaderHeight"
:z-index="700"
:open="open"
@close="closeDrawer"

View File

@ -1,5 +1,4 @@
.whats-new-drawer {
margin-top: calc(#{$header-height} + #{$calc-application-bars-height});
@include gl-shadow-none;
overflow-y: hidden;
width: 500px;

View File

@ -301,10 +301,6 @@ $tabs-holder-z-index: 250;
}
.tree-list-icon {
top: 50%;
left: 10px;
transform: translateY(-50%);
&,
svg {
fill: var(--gray-400, $gray-400);

View File

@ -52,6 +52,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:auto_merge_labels_mr_widget, project)
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
push_frontend_feature_flag(:mr_activity_filters, current_user)
push_frontend_feature_flag(:review_apps_redeploy_mr_widget, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :discussions]

View File

@ -41,6 +41,7 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:synchronize_fork, @project&.fork_source)
push_frontend_feature_flag(:remove_monitor_metrics, @project)
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:ci_namespace_catalog_experimental, @project)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Resolvers
module AuditEvents
class AuditEventDefinitionsResolver < BaseResolver
type [Types::AuditEvents::DefinitionType], null: false
def resolve
Gitlab::Audit::Type::Definition.definitions.values
end
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
module Types
module AuditEvents
class DefinitionType < ::Types::BaseObject
graphql_name 'AuditEventDefinition'
description 'Represents the YAML definitions for audit events defined ' \
'under ee/config/audit_events/types/<event-type-name>.yml ' \
'and config/audit_events/types/<event-type-name>.yml.'
authorize :audit_event_definitions
field :name, GraphQL::Types::String,
null: false,
description: 'Key name of the audit event.'
field :description, GraphQL::Types::String,
null: false,
description: 'Description of what action the audit event tracks.'
field :introduced_by_issue, GraphQL::Types::String,
null: true,
description: 'Link to the issue introducing the event. For older' \
'audit events, it can be a commit URL rather than a' \
'merge request URL.'
field :introduced_by_mr, GraphQL::Types::String,
null: true,
description: 'Link to the merge request introducing the event. For' \
'older audit events, it can be a commit URL rather than' \
'a merge request URL.'
field :feature_category, GraphQL::Types::String,
null: false,
description: 'Feature category associated with the event.'
field :milestone, GraphQL::Types::String,
null: false,
description: 'Milestone the event was introduced in.'
field :saved_to_database, GraphQL::Types::Boolean,
null: false,
description: 'Indicates if the event is saved to PostgreSQL database.'
field :streamed, GraphQL::Types::Boolean,
null: false,
description: 'Indicates if the event is streamed to an external destination.'
end
end
end

View File

@ -165,6 +165,12 @@ module Types
alpha: { milestone: '15.1' },
description: 'Find a work item.'
field :audit_event_definitions,
Types::AuditEvents::DefinitionType.connection_type,
null: false,
description: 'Definitions for all audit events available on the instance.',
resolver: Resolvers::AuditEvents::AuditEventDefinitionsResolver
def design_management
DesignManagementObject.new(nil)
end

View File

@ -3,6 +3,10 @@
module Ci
module Catalog
module ResourcesHelper
def can_add_catalog_resource?(_project)
false
end
def can_view_namespace_catalog?(_project)
false
end

View File

@ -87,8 +87,6 @@ module NavHelper
end
def show_super_sidebar?(user = current_user)
return false unless Feature.enabled?(:super_sidebar_nav, user)
# The new sidebar is not enabled for anonymous use
# Once we enable the new sidebar by default, this
# should return true

View File

@ -421,8 +421,9 @@ module ProjectsHelper
packagesAvailable: ::Gitlab.config.packages.enabled,
packagesHelpPath: help_page_path('user/packages/index'),
currentSettings: project_permissions_settings(project),
canDisableEmails: can_disable_emails?(project, current_user),
canAddCatalogResource: can_add_catalog_resource?(project),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
canDisableEmails: can_disable_emails?(project, current_user),
allowedVisibilityOptions: project_allowed_visibility_levels(project),
visibilityHelpPath: help_page_path('user/public_access'),
registryAvailable: Gitlab.config.registry.enabled,

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module AuditEvents
class DefinitionPolicy < ::BasePolicy
condition(:read_audit_events_definitions_enabled) do
true
end
rule { read_audit_events_definitions_enabled }.enable :audit_event_definitions
end
end

View File

@ -42,9 +42,8 @@
%li.d-md-none
= link_to _("Switch to GitLab Next"), Gitlab::Saas.canary_toggle_com_url, data: { track_action: "click_link", track_label: "switch_to_canary", track_property: "navigation_top" }
- if Feature.enabled?(:super_sidebar_nav, current_user)
%li.divider
.js-new-nav-toggle{ data: { enabled: show_super_sidebar?.to_s, endpoint: profile_preferences_url} }
%li.divider
.js-new-nav-toggle{ data: { enabled: show_super_sidebar?.to_s, endpoint: profile_preferences_url} }
- if current_user_menu?(:sign_out)
%li.divider

View File

@ -1,8 +0,0 @@
---
name: super_sidebar_nav
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101910
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381456
milestone: '15.7'
type: development
group: group::foundations
default_enabled: true

View File

@ -18,7 +18,7 @@ if Rails.env.production?
else
ActiveSupport::Deprecation.silenced = false
ActiveSupport::Deprecation.behavior = [:stderr, :notify]
ActiveSupport::Deprecation.disallowed_behavior = :raise
ActiveSupport::Deprecation.disallowed_behavior = [:stderr, :raise]
rails7_deprecation_warnings = [
# https://gitlab.com/gitlab-org/gitlab/-/issues/366910

View File

@ -33,6 +33,8 @@ swap:
n/a: "not applicable"
navigate to: "go to"
OAuth2: "OAuth 2.0"
omnibus gitlab: "Linux package"
'omnibus(?!\))': "Linux package"
once that: "after that"
once the: "after the"
once you: "after you"

View File

@ -18,7 +18,7 @@ swap:
GitLabber: GitLab team member
GitLabbers: GitLab team members
GitLab-shell: GitLab Shell
gitlab omnibus: Omnibus GitLab
gitlab omnibus: Linux package
param: parameter
params: parameters
pg: PostgreSQL

View File

@ -57,6 +57,16 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="queryaimessagesrequestids"></a>`requestIds` | [`[ID!]`](#id) | Array of request IDs to fetch. |
| <a id="queryaimessagesroles"></a>`roles` | [`[AiCachedMessageRole!]`](#aicachedmessagerole) | Array of roles to fetch. |
### `Query.auditEventDefinitions`
Definitions for all audit events available on the instance.
Returns [`AuditEventDefinitionConnection!`](#auditeventdefinitionconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
### `Query.boardList`
Find an issue board list.
@ -7411,6 +7421,29 @@ The edge type for [`ApprovalProjectRule`](#approvalprojectrule).
| <a id="approvalprojectruleedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="approvalprojectruleedgenode"></a>`node` | [`ApprovalProjectRule`](#approvalprojectrule) | The item at the end of the edge. |
#### `AuditEventDefinitionConnection`
The connection type for [`AuditEventDefinition`](#auditeventdefinition).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="auditeventdefinitionconnectionedges"></a>`edges` | [`[AuditEventDefinitionEdge]`](#auditeventdefinitionedge) | A list of edges. |
| <a id="auditeventdefinitionconnectionnodes"></a>`nodes` | [`[AuditEventDefinition]`](#auditeventdefinition) | A list of nodes. |
| <a id="auditeventdefinitionconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `AuditEventDefinitionEdge`
The edge type for [`AuditEventDefinition`](#auditeventdefinition).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="auditeventdefinitionedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="auditeventdefinitionedgenode"></a>`node` | [`AuditEventDefinition`](#auditeventdefinition) | The item at the end of the edge. |
#### `AuditEventStreamingHeaderConnection`
The connection type for [`AuditEventStreamingHeader`](#auditeventstreamingheader).
@ -12073,6 +12106,23 @@ Represents a vulnerability asset type.
| <a id="assettypetype"></a>`type` | [`String!`](#string) | Type of the asset. |
| <a id="assettypeurl"></a>`url` | [`String!`](#string) | URL of the asset. |
### `AuditEventDefinition`
Represents the YAML definitions for audit events defined under ee/config/audit_events/types/<event-type-name>.yml and config/audit_events/types/<event-type-name>.yml.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="auditeventdefinitiondescription"></a>`description` | [`String!`](#string) | Description of what action the audit event tracks. |
| <a id="auditeventdefinitionfeaturecategory"></a>`featureCategory` | [`String!`](#string) | Feature category associated with the event. |
| <a id="auditeventdefinitionintroducedbyissue"></a>`introducedByIssue` | [`String`](#string) | Link to the issue introducing the event. For olderaudit events, it can be a commit URL rather than amerge request URL. |
| <a id="auditeventdefinitionintroducedbymr"></a>`introducedByMr` | [`String`](#string) | Link to the merge request introducing the event. Forolder audit events, it can be a commit URL rather thana merge request URL. |
| <a id="auditeventdefinitionmilestone"></a>`milestone` | [`String!`](#string) | Milestone the event was introduced in. |
| <a id="auditeventdefinitionname"></a>`name` | [`String!`](#string) | Key name of the audit event. |
| <a id="auditeventdefinitionsavedtodatabase"></a>`savedToDatabase` | [`Boolean!`](#boolean) | Indicates if the event is saved to PostgreSQL database. |
| <a id="auditeventdefinitionstreamed"></a>`streamed` | [`Boolean!`](#boolean) | Indicates if the event is streamed to an external destination. |
### `AuditEventStreamingHeader`
Represents a HTTP header key/value that belongs to an audit streaming destination.
@ -13244,6 +13294,35 @@ Returns [`CommitParentNames`](#commitparentnames).
| ---- | ---- | ----------- |
| <a id="commitreferencestippingtagslimit"></a>`limit` | [`Int!`](#int) | Number of ref names to return. |
### `ComparedSecurityReport`
Represents compared security report.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="comparedsecurityreportadded"></a>`added` **{warning-solid}** | [`[ComparedSecurityReportFinding!]`](#comparedsecurityreportfinding) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. New vulnerability findings. |
| <a id="comparedsecurityreportbasereportcreatedat"></a>`baseReportCreatedAt` | [`Time`](#time) | Time of the base report creation. |
| <a id="comparedsecurityreportbasereportoutofdate"></a>`baseReportOutOfDate` | [`Boolean`](#boolean) | Indicates whether the base report out of date. |
| <a id="comparedsecurityreportfixed"></a>`fixed` **{warning-solid}** | [`[ComparedSecurityReportFinding!]`](#comparedsecurityreportfinding) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Fixed vulnerability findings. |
| <a id="comparedsecurityreportheadreportcreatedat"></a>`headReportCreatedAt` | [`Time`](#time) | Time of the base report creation. |
### `ComparedSecurityReportFinding`
Represents finding.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="comparedsecurityreportfindingdescription"></a>`description` | [`String`](#string) | Description of the vulnerability finding. |
| <a id="comparedsecurityreportfindingfoundbypipelineiid"></a>`foundByPipelineIid` | [`String`](#string) | IID of the pipeline. |
| <a id="comparedsecurityreportfindingseverity"></a>`severity` | [`VulnerabilitySeverity`](#vulnerabilityseverity) | Severity of the vulnerability finding. |
| <a id="comparedsecurityreportfindingstate"></a>`state` | [`VulnerabilityState`](#vulnerabilitystate) | Finding status. |
| <a id="comparedsecurityreportfindingtitle"></a>`title` | [`String`](#string) | Title of the vulnerability finding. |
| <a id="comparedsecurityreportfindinguuid"></a>`uuid` | [`String`](#string) | UUIDv5 digest based on the vulnerability's report type, primary identifier, location, fingerprint, project identifier. |
### `ComplianceFramework`
Represents a ComplianceFramework associated with a Project.
@ -15054,6 +15133,18 @@ Describes an external status check.
| <a id="fileuploadpath"></a>`path` | [`String!`](#string) | Path of the upload. |
| <a id="fileuploadsize"></a>`size` | [`Int!`](#int) | Size of the upload in bytes. |
### `FindingReportsComparer`
Represents security reports comparison for vulnerability findings.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="findingreportscomparerreport"></a>`report` **{warning-solid}** | [`ComparedSecurityReport`](#comparedsecurityreport) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Compared security report. |
| <a id="findingreportscomparerstatus"></a>`status` | [`FindingReportsComparerStatus`](#findingreportscomparerstatus) | Comparison status. |
| <a id="findingreportscomparerstatusreason"></a>`statusReason` | [`String`](#string) | Text explaining the status. |
### `Forecast`
Information about specific forecast created.
@ -17314,6 +17405,22 @@ Returns [`[DiffStats!]`](#diffstats).
| ---- | ---- | ----------- |
| <a id="mergerequestdiffstatspath"></a>`path` | [`String`](#string) | Specific file path. |
##### `MergeRequest.findingReportsComparer`
Vulnerability finding reports comparison reported on the merge request.
WARNING:
**Introduced** in 16.1.
This feature is an Experiment. It can be changed or removed at any time.
Returns [`FindingReportsComparer`](#findingreportscomparer).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestfindingreportscomparerreporttype"></a>`reportType` | [`ComparableSecurityReportType!`](#comparablesecurityreporttype) | Filter vulnerability findings by report type. |
##### `MergeRequest.pipelines`
Pipelines for the merge request. Note: for performance reasons, no more than the most recent 500 pipelines will be returned.
@ -24450,6 +24557,20 @@ Mode of a commit action.
| <a id="commitencodingbase64"></a>`BASE64` | Base64 encoding. |
| <a id="commitencodingtext"></a>`TEXT` | Text encoding. |
### `ComparableSecurityReportType`
Comparable security report type.
| Value | Description |
| ----- | ----------- |
| <a id="comparablesecurityreporttypeapi_fuzzing"></a>`API_FUZZING` | API Fuzzing report. |
| <a id="comparablesecurityreporttypecontainer_scanning"></a>`CONTAINER_SCANNING` | Container Scanning report. |
| <a id="comparablesecurityreporttypecoverage_fuzzing"></a>`COVERAGE_FUZZING` | Coverage Fuzzing report. |
| <a id="comparablesecurityreporttypedast"></a>`DAST` | DAST report. |
| <a id="comparablesecurityreporttypedependency_scanning"></a>`DEPENDENCY_SCANNING` | Dependency Scanning report. |
| <a id="comparablesecurityreporttypesast"></a>`SAST` | SAST report. |
| <a id="comparablesecurityreporttypesecret_detection"></a>`SECRET_DETECTION` | Secret Detection report. |
### `ComplianceFrameworkPresenceFilter`
ComplianceFramework of a project for filtering.
@ -24965,6 +25086,16 @@ Event action.
| <a id="eventactionreopened"></a>`REOPENED` | Reopened action. |
| <a id="eventactionupdated"></a>`UPDATED` | Updated action. |
### `FindingReportsComparerStatus`
Report comparison status.
| Value | Description |
| ----- | ----------- |
| <a id="findingreportscomparerstatuserror"></a>`ERROR` | An error happened while generating the report. |
| <a id="findingreportscomparerstatusparsed"></a>`PARSED` | Report is generated. |
| <a id="findingreportscomparerstatusparsing"></a>`PARSING` | Report is being generated. |
### `ForecastStatus`
List of statuses for forecasting model.

View File

@ -190,13 +190,12 @@ After components are added to a components repository, they can immediately be [
However, this repository is not discoverable. You must mark this project as a catalog resource to allow it to be visible in the CI Catalog
so other users can discover it.
To mark a project as a catalog resource, run the following [graphQL](../../api/graphql/index.md)
mutation:
To mark a project as a catalog resource:
```graphql
mutation {
catalogResourcesCreate(input: { projectPath: "path-to-project"}) {
errors
}
}
```
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Settings > General**.
1. Expand **Visibility, project features, permissions**.
1. Scroll down to **CI/CD Catalog resource** and select the toggle to mark the project as a catalog resource.
NOTE:
This action is not reversible.

View File

@ -180,7 +180,7 @@ sentry_dsn = "X"
MachineOptions = [
"google-project=PROJECT",
"google-disk-size=25",
"google-machine-type=n1-standard-1",
"google-machine-type=n2d-standard-2",
"google-username=core",
"google-tags=gitlab-com,srm",
"google-use-internal-ip",

View File

@ -68,7 +68,7 @@ All AI features are experimental.
1. Enable the specific feature flag for the feature you want to test
1. Set the required access token. To receive an access token:
1. For Vertex, follow the [instructions below](#configure-gcp-vertex-access).
1. For all other providers, create an access request where `@m_gill`, `@wayne`, and `@timzallmann` are the tech stack owners.
1. For all other providers, like Anthropic or OpenAI, create an access request where `@m_gill`, `@wayne`, and `@timzallmann` are the tech stack owners.
### Set up the embedding database
@ -84,7 +84,7 @@ For features that use the embedding database, additional setup is needed.
1. Run `gdk reconfigure`
1. Run database migrations to create the embedding database
### Setup for GitLab chat
### Setup for GitLab documentation chat (legacy chat)
To populate the embedding database for GitLab chat:

View File

@ -28,12 +28,23 @@ The reviewer can:
- Give you a second opinion on the chosen solution and implementation.
- Help look for bugs, logic problems, or uncovered edge cases.
If the merge request is trivial to review (for example, fixing a typo or a tiny refactor that doesn't change the behavior or any data),
you can skip the reviewer step and directly ask a [maintainer](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer).
Otherwise, a merge request should always be first reviewed by a reviewer in each
[category (e.g. backend, database)](#approval-guidelines)
the MR touches, as maintainers may not have the relevant domain knowledge, and
also to spread the workload.
If the merge request is small and straightforward to review, you can skip the reviewer step and
directly ask a
[maintainer](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer).
What constitutes "small and straightforward" is a gray area. Here are
some examples of small and straightforward changes:
- Fixing a typo or making small copy changes ([example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121337#note_1399406719)).
- A tiny refactor that doesn't change any behavior or data.
- Removing references to a feature flag that has been default enabled for > 1 month.
- Removing unused methods or classes.
- A well-understood logic change that requires changes to < 5 lines of code.
Otherwise, a merge request should be first reviewed by a reviewer in each
[category (for example: backend, database)](#approval-guidelines)
the MR touches, as maintainers may not have the relevant domain knowledge. This
also helps to spread the workload.
For assistance with security scans or comments, include the Application Security Team (`@gitlab-com/gl-security/appsec`).
@ -51,7 +62,9 @@ Some domain areas (like `Verify`) require an approval from a domain expert, base
CODEOWNERS rules. Because CODEOWNERS sections are independent approval rules, we could have certain
rules (for example `Verify`) that may be a subset of other more generic approval rules (for example `backend`).
For a more efficient process, authors should look for domain-specific approvals before generic approvals.
Domain-specific approvers may also be maintainers, and if so they should review the domain specifics and broader change at the same time and approve once for both roles.
Domain-specific approvers may also be maintainers, and if so they should review
the domain specifics and broader change at the same time and approve once for
both roles.
Read more about [author responsibilities](#the-responsibility-of-the-merge-request-author) below.
@ -180,14 +193,14 @@ by a reviewer before passing it to a maintainer as described in the
| A new service to GitLab (Puma, Sidekiq, Gitaly are examples) | [Product manager](https://about.gitlab.com/company/team/). See the [process for adding a service component to GitLab](adding_service_component.md) for details. |
| Changes related to authentication or authorization | [Manage:Authentication and Authorization team member](https://about.gitlab.com/company/team/). Check the [code review section on the group page](https://about.gitlab.com/handbook/engineering/development/dev/manage/authentication-and-authorization/#additional-considerations) for more details. Patterns for files known to require review from the team are listed in the in the `Authentication and Authorization` section of the [`CODEOWNERS`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/CODEOWNERS) file, and the team will be listed in the approvers section of all merge requests that modify these files. |
- (*1*): Specs other than JavaScript specs are considered `~backend` code. Haml markup is considered `~frontend` code. However, Ruby code within Haml templates is considered `~backend` code. When in doubt, request both a frontend and backend review.
- (*1*): Specs other than JavaScript specs are considered `~backend` code. Haml markup is considered `~frontend` code. However, Ruby code in Haml templates is considered `~backend` code. When in doubt, request both a frontend and backend review.
- (*2*): We encourage you to seek guidance from a database maintainer if your merge
request is potentially introducing expensive queries. It is most efficient to comment
on the line of code in question with the SQL queries so they can give their advice.
- (*3*): User-facing changes include both visual changes (regardless of how minor),
and changes to the rendered DOM which impact how a screen reader may announce
the content.
- (*4*): End-to-end changes include all files within the `qa` directory.
- (*4*): End-to-end changes include all files in the `qa` directory.
#### Acceptance checklist
@ -382,7 +395,7 @@ codebase, and not that of any specific domain, they can review, approve, and mer
merge requests from any team and in any product area.
Maintainers are the DRI of assuring that the acceptance criteria of a merge request are reasonably met.
In general, [quality is everyones responsibility](https://about.gitlab.com/handbook/engineering/quality/),
In general, [quality is everyone's responsibility](https://about.gitlab.com/handbook/engineering/quality/),
but maintainers of an MR are held responsible for **ensuring** that an MR meets those general quality standards.
If a maintainer feels that an MR is substantial enough, or requires a [domain expert](#domain-experts),

View File

@ -15,10 +15,7 @@ the sidebar is a work in progress, and so is this documentation.
## Enable the new navigation sidebar
To enable the new navigation sidebar:
- Enable the `super_sidebar_nav` feature flag.
- Select your avatar, then turn on the **New navigation** toggle.
To enable the new navigation sidebar, select your avatar, then turn on the **New navigation** toggle.
## Adding items to the sidebar

View File

@ -481,6 +481,50 @@ for how it works.
committed. More context can be found at:
[Setting it to `false` to skip it](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118938#note_1374688877)
##### Why do we have both the mirror project and validation project?
We have separate projects for a several reasons.
- **Security**: Previously, we had the mirror project only. However, to fully
mitigate a [security issue](https://gitlab.com/gitlab-org/gitlab/-/issues/369898),
we had to make the mirror project private.
- **Isolation**: We want to run JH code in a completely isolated and standalone project.
We should not run it under the `gitlab-org` group, which is where the mirror
project is. The validation project is completely isolated.
- **Cost**: We don't want to connect to JiHuLab.com from each merge request.
It is more cost effective to mirror the code from JiHuLab.com to
somewhere at GitLab.com, and have our merge requests fetch code from there.
This means that the validation project can fetch code from the mirror, rather
than from JiHuLab.com. The mirror project will periodically fetch from
JiHuLab.com.
- **Branch separation/security/efficiency**: We want to mirror all branches,
so that we can fetch the corresponding JH branch from JiHuLab.com. However,
we don't want to overwrite the `as-if-jh-code-sync` branch in the validation project,
because we use it to control the validation pipeline and it has access to
`AS_IF_JH_TOKEN`. However, we cannot mirror all branches except a single
one. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/413032) for details.
Given this issue, the validation project is set to only mirror `master` and
`main-jh`. Technically, we don't even need those branches, but we do want to
keep the repository up-to-date with all the default branches so that when
we push changes from the merge request, we only need to push changes from
the merge request, which can be more efficient.
- Separation of concerns:
- Validation project only has the following branches:
- `master` and `main-jh` to keep changes up-to-date.
- `as-if-jh-code-sync` for dependency synchronization.
We should never mirror this.
- `as-if-jh/*` branches from the merge requests.
We should never mirror these.
- All branches from the mirror project are all coming from JiHuLab.com.
We never push anything to the mirror project, nor does it run any
pipelines. CI/CD is disabled in the mirror project.
We can consider merging the two projects to simplify the
setup and process, but we need to make sure that all of these reasons
are no longer concerns.
### `rspec:undercoverage` job
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74859) in GitLab 14.6.

View File

@ -70,7 +70,7 @@ Bot users for groups are service accounts and do not count as licensed seats.
You can use the [group access tokens API](../api/group_access_tokens.md) to
programmatically take action, such as
[rotating a project access token](../api/group_access_tokens.md#rotate-a-group-access-token).
[rotating a group access token](../api/group_access_tokens.md#rotate-a-group-access-token).
## Deploy tokens

View File

@ -444,7 +444,7 @@ However, as discussed in [the section about rebasing](#squashing-commits-with-re
Rebasing could create more work, as every time you rebase, you may need to resolve the same conflicts.
Sometimes you can reuse recorded resolutions (`rerere`), but merging is better, because you only have to resolve conflicts once.
You can read a more thorough explanation of the tradeoffs between merging and rebasing [here](https://git-scm.com/book/en/v2/Git-Branching-Rebasing#:~:text=Final%20commit%20history-,The,-Perils%20of%20Rebasing).
The Git documentation has a thorough explanation of the [tradeoffs between merging and rebasing](https://git-scm.com/book/en/v2/Git-Branching-Rebasing#:~:text=Final%20commit%20history-,The,-Perils%20of%20Rebasing).
A good way to prevent creating many merge commits is to not frequently merge `main` into the feature branch.
Three reasons to merge in `main`:
@ -543,9 +543,17 @@ In GitLab Flow, your can include automated CI tests in your branch or merge requ
## Working with feature branches
When creating a feature branch, always branch from an up-to-date `main`.
If you know before you start that your work depends on another branch, you can also branch from there.
If you need to merge in another branch after starting, explain the reason in the merge commit.
If you have not pushed your commits to a shared location yet, you can also incorporate changes by rebasing on `main` or another feature branch.
Do not merge from upstream again if your code can work and merge cleanly without doing so.
Merging only when needed prevents creating merge commits in your feature branch that later end up littering the `main` history.
Some tips for working with feature branches:
- When you create a feature branch locally, always update your local copy of `main` before
branching off from it.
- When creating a feature branch, always branch from `main` unless you know your work
depends on some other branch. For example, to create `feature-x-update`, branch from
`feature-x` instead of `main`.
- If you merge in another branch after starting, explain the reason in the merge commit.
- If you have not pushed your branch upstream yet, you can still pull in new changes
by rebasing your local feature branch against your local copy of its parent branch.
- Do not merge recent changes from other branches into your local feature branch if your code
can work and merge cleanly without those extra changes. Each time you merge commits into your
feature branch, you add a merge commit to your feature branch. These merge commits
later end up littering the history in your `main` branch.

View File

@ -42,7 +42,7 @@ export GITLAB_ACTIVATION_CODE=your_activation_code
If you have a license, you can also import it when you install GitLab.
- **For installations from source**
- For self-compiled installations:
- Place the `Gitlab.gitlab-license` file in the `config/` directory.
- To specify a custom location and filename for the license, set the
`GITLAB_LICENSE_FILE` environment variable with the path to the file:
@ -51,7 +51,7 @@ If you have a license, you can also import it when you install GitLab.
export GITLAB_LICENSE_FILE="/path/to/license/file"
```
- **For Omnibus package**
- For Linux package installations:
- Place the `Gitlab.gitlab-license` file in the `/etc/gitlab/` directory.
- To specify a custom location and filename for the license, add this entry to `gitlab.rb`:

View File

@ -352,7 +352,7 @@ error, the [max attachment size](#max-attachment-size)
is probably larger than the web server's allowed value.
To increase the max attachment size to 200 MB in a
[Omnibus GitLab](https://docs.gitlab.com/omnibus/) install, you may need to
[Linux package](https://docs.gitlab.com/omnibus/) install, you may need to
add the line below to `/etc/gitlab/gitlab.rb` before increasing the max attachment size:
```ruby

View File

@ -32,12 +32,12 @@ authorization service.
Whenever access is granted or denied this is logged in a log file called
`external-policy-access-control.log`. Read more about the logs GitLab keeps in
the [Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/settings/logs.html).
the [Linux package documentation](https://docs.gitlab.com/omnibus/settings/logs.html).
When using TLS Authentication with a self signed certificate, the CA certificate
needs to be trusted by the OpenSSL installation. When using GitLab installed
using Omnibus, learn to install a custom CA in the
[Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/settings/ssl/index.html).
using the Linux package, learn to install a custom CA in the
[Linux package documentation](https://docs.gitlab.com/omnibus/settings/ssl/index.html).
Alternatively, learn where to install custom certificates by using
`openssl version -d`.

View File

@ -131,7 +131,7 @@ GitLab. For example:
- Set `Gitlab-Bypass-Rate-Limiting` to a value other than `1` on all requests that
should be affected by rate limiting.
1. Set the environment variable `GITLAB_THROTTLE_BYPASS_HEADER`.
- For [Omnibus](https://docs.gitlab.com/omnibus/settings/environment-variables.html),
- For [Linux package installations](https://docs.gitlab.com/omnibus/settings/environment-variables.html),
set `'GITLAB_THROTTLE_BYPASS_HEADER' => 'Gitlab-Bypass-Rate-Limiting'` in `gitlab_rails['env']`.
- For source installations, set `export GITLAB_THROTTLE_BYPASS_HEADER=Gitlab-Bypass-Rate-Limiting`
in `/etc/default/gitlab`.
@ -163,7 +163,7 @@ the `GITLAB_THROTTLE_USER_ALLOWLIST` environment variable. If you want
users 1, 53 and 217 to bypass the authenticated request rate limiter,
the allowlist configuration would be `1,53,217`.
- For [Omnibus](https://docs.gitlab.com/omnibus/settings/environment-variables.html),
- For [Linux package installations](https://docs.gitlab.com/omnibus/settings/environment-variables.html),
set `'GITLAB_THROTTLE_USER_ALLOWLIST' => '1,53,217'` in `gitlab_rails['env']`.
- For source installations, set `export GITLAB_THROTTLE_USER_ALLOWLIST=1,53,217`
in `/etc/default/gitlab`.

View File

@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) the `ci_access` attribute in GitLab 14.3.
> - The ability to authorize groups was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) to GitLab Free in 14.5.
> - Support for Omnibus installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5.
> - Support for Linux package installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5.
> - The ability to switch between certificate-based clusters and agents was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335089) in GitLab 14.9. The certificate-based cluster context is always called `gitlab-deploy`.
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80508) from _CI/CD tunnel_ to _CI/CD workflow_ in GitLab 14.9.

View File

@ -468,7 +468,7 @@ and can't be configured on GitLab.com to expire. You can erase job logs
## GitLab.com at scale
In addition to the GitLab Enterprise Edition Omnibus install, GitLab.com uses
In addition to the GitLab Enterprise Edition Linux package install, GitLab.com uses
the following applications and settings to achieve scale. All settings are
publicly available, as [Kubernetes configuration](https://gitlab.com/gitlab-com/gl-infra/k8s-workloads/gitlab-com)
or [Chef cookbooks](https://gitlab.com/gitlab-cookbooks).

View File

@ -108,7 +108,7 @@ Configure FortiAuthenticator in GitLab. On your GitLab server:
1. Open the configuration file.
For Omnibus GitLab:
For Linux package installations:
```shell
sudo editor /etc/gitlab/gitlab.rb
@ -123,7 +123,7 @@ Configure FortiAuthenticator in GitLab. On your GitLab server:
1. Add the provider configuration:
For Omnibus package:
For Linux package installations:
```ruby
gitlab_rails['forti_authenticator_enabled'] = true
@ -145,8 +145,8 @@ Configure FortiAuthenticator in GitLab. On your GitLab server:
```
1. Save the configuration file.
1. [Reconfigure](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) (Omnibus GitLab) or
[restart](../../../administration/restart_gitlab.md#installations-from-source) (GitLab installed from source).
1. [Reconfigure](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) (Linux package) or
[restart](../../../administration/restart_gitlab.md#installations-from-source) (self-compiled installations).
### Enable one-time password using Duo
@ -174,7 +174,7 @@ On your GitLab server:
1. Open the configuration file.
For Omnibus GitLab:
For Linux package installations:
```shell
sudo editor /etc/gitlab/gitlab.rb
@ -189,7 +189,7 @@ On your GitLab server:
1. Add the provider configuration:
For Omnibus package:
For Linux package installations:
```ruby
gitlab_rails['duo_auth_enabled'] = false
@ -209,7 +209,7 @@ On your GitLab server:
```
1. Save the configuration file.
1. For Omnibus GitLab, [reconfigure GitLab](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure).
1. For Linux package installations, [reconfigure GitLab](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure).
For installations from source, [restart GitLab](../../../administration/restart_gitlab.md#installations-from-source).
### Enable one-time password using FortiToken Cloud
@ -233,7 +233,7 @@ Configure FortiToken Cloud in GitLab. On your GitLab server:
1. Open the configuration file.
For Omnibus GitLab:
For Linux package installations:
```shell
sudo editor /etc/gitlab/gitlab.rb
@ -248,7 +248,7 @@ Configure FortiToken Cloud in GitLab. On your GitLab server:
1. Add the provider configuration:
For Omnibus package:
For Linux package installations:
```ruby
gitlab_rails['forti_token_cloud_enabled'] = true
@ -266,8 +266,8 @@ Configure FortiToken Cloud in GitLab. On your GitLab server:
```
1. Save the configuration file.
1. [Reconfigure](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) (Omnibus GitLab) or
[restart](../../../administration/restart_gitlab.md#installations-from-source) (GitLab installed from source).
1. [Reconfigure](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) (Linux package) or
[restart](../../../administration/restart_gitlab.md#installations-from-source) (self-compiled installations).
### Set up a WebAuthn device

View File

@ -23,6 +23,8 @@ authorization provider, you do not need to choose a password. GitLab
## Change your password
> Password reset emails sent to any verified email address [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16311) in GitLab 16.1.
You can change your password. GitLab enforces [password requirements](#password-requirements) when you choose your new
password.
@ -33,8 +35,17 @@ password.
1. In the **New password** and **Password confirmation** text box, enter your new password.
1. Select **Save password**.
If you don't know your current password, select the **I forgot my password** link. A password reset email is sent to the
account's **primary** email address.
If you do not know your current password, select **I forgot my password**
and complete the form. A password reset email is sent to the email address you
enter into this form, provided that the email address is verified. If you enter an
unverified email address into this form, no email is sent, and you see the following
message:
> "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
NOTE:
Your account can have more than one verified email address, and any email address
associated with your account can be verified.
## Password requirements

View File

@ -17,9 +17,9 @@ separately configured [Mattermost notifications](mattermost.md).
GitLab provides different ways to configure Mattermost slash commands. For any of these options,
you must have Mattermost [3.4 or later](https://mattermost.com/blog/category/platform/releases/).
- **Omnibus GitLab installations**: Mattermost is bundled with
[Omnibus GitLab](https://docs.gitlab.com/omnibus/). To configure Mattermost for Omnibus GitLab,
read the [Omnibus GitLab Mattermost documentation](../../../integration/mattermost/index.md).
- **Linux package installations**: Mattermost is bundled with
[Linux package](https://docs.gitlab.com/omnibus/). To configure Mattermost for Linux package
installations, read the [Linux package Mattermost documentation](../../../integration/mattermost/index.md).
- **If Mattermost is installed on the same server as GitLab**, use the
[automated configuration](#configure-automatically).
- **For all other installations**, use the [manual configuration](#configure-manually).
@ -133,7 +133,7 @@ The available slash commands for Mattermost are:
## Related topics
- [Mattermost slash commands](https://developers.mattermost.com/integrate/slash-commands/)
- [Omnibus GitLab Mattermost](../../../integration/mattermost/index.md)
- [Linux package Mattermost](../../../integration/mattermost/index.md)
## Troubleshooting

View File

@ -238,9 +238,10 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip
NOTE:
In GitLab 15.3 and later, Service Desk uses `webhook` (internal API call) by default instead of enqueuing a Sidekiq job.
To use `webhook` on an Omnibus installation running GitLab 15.3, you must generate a secret file.
To use `webhook` on a Linux package installation running GitLab 15.3, you must generate a secret file.
For more information, see [merge request 5927](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5927).
In GitLab 15.4, reconfiguring an Omnibus installation generates this secret file automatically, so no secret file configuration setting is needed.
In GitLab 15.4, reconfiguring a Linux package installation generates this secret file automatically, so no
secret file configuration setting is needed.
For more information, see [issue 1462](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1462).
```ruby
@ -419,7 +420,7 @@ Service Desk can be configured to read Microsoft Exchange Online mailboxes with
Graph API instead of IMAP. Set up an OAuth 2.0 application for Microsoft Graph
[the same way as for incoming email](../../administration/incoming_email.md#microsoft-graph).
- Example for Omnibus GitLab installations:
- Example for Linux package installations:
```ruby
gitlab_rails['service_desk_email_enabled'] = true
@ -502,8 +503,8 @@ and the [common settings for `service_desk_email`](https://docs.gitlab.com/chart
#### Linux package (Omnibus)
In multi-node Linux package (Omnibus) environments, run `mail_room` only on one node. Run it either on a single
`rails` node (for example, [Omnibus role](https://docs.gitlab.com/omnibus/roles/index.html) `application_role`)
In multi-node Linux package installation environments, run `mail_room` only on one node. Run it either on a single
`rails` node (for example, `application_role`)
or completely separately.
##### Set up all nodes

View File

@ -13,6 +13,10 @@ module Gitlab
validate :validate_schema
validate :validate_file_name
def self.declarative_policy_class
'AuditEvents::DefinitionPolicy'
end
InvalidAuditEventTypeError = Class.new(StandardError)
AUDIT_EVENT_TYPE_SCHEMA_PATH = Rails.root.join('config', 'audit_events', 'types', 'type_schema.json')

View File

@ -6,53 +6,51 @@
# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny
# If you are interested in using Android with FastLane for publishing take a look at the Android-Fastlane template.
image: openjdk:8-jdk
image: eclipse-temurin:17-jdk-jammy
variables:
# ANDROID_COMPILE_SDK is the version of Android you're compiling with.
# It should match compileSdkVersion.
ANDROID_COMPILE_SDK: "29"
ANDROID_COMPILE_SDK: "33"
# ANDROID_BUILD_TOOLS is the version of the Android build tools you are using.
# It should match buildToolsVersion.
ANDROID_BUILD_TOOLS: "29.0.3"
ANDROID_BUILD_TOOLS: "33.0.2"
# It's what version of the command line tools we're going to download from the official site.
# Official Site-> https://developer.android.com/studio/index.html
# There, look down below at the cli tools only, sdk tools package is of format:
# commandlinetools-os_type-ANDROID_SDK_TOOLS_latest.zip
# when the script was last modified for latest compileSdkVersion, it was which is written down below
ANDROID_SDK_TOOLS: "6514223"
ANDROID_SDK_TOOLS: "9477386"
# Packages installation before running script
before_script:
- apt-get --quiet update --yes
- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
- apt-get --quiet install --yes wget unzip
# Setup path as android_home for moving/exporting the downloaded sdk into it
- export ANDROID_HOME="${PWD}/android-home"
- export ANDROID_HOME="${PWD}/android-sdk-root"
# Create a new directory at specified location
- install -d $ANDROID_HOME
# Here we are installing androidSDK tools from official source,
# (the key thing here is the url from where you are downloading these sdk tool for command line, so please do note this url pattern there and here as well)
# after that unzipping those tools and
# then running a series of SDK manager commands to install necessary android SDK packages that'll allow the app to build
- wget --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
# move to the archive at ANDROID_HOME
- pushd $ANDROID_HOME
- unzip -d cmdline-tools cmdline-tools.zip
- popd
- export PATH=$PATH:${ANDROID_HOME}/cmdline-tools/tools/bin/
- wget --no-verbose --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
- unzip -q -d "$ANDROID_HOME/cmdline-tools" "$ANDROID_HOME/cmdline-tools.zip"
- mv -T "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools"
- export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin
# Nothing fancy here, just checking sdkManager version
- sdkmanager --version
# use yes to accept all licenses
- yes | sdkmanager --sdk_root=${ANDROID_HOME} --licenses || true
- sdkmanager --sdk_root=${ANDROID_HOME} "platforms;android-${ANDROID_COMPILE_SDK}"
- sdkmanager --sdk_root=${ANDROID_HOME} "platform-tools"
- sdkmanager --sdk_root=${ANDROID_HOME} "build-tools;${ANDROID_BUILD_TOOLS}"
- yes | sdkmanager --licenses > /dev/null || true
- sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}"
- sdkmanager "platform-tools"
- sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}"
# Not necessary, but just for surity
- chmod +x ./gradlew
@ -64,6 +62,11 @@ lintDebug:
stage: build
script:
- ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint
artifacts:
paths:
- app/lint/reports/lint-results-debug.html
expose_as: "lint-report"
when: always
# Make Project
assembleDebug:
@ -77,6 +80,7 @@ assembleDebug:
# Run all tests, if any fails, interrupt the pipeline(fail it)
debugTests:
needs: [lintDebug, assembleDebug]
interruptible: true
stage: test
script:

View File

@ -5376,9 +5376,6 @@ msgstr ""
msgid "Anonymous"
msgstr ""
msgid "Another action is currently in progress"
msgstr ""
msgid "Another issue tracker is already in use. Only one issue tracker service can be active at a time"
msgstr ""
@ -9577,6 +9574,9 @@ msgstr ""
msgid "Ci config already present"
msgstr ""
msgid "CiCatalog|CI/CD Catalog resource"
msgstr ""
msgid "CiCatalog|CI/CD catalog"
msgstr ""
@ -9589,6 +9589,12 @@ msgstr ""
msgid "CiCatalog|Learn more"
msgstr ""
msgid "CiCatalog|Mark project as a CI/CD Catalog resource"
msgstr ""
msgid "CiCatalog|Mark project as a CI/CD Catalog resource. %{linkStart}What is the CI/CD Catalog?%{linkEnd}"
msgstr ""
msgid "CiCatalog|Page %{currentPage} of %{totalPage}"
msgstr ""
@ -9598,9 +9604,24 @@ msgstr ""
msgid "CiCatalog|Repositories of pipeline components available in this namespace."
msgstr ""
msgid "CiCatalog|The project must contain a README.md file and a template.yml file. When enabled, the repository is available in the CI/CD Catalog."
msgstr ""
msgid "CiCatalog|There was a problem fetching the CI/CD Catalog setting."
msgstr ""
msgid "CiCatalog|There was a problem marking the project as a CI/CD Catalog resource."
msgstr ""
msgid "CiCatalog|There was an error fetching CI/CD Catalog resources."
msgstr ""
msgid "CiCatalog|This project is now a CI/CD Catalog resource."
msgstr ""
msgid "CiCatalog|This project will be marked as a CI/CD Catalog resource and will be visible in the CI/CD Catalog. This action is not reversible."
msgstr ""
msgid "CiCatalog|We want to help you create and manage pipeline component repositories, while also making it easier to reuse pipeline configurations. Let us know how we're doing!"
msgstr ""
@ -17400,12 +17421,18 @@ msgstr ""
msgid "Environment|Deployments"
msgstr ""
msgid "Environment|Environment health"
msgstr ""
msgid "Environment|External IP"
msgstr ""
msgid "Environment|Failed"
msgstr ""
msgid "Environment|Healthy"
msgstr ""
msgid "Environment|Jobs"
msgstr ""
@ -17439,6 +17466,9 @@ msgstr ""
msgid "Environment|Summary"
msgstr ""
msgid "Environment|Unhealthy"
msgstr ""
msgid "Epic"
msgstr ""
@ -33678,9 +33708,6 @@ msgstr ""
msgid "Pipeline|Failed"
msgstr ""
msgid "Pipeline|In progress"
msgstr ""
msgid "Pipeline|Manual"
msgstr ""
@ -48932,21 +48959,9 @@ msgstr ""
msgid "UsageQuota|User settings &gt; Usage quotas"
msgstr ""
msgid "UsageQuota|When you purchase additional storage, we automatically unlock projects that were locked if the storage limit was reached."
msgstr ""
msgid "UsageQuota|Wiki content."
msgstr ""
msgid "UsageQuota|You have consumed all of your additional storage. Purchase more to unlock projects over the limit."
msgstr ""
msgid "UsageQuota|You have reached the free storage limit on %{projectsLockedText}. To unlock them, purchase additional storage."
msgstr ""
msgid "UsageQuota|Your purchased storage is running low. To avoid locked projects, purchase more storage."
msgstr ""
msgid "UsageTrends|Could not load the issues and merge requests chart. Please refresh the page to try again."
msgstr ""

View File

@ -33,10 +33,13 @@ module Gitlab
# Waits for subscription to be synced and UI to be updated
#
# @param subscription_plan [String]
def wait_for_subscription(subscription_plan, page:)
def wait_for_subscription(subscription_plan)
::QA::Support::Waiter.wait_until(
max_duration: ::QA::Support::Helpers::Zuora::ZUORA_TIMEOUT, sleep_interval: 2, reload_page: page,
message: "Subscription plan '#{subscription_plan}' failed to appear") do
max_duration: ::QA::Support::Helpers::Zuora::ZUORA_TIMEOUT,
sleep_interval: 2,
reload_page: Chemlab.configuration.browser.session,
message: "Subscription plan '#{subscription_plan}' failed to appear"
) do
billing_plan_header.match?(/currently using the #{subscription_plan} saas plan/i)
end
end

View File

@ -82,6 +82,34 @@ module Gitlab
purchased_usage_total[/(\d+){2}.\d+/].to_f
end
# Waits for additional CI minutes to be available on the page
def wait_for_additional_ci_minutes_available
::QA::Support::Waiter.wait_until(
max_duration: ::QA::Support::Helpers::Zuora::ZUORA_TIMEOUT,
sleep_interval: 2,
reload_page: Chemlab.configuration.browser.session,
message: 'Expected additional CI minutes but they did not appear.'
) do
additional_ci_minutes_added?
end
end
# Waits for additional CI minutes amount to match the expected number of minutes
#
# @param [String] minutes
def wait_for_additional_ci_minute_limits(minutes)
wait_for_additional_ci_minutes_available
::QA::Support::Waiter.wait_until(
max_duration: ::QA::Support::Helpers::Zuora::ZUORA_TIMEOUT,
sleep_interval: 2,
reload_page: Chemlab.configuration.browser.session,
message: "Expected additional CI minutes to equal #{minutes}"
) do
additional_ci_limits == minutes
end
end
end
end
end

View File

@ -211,7 +211,7 @@ module QA
# Click by JS is needed to bypass the Moved MR actions popover
# Change back to regular click_element when moved_mr_sidebar FF is removed
# Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460
click_by_javascript(find_element(:edit_title_button))
click_by_javascript(find_element(:edit_title_button, skip_finished_loading_check: true))
end
def fast_forward_not_possible?

View File

@ -70,7 +70,6 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
context "when super sidebar is enabled" do
before do
user.update!(use_new_navigation: true)
stub_feature_flags(super_sidebar_nav: true)
end
it_behaves_like "single commit view"

View File

@ -120,6 +120,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js, feature_categ
end
before do
stub_feature_flags(review_apps_redeploy_mr_widget: false)
build.success!
deployment.update!(on_stop: manual.name)
visit project_merge_request_path(project, merge_request)
@ -142,5 +143,56 @@ RSpec.describe 'Merge request > User sees deployment widget', :js, feature_categ
end
end
end
context 'with stop action with the review_apps_redeploy_mr_widget feature flag turned on' do
let(:manual) do
create(:ci_build, :manual, pipeline: pipeline,
name: 'close_app', environment: environment.name)
end
before do
stub_feature_flags(review_apps_redeploy_mr_widget: true)
build.success!
deployment.update!(on_stop: manual.name)
visit project_merge_request_path(project, merge_request)
wait_for_requests
end
it 'displays the re-deploy button' do
accept_gl_confirm(button_text: 'Stop environment') do
find('.js-stop-env').click
end
expect(page).to have_selector('.js-redeploy-action')
end
context 'for reporter' do
let(:role) { :reporter }
it 'does not show stop button' do
expect(page).not_to have_selector('.js-stop-env')
end
end
end
context 'with redeploy action and with the review_apps_redeploy_mr_widget feature flag turned on' do
before do
stub_feature_flags(review_apps_redeploy_mr_widget: true)
build.success!
environment.update!(state: 'stopped')
visit project_merge_request_path(project, merge_request)
wait_for_requests
end
it 'begins redeploying the deployment' do
accept_gl_confirm(button_text: 'Re-deploy') do
find('.js-redeploy-action').click
end
wait_for_requests
expect(page).to have_content('Will deploy to')
end
end
end
end

View File

@ -7,75 +7,53 @@ RSpec.describe 'new navigation toggle', :js, feature_category: :navigation do
before do
user.update!(use_new_navigation: user_preference)
stub_feature_flags(super_sidebar_nav: new_nav_ff)
sign_in(user)
visit explore_projects_path
end
context 'with feature flag off' do
let(:new_nav_ff) { false }
context 'when user has new nav disabled' do
let(:user_preference) { false }
where(:user_preference) do
[true, false]
it 'allows to enable new nav', :aggregate_failures do
within '.js-nav-user-dropdown' do
find('a[data-toggle="dropdown"]').click
expect(page).to have_content('Navigation redesign')
toggle = page.find('.gl-toggle:not(.is-checked)')
toggle.click # reloads the page
end
wait_for_requests
expect(user.reload.use_new_navigation).to eq true
end
with_them do
it 'shows old topbar user dropdown with no way to toggle to new nav' do
within '.js-header-content .js-nav-user-dropdown' do
find('a[data-toggle="dropdown"]').click
expect(page).not_to have_content('Navigation redesign')
end
end
it 'shows the old navigation' do
expect(page).to have_selector('.js-navbar')
expect(page).not_to have_selector('[data-testid="super-sidebar"]')
end
end
context 'with feature flag on' do
let(:new_nav_ff) { true }
context 'when user has new nav enabled' do
let(:user_preference) { true }
context 'when user has new nav disabled' do
let(:user_preference) { false }
it 'allows to disable new nav', :aggregate_failures do
within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
click_button "#{user.name} users menu"
expect(page).to have_content('Navigation redesign')
it 'allows to enable new nav', :aggregate_failures do
within '.js-nav-user-dropdown' do
find('a[data-toggle="dropdown"]').click
expect(page).to have_content('Navigation redesign')
toggle = page.find('.gl-toggle:not(.is-checked)')
toggle.click # reloads the page
end
wait_for_requests
expect(user.reload.use_new_navigation).to eq true
toggle = page.find('.gl-toggle.is-checked')
toggle.click # reloads the page
end
it 'shows the old navigation' do
expect(page).to have_selector('.js-navbar')
expect(page).not_to have_selector('[data-testid="super-sidebar"]')
end
wait_for_requests
expect(user.reload.use_new_navigation).to eq false
end
context 'when user has new nav enabled' do
let(:user_preference) { true }
it 'allows to disable new nav', :aggregate_failures do
within '[data-testid="super-sidebar"] [data-testid="user-dropdown"]' do
click_button "#{user.name} users menu"
expect(page).to have_content('Navigation redesign')
toggle = page.find('.gl-toggle.is-checked')
toggle.click # reloads the page
end
wait_for_requests
expect(user.reload.use_new_navigation).to eq false
end
it 'shows the new navigation' do
expect(page).not_to have_selector('.js-navbar')
expect(page).to have_selector('[data-testid="super-sidebar"]')
end
it 'shows the new navigation' do
expect(page).not_to have_selector('.js-navbar')
expect(page).to have_selector('[data-testid="super-sidebar"]')
end
end
end

View File

@ -189,7 +189,6 @@ RSpec.describe "Compare", :js, feature_category: :groups_and_projects do
context "when super sidebar is enabled" do
before do
user.update!(use_new_navigation: true)
stub_feature_flags(super_sidebar_nav: true)
end
it_behaves_like "compare view of branches"

View File

@ -99,7 +99,6 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
context 'when the new navigation is enabled' do
before do
user.update!(use_new_navigation: true)
stub_feature_flags(super_sidebar_nav: true)
end
include_examples '"New project" page'

View File

@ -5,6 +5,7 @@ import KubernetesOverview from '~/environments/components/kubernetes_overview.vu
import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
import KubernetesStatusBar from '~/environments/components/kubernetes_status_bar.vue';
import { agent, kubernetesNamespace } from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
@ -32,6 +33,7 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo);
const findKubernetesPods = () => wrapper.findComponent(KubernetesPods);
const findKubernetesTabs = () => wrapper.findComponent(KubernetesTabs);
const findKubernetesStatusBar = () => wrapper.findComponent(KubernetesStatusBar);
const findAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = () => {
@ -105,6 +107,49 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
configuration,
});
});
it('renders kubernetes status bar', () => {
expect(findKubernetesStatusBar().exists()).toBe(true);
});
});
describe('Kubernetes health status', () => {
beforeEach(() => {
createWrapper();
toggleCollapse();
});
it("doesn't set `clusterHealthStatus` when pods are still loading", async () => {
findKubernetesPods().vm.$emit('loading', true);
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('');
});
it("doesn't set `clusterHealthStatus` when workload types are still loading", async () => {
findKubernetesTabs().vm.$emit('loading', true);
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('');
});
it('sets `clusterHealthStatus` as error when pods emitted a failure', async () => {
findKubernetesPods().vm.$emit('failed');
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
});
it('sets `clusterHealthStatus` as error when workload types emitted a failure', async () => {
findKubernetesTabs().vm.$emit('failed');
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
});
it('sets `clusterHealthStatus` as success when data is loaded and no failures where emitted', () => {
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('success');
});
});
describe('on cluster error', () => {

View File

@ -50,6 +50,14 @@ describe('~/environments/components/kubernetes_pods.vue', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('emits loading state', async () => {
createWrapper();
expect(wrapper.emitted('loading')[0]).toEqual([true]);
await waitForPromises();
expect(wrapper.emitted('loading')[1]).toEqual([false]);
});
it('hides the loading icon when the list of pods loaded', async () => {
createWrapper();
await waitForPromises();
@ -84,6 +92,13 @@ describe('~/environments/components/kubernetes_pods.vue', () => {
});
},
);
it('emits a failed event when there are failed pods', async () => {
createWrapper();
await waitForPromises();
expect(wrapper.emitted('failed')).toHaveLength(1);
});
});
describe('when gets an error from the cluster_client API', () => {

View File

@ -0,0 +1,42 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import KubernetesStatusBar from '~/environments/components/kubernetes_status_bar.vue';
import {
CLUSTER_STATUS_HEALTHY_TEXT,
CLUSTER_STATUS_UNHEALTHY_TEXT,
} from '~/environments/constants';
describe('~/environments/components/kubernetes_status_bar.vue', () => {
let wrapper;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findHealthBadge = () => wrapper.findComponent(GlBadge);
const createWrapper = ({ clusterHealthStatus = '' } = {}) => {
wrapper = shallowMount(KubernetesStatusBar, {
propsData: { clusterHealthStatus },
});
};
describe('health badge', () => {
it('shows loading icon when cluster health is not present', () => {
createWrapper();
expect(findLoadingIcon().exists()).toBe(true);
});
it.each([
['success', 'success', CLUSTER_STATUS_HEALTHY_TEXT],
['error', 'danger', CLUSTER_STATUS_UNHEALTHY_TEXT],
])(
'when clusterHealthStatus is %s shows health badge with variant %s and text %s',
(status, variant, text) => {
createWrapper({ clusterHealthStatus: status });
expect(findLoadingIcon().exists()).toBe(false);
expect(findHealthBadge().props('variant')).toBe(variant);
expect(findHealthBadge().text()).toBe(text);
},
);
});
});

View File

@ -59,6 +59,14 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('emits loading state', async () => {
createWrapper();
expect(wrapper.emitted('loading')[0]).toEqual([true]);
await waitForPromises();
expect(wrapper.emitted('loading')[1]).toEqual([false]);
});
describe('when workloads data is loaded', () => {
beforeEach(async () => {
await createWrapper();
@ -94,6 +102,10 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
);
});
it('emits a failed event when there are failed workload types', () => {
expect(wrapper.emitted('failed')).toHaveLength(1);
});
it('emits an error message when gets an error from the cluster_client API', async () => {
const error = new Error('Error from the cluster_client API');
const createErroredApolloProvider = () => {

View File

@ -165,4 +165,23 @@ describe('~/environments/components/kubernetes_tabs.vue', () => {
expect(wrapper.emitted('cluster-error')).toEqual([[error]]);
});
});
describe('summary tab', () => {
beforeEach(() => {
createWrapper();
});
it('emits loading event when gets it from the component', () => {
findKubernetesSummary().vm.$emit('loading', true);
expect(wrapper.emitted('loading')[0]).toEqual([true]);
findKubernetesSummary().vm.$emit('loading', false);
expect(wrapper.emitted('loading')[1]).toEqual([false]);
});
it('emits a failed event when gets it from the component', () => {
findKubernetesSummary().vm.$emit('failed');
expect(wrapper.emitted('failed')).toHaveLength(1);
});
});
});

View File

@ -40,11 +40,8 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
expect(response).to be_successful
end
# This Feature Flag is off by default
# This ensures that the correct css is generated for super sidebar
# When the feature flag is off, the general startup will capture it
it "startup_css/project-#{type}-super-sidebar.html" do
stub_feature_flags(super_sidebar_nav: true)
user.update!(use_new_navigation: true)
get :show, params: {

View File

@ -0,0 +1,147 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlBadge, GlLoadingIcon, GlModal, GlSprintf, GlToggle } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import catalogResourcesCreate from '~/pages/projects/shared/permissions/graphql/mutations/catalog_resources_create.mutation.graphql';
import getCiCatalogSettingsQuery from '~/pages/projects/shared/permissions/graphql/queries/get_ci_catalog_settings.query.graphql';
import CiCatalogSettings, {
i18n,
} from '~/pages/projects/shared/permissions/components/ci_catalog_settings.vue';
import { mockCiCatalogSettingsResponse } from './mock_data';
Vue.use(VueApollo);
jest.mock('~/alert');
describe('CiCatalogSettings', () => {
let wrapper;
let ciCatalogSettingsResponse;
let catalogResourcesCreateResponse;
const fullPath = 'gitlab-org/gitlab';
const createComponent = ({ ciCatalogSettingsHandler = ciCatalogSettingsResponse } = {}) => {
const handlers = [
[getCiCatalogSettingsQuery, ciCatalogSettingsHandler],
[catalogResourcesCreate, catalogResourcesCreateResponse],
];
const mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(CiCatalogSettings, {
propsData: {
fullPath,
},
stubs: {
GlSprintf,
},
apolloProvider: mockApollo,
});
return waitForPromises();
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBadge = () => wrapper.findComponent(GlBadge);
const findModal = () => wrapper.findComponent(GlModal);
const findToggle = () => wrapper.findComponent(GlToggle);
const findCiCatalogSettings = () => wrapper.findByTestId('ci-catalog-settings');
beforeEach(() => {
ciCatalogSettingsResponse = jest.fn().mockResolvedValue(mockCiCatalogSettingsResponse);
catalogResourcesCreateResponse = jest.fn();
});
describe('when initial queries are loading', () => {
beforeEach(() => {
createComponent();
});
it('shows a loading icon and no CI catalog settings', () => {
expect(findLoadingIcon().exists()).toBe(true);
expect(findCiCatalogSettings().exists()).toBe(false);
});
});
describe('when queries have loaded', () => {
beforeEach(async () => {
await createComponent();
});
it('does not show a loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('renders the CI Catalog settings', () => {
expect(findCiCatalogSettings().exists()).toBe(true);
});
it('renders the experiment badge', () => {
expect(findBadge().exists()).toBe(true);
});
it('renders the toggle', () => {
expect(findToggle().exists()).toBe(true);
});
it('renders the modal', () => {
expect(findModal().exists()).toBe(true);
expect(findModal().attributes('title')).toBe(i18n.modal.title);
});
describe('when queries have loaded', () => {
beforeEach(() => {
catalogResourcesCreateResponse.mockResolvedValue(mockCiCatalogSettingsResponse);
});
it('shows the modal when the toggle is clicked', async () => {
expect(findModal().props('visible')).toBe(false);
await findToggle().vm.$emit('change', true);
expect(findModal().props('visible')).toBe(true);
expect(findModal().props('actionPrimary').text).toBe(i18n.modal.actionPrimary.text);
});
it('hides the modal when cancel is clicked', () => {
findToggle().vm.$emit('change', true);
findModal().vm.$emit('canceled');
expect(findModal().props('visible')).toBe(false);
expect(catalogResourcesCreateResponse).not.toHaveBeenCalled();
});
it('calls the mutation with the correct input from the modal click', async () => {
expect(catalogResourcesCreateResponse).toHaveBeenCalledTimes(0);
findToggle().vm.$emit('change', true);
findModal().vm.$emit('primary');
await waitForPromises();
expect(catalogResourcesCreateResponse).toHaveBeenCalledTimes(1);
expect(catalogResourcesCreateResponse).toHaveBeenCalledWith({
input: {
projectPath: fullPath,
},
});
});
});
});
describe('when the query is unsuccessful', () => {
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
it('throws an error', async () => {
await createComponent({ ciCatalogSettingsHandler: failedHandler });
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: i18n.catalogResourceQueryError });
});
});
});

View File

@ -0,0 +1,7 @@
export const mockCiCatalogSettingsResponse = {
data: {
catalogResourcesCreate: {
errors: [],
},
},
};

View File

@ -1,6 +1,7 @@
import { GlSprintf, GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import ProjectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
import CiCatalogSettings from '~/pages/projects/shared/permissions/components/ci_catalog_settings.vue';
import settingsPanel from '~/pages/projects/shared/permissions/components/settings_panel.vue';
import {
featureAccessLevel,
@ -34,6 +35,7 @@ const defaultProps = {
warnAboutPotentiallyUnwantedCharacters: true,
},
isGitlabCom: true,
canAddCatalogResource: false,
canDisableEmails: true,
canChangeVisibilityLevel: true,
allowedVisibilityOptions: [0, 10, 20],
@ -118,6 +120,7 @@ describe('Settings Panel', () => {
const findPagesSettings = () => wrapper.findComponent({ ref: 'pages-settings' });
const findPagesAccessLevels = () =>
wrapper.find('[name="project[project_feature_attributes][pages_access_level]"]');
const findCiCatalogSettings = () => wrapper.findComponent(CiCatalogSettings);
const findEmailSettings = () => wrapper.findComponent({ ref: 'email-settings' });
const findShowDefaultAwardEmojis = () =>
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
@ -647,6 +650,19 @@ describe('Settings Panel', () => {
});
});
describe('CI Catalog Settings', () => {
it('should show the CI Catalog settings if user has permission', () => {
wrapper = mountComponent({ canAddCatalogResource: true });
expect(findCiCatalogSettings().exists()).toBe(true);
});
it('should not show the CI Catalog settings if user does not have permission', () => {
wrapper = mountComponent();
expect(findCiCatalogSettings().exists()).toBe(false);
});
});
describe('Email notifications', () => {
it('should show the disable email notifications input if emails an be disabled', () => {
wrapper = mountComponent({ canDisableEmails: true });

View File

@ -59,6 +59,7 @@ describe('Pipeline details header', () => {
const findTimeAgo = () => wrapper.findComponent(TimeAgo);
const findAllBadges = () => wrapper.findAllComponents(GlBadge);
const findPipelineName = () => wrapper.findByTestId('pipeline-name');
const findCommitTitle = () => wrapper.findByTestId('pipeline-commit-title');
const findTotalJobs = () => wrapper.findByTestId('total-jobs');
const findComputeCredits = () => wrapper.findByTestId('compute-credits');
const findCommitLink = () => wrapper.findByTestId('commit-link');
@ -178,6 +179,19 @@ describe('Pipeline details header', () => {
});
});
describe('without pipeline name', () => {
it('displays commit title', async () => {
createComponent(defaultHandlers, { ...defaultProps, name: '' });
await waitForPromises();
const expectedTitle = pipelineHeaderSuccess.data.project.pipeline.commit.title;
expect(findPipelineName().exists()).toBe(false);
expect(findCommitTitle().text()).toBe(expectedTitle);
});
});
describe('finished pipeline', () => {
beforeEach(async () => {
createComponent();

View File

@ -8,7 +8,7 @@ describe('Timeago component', () => {
const defaultProps = { duration: 0, finished_at: '' };
const createComponent = (props = defaultProps, stuck = false, extraProps) => {
const createComponent = (props = defaultProps, extraProps) => {
wrapper = extendedWrapper(
shallowMount(TimeAgo, {
propsData: {
@ -16,9 +16,6 @@ describe('Timeago component', () => {
details: {
...props,
},
flags: {
stuck,
},
},
...extraProps,
},
@ -33,10 +30,6 @@ describe('Timeago component', () => {
const duration = () => wrapper.find('.duration');
const finishedAt = () => wrapper.find('.finished-at');
const findInProgress = () => wrapper.findByTestId('pipeline-in-progress');
const findSkipped = () => wrapper.findByTestId('pipeline-skipped');
const findHourGlassIcon = () => wrapper.findByTestId('hourglass-icon');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findCalendarIcon = () => wrapper.findByTestId('calendar-icon');
describe('with duration', () => {
@ -79,9 +72,12 @@ describe('Timeago component', () => {
});
it('should hide calendar icon if correct prop is passed', () => {
createComponent({ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' }, false, {
displayCalendarIcon: false,
});
createComponent(
{ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' },
{
displayCalendarIcon: false,
},
);
expect(findCalendarIcon().exists()).toBe(false);
});
@ -97,45 +93,4 @@ describe('Timeago component', () => {
expect(findCalendarIcon().exists()).toBe(false);
});
});
describe('in progress', () => {
it.each`
durationTime | finishedAtTime | shouldShow
${10} | ${'2017-04-26T12:40:23.277Z'} | ${false}
${10} | ${''} | ${false}
${0} | ${'2017-04-26T12:40:23.277Z'} | ${false}
${0} | ${''} | ${true}
`(
'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime',
({ durationTime, finishedAtTime, shouldShow }) => {
createComponent({
duration: durationTime,
finished_at: finishedAtTime,
});
expect(findInProgress().exists()).toBe(shouldShow);
expect(findSkipped().exists()).toBe(false);
},
);
it('should show warning icon beside in progress if pipeline is stuck', () => {
const stuck = true;
createComponent(defaultProps, stuck);
expect(findWarningIcon().exists()).toBe(true);
expect(findHourGlassIcon().exists()).toBe(false);
});
});
describe('skipped', () => {
it('should show skipped if pipeline was skipped', () => {
createComponent({
status: { label: 'skipped' },
});
expect(findSkipped().exists()).toBe(true);
expect(findInProgress().exists()).toBe(false);
});
});
});

View File

@ -5,6 +5,7 @@ import {
RUNNING,
DEPLOYING,
REDEPLOYING,
WILL_DEPLOY,
} from '~/vue_merge_request_widget/components/deployment/constants';
import DeploymentActionButton from '~/vue_merge_request_widget/components/deployment/deployment_action_button.vue';
import { actionButtonMocks } from './deployment_mock_data';
@ -118,4 +119,20 @@ describe('Deployment action button', () => {
expect(wrapper.findComponent(GlButton).props('disabled')).toBe(false);
});
});
describe('when the deployment status is will_deploy', () => {
beforeEach(() => {
factory({
propsData: {
...baseProps,
actionInProgress: actionButtonMocks[REDEPLOYING].actionName,
computedDeploymentStatus: WILL_DEPLOY,
},
});
});
it('is disabled and shows the loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true);
});
});
});

View File

@ -9,6 +9,7 @@ import {
FAILED,
DEPLOYING,
REDEPLOYING,
SUCCESS,
STOPPING,
} from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
@ -35,7 +36,8 @@ describe('DeploymentAction component', () => {
const findStopButton = () => wrapper.find('.js-stop-env');
const findDeployButton = () => wrapper.find('.js-manual-deploy-action');
const findRedeployButton = () => wrapper.find('.js-manual-redeploy-action');
const findManualRedeployButton = () => wrapper.find('.js-manual-redeploy-action');
const findRedeployButton = () => wrapper.find('.js-redeploy-action');
beforeEach(() => {
executeActionSpy = jest.spyOn(MRWidgetService, 'executeInlineAction');
@ -79,17 +81,17 @@ describe('DeploymentAction component', () => {
describe('when there is no retry_path in details', () => {
it('the manual redeploy button does not appear', () => {
expect(findRedeployButton().exists()).toBe(false);
expect(findManualRedeployButton().exists()).toBe(false);
});
});
});
describe('when conditions are met', () => {
describe.each`
configConst | computedDeploymentStatus | displayConditionChanges | finderFn | endpoint
${STOPPING} | ${CREATED} | ${{}} | ${findStopButton} | ${deploymentMockData.stop_url}
${DEPLOYING} | ${MANUAL_DEPLOY} | ${playDetails} | ${findDeployButton} | ${playDetails.playable_build.play_path}
${REDEPLOYING} | ${FAILED} | ${retryDetails} | ${findRedeployButton} | ${retryDetails.playable_build.retry_path}
configConst | computedDeploymentStatus | displayConditionChanges | finderFn | endpoint
${STOPPING} | ${CREATED} | ${{}} | ${findStopButton} | ${deploymentMockData.stop_url}
${DEPLOYING} | ${MANUAL_DEPLOY} | ${playDetails} | ${findDeployButton} | ${playDetails.playable_build.play_path}
${REDEPLOYING} | ${FAILED} | ${retryDetails} | ${findManualRedeployButton} | ${retryDetails.playable_build.retry_path}
`(
'$configConst action',
({ configConst, computedDeploymentStatus, displayConditionChanges, finderFn, endpoint }) => {
@ -231,4 +233,141 @@ describe('DeploymentAction component', () => {
},
);
});
describe('with the reviewAppsRedeployMrWidget feature flag turned on', () => {
beforeEach(() => {
factory({
propsData: {
computedDeploymentStatus: SUCCESS,
deployment: {
...deploymentMockData,
details: undefined,
retry_url: retryDetails.playable_build.retry_path,
environment_available: false,
},
},
provide: {
glFeatures: {
reviewAppsRedeployMrWidget: true,
},
},
});
});
it('should display the redeploy button', () => {
expect(findRedeployButton().exists()).toBe(true);
});
describe('when the redeploy button is clicked', () => {
describe('should show a confirm dialog but not call executeInlineAction when declined', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
confirmAction.mockResolvedValueOnce(false);
findRedeployButton().trigger('click');
});
it('should show the confirm dialog', () => {
expect(confirmAction).toHaveBeenCalled();
expect(confirmAction).toHaveBeenCalledWith(
actionButtonMocks[REDEPLOYING].confirmMessage,
{
primaryBtnVariant: actionButtonMocks[REDEPLOYING].buttonVariant,
primaryBtnText: actionButtonMocks[REDEPLOYING].buttonText,
},
);
});
it('should not execute the action', () => {
expect(MRWidgetService.executeInlineAction).not.toHaveBeenCalled();
});
});
describe('should show a confirm dialog and call executeInlineAction when accepted', () => {
beforeEach(() => {
executeActionSpy.mockResolvedValueOnce();
confirmAction.mockResolvedValueOnce(true);
findRedeployButton().trigger('click');
});
it('should show the confirm dialog', () => {
expect(confirmAction).toHaveBeenCalled();
expect(confirmAction).toHaveBeenCalledWith(
actionButtonMocks[REDEPLOYING].confirmMessage,
{
primaryBtnVariant: actionButtonMocks[REDEPLOYING].buttonVariant,
primaryBtnText: actionButtonMocks[REDEPLOYING].buttonText,
},
);
});
it('should not throw an error', () => {
expect(createAlert).not.toHaveBeenCalled();
});
describe('response includes redirect_url', () => {
const url = '/root/example';
beforeEach(async () => {
executeActionSpy.mockResolvedValueOnce({
data: { redirect_url: url },
});
await waitForPromises();
confirmAction.mockResolvedValueOnce(true);
findRedeployButton().trigger('click');
});
it('does not call visit url', () => {
expect(visitUrl).not.toHaveBeenCalled();
});
});
describe('it should call the executeAction method', () => {
beforeEach(async () => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
jest.spyOn(eventHub, '$emit');
await waitForPromises();
confirmAction.mockResolvedValueOnce(true);
findRedeployButton().trigger('click');
});
it('calls with the expected arguments', () => {
expect(wrapper.vm.executeAction).toHaveBeenCalled();
expect(wrapper.vm.executeAction).toHaveBeenCalledWith(
retryDetails.playable_build.retry_path,
actionButtonMocks[REDEPLOYING],
);
});
it('emits the FetchDeployments event', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('FetchDeployments');
});
});
describe('when executeInlineAction errors', () => {
beforeEach(async () => {
executeActionSpy.mockRejectedValueOnce();
jest.spyOn(eventHub, '$emit');
await waitForPromises();
confirmAction.mockResolvedValueOnce(true);
findRedeployButton().trigger('click');
});
it('should call createAlert with error message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: actionButtonMocks[REDEPLOYING].errorMessage,
});
});
it('emits the FetchDeployments event', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('FetchDeployments');
});
});
});
});
});
});

View File

@ -43,6 +43,7 @@ const deploymentMockData = {
external_url_formatted: 'gitlab',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
environment_available: true,
details: {},
status: SUCCESS,
changes: [

View File

@ -427,6 +427,7 @@ export const mockStore = {
external_url: 'https://fake.com',
external_url_formatted: 'https://fake.com',
status: SUCCESS,
environment_available: true,
},
{
id: 1,
@ -434,6 +435,7 @@ export const mockStore = {
external_url: 'https://fake.com',
external_url_formatted: 'https://fake.com',
status: SUCCESS,
environment_available: true,
},
],
postMergeDeployments: [

View File

@ -626,6 +626,7 @@ describe('MrWidgetOptions', () => {
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes,
status: SUCCESS,
environment_available: true,
};
beforeEach(() => {

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::AuditEvents::AuditEventDefinitionsResolver, feature_category: :audit_events do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
describe '#resolve' do
let(:args) { {} }
subject(:audit_event_definitions) { resolve(described_class, args: args, ctx: { current_user: current_user }) }
it 'returns an array of audit event definitions' do
expect(audit_event_definitions).to be_an(Array)
expect(audit_event_definitions).to match_array(Gitlab::Audit::Type::Definition.definitions.values)
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['AuditEventDefinition'], feature_category: :audit_events do
let(:fields) do
%i[
name description introduced_by_issue introduced_by_mr
feature_category milestone saved_to_database streamed
]
end
specify { expect(described_class.graphql_name).to eq('AuditEventDefinition') }
specify { expect(described_class).to have_graphql_fields(fields) }
end

View File

@ -7,6 +7,18 @@ RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composi
let_it_be(:project) { build(:project) }
describe '#can_add_catalog_resource?' do
subject { helper.can_add_catalog_resource?(project) }
before do
stub_licensed_features(ci_namespace_catalog: false)
end
it 'user cannot add a catalog resource in CE regardless of permissions' do
expect(subject).to be false
end
end
describe '#can_view_namespace_catalog?' do
subject { helper.can_view_namespace_catalog?(project) }

View File

@ -138,56 +138,35 @@ RSpec.describe NavHelper, feature_category: :navigation do
describe '#show_super_sidebar?' do
shared_examples 'show_super_sidebar is supposed to' do
before do
stub_feature_flags(super_sidebar_nav: new_nav_ff)
user.update!(use_new_navigation: user_preference)
end
context 'with feature flag off' do
let(:new_nav_ff) { false }
context 'when user has not interacted with the new nav toggle yet' do
let(:user_preference) { nil }
context 'when user has new nav disabled' do
let(:user_preference) { false }
specify { expect(subject).to eq false }
specify { expect(subject).to eq false }
end
context 'when user has new nav enabled' do
let(:user_preference) { true }
specify { expect(subject).to eq false }
end
end
context 'with feature flag on' do
let(:new_nav_ff) { true }
context 'when user has not interacted with the new nav toggle yet' do
let(:user_preference) { nil }
specify { expect(subject).to eq false }
context 'when the user was enrolled into the new nav via a special feature flag' do
before do
# this ff is disabled in globally to keep tests of the old nav working
stub_feature_flags(super_sidebar_nav_enrolled: true)
end
specify { expect(subject).to eq true }
context 'when the user was enrolled into the new nav via a special feature flag' do
before do
# this ff is disabled in globally to keep tests of the old nav working
stub_feature_flags(super_sidebar_nav_enrolled: true)
end
end
context 'when user has new nav disabled' do
let(:user_preference) { false }
specify { expect(subject).to eq false }
end
context 'when user has new nav enabled' do
let(:user_preference) { true }
specify { expect(subject).to eq true }
end
end
context 'when user has new nav disabled' do
let(:user_preference) { false }
specify { expect(subject).to eq false }
end
context 'when user has new nav enabled' do
let(:user_preference) { true }
specify { expect(subject).to eq true }
end
end
context 'when nil is provided' do

View File

@ -1052,6 +1052,12 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
it 'includes membersPagePath' do
expect(subject).to include(membersPagePath: project_project_members_path(project))
end
it 'includes canAddCatalogResource' do
allow(helper).to receive(:can?) { false }
expect(subject).to include(canAddCatalogResource: false)
end
end
describe '#project_classes' do

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting a list of audit event definitions', feature_category: :audit_events do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let(:path) { %i[audit_event_definitions nodes] }
let(:audit_event_definition_keys) do
Gitlab::Audit::Type::Definition.definitions.keys
end
let(:query) { graphql_query_for(:audit_event_definitions, {}, 'nodes { name }') }
it 'returns the audit event definitions' do
post_graphql(query, current_user: current_user)
returned_names = graphql_data_at(*path).map { |v| v['name'].to_sym }
expect(returned_names).to all be_in(audit_event_definition_keys)
end
end

View File

@ -53,7 +53,11 @@ Capybara.register_server :puma_via_workhorse do |app, port, host, **options|
# In cases of multiple installations of chromedriver, prioritize the version installed by SeleniumManager
# selenium-manager doesn't work with Linux arm64 yet:
# https://github.com/SeleniumHQ/selenium/issues/11357
if RUBY_PLATFORM =~ /x86_64-linux|darwin/
if RUBY_PLATFORM.include?('x86_64-linux') ||
# Rosetta is required on macOS because the selenium-manager
# binaries (https://github.com/SeleniumHQ/selenium/tree/trunk/common/manager/macos)
# are only compiled for macOS x86.
(RUBY_PLATFORM.include?('darwin') && system('/usr/bin/pgrep -q oahd'))
chrome_options = Selenium::WebDriver::Chrome::Options.chrome
chromedriver_path = File.dirname(Selenium::WebDriver::SeleniumManager.driver_path(chrome_options))
ENV['PATH'] = "#{chromedriver_path}:#{ENV['PATH']}" # rubocop:disable RSpec/EnvAssignment

View File

@ -149,7 +149,7 @@ module LoginHelpers
mock_auth_hash(provider, uid, email, response_object: response_object)
end
def configure_mock_auth(provider, uid, email, response_object: nil, additional_info: {})
def configure_mock_auth(provider, uid, email, response_object: nil, additional_info: {}, name: 'mockuser')
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
@ -157,7 +157,7 @@ module LoginHelpers
provider: provider,
uid: uid,
info: {
name: 'mockuser',
name: name,
email: email,
image: 'mock_user_thumbnail_url'
},
@ -180,8 +180,10 @@ module LoginHelpers
}).merge(additional_info) { |_, old_hash, new_hash| old_hash.merge(new_hash) }
end
def mock_auth_hash(provider, uid, email, additional_info: {}, response_object: nil)
configure_mock_auth(provider, uid, email, additional_info: additional_info, response_object: response_object)
def mock_auth_hash(provider, uid, email, additional_info: {}, response_object: nil, name: 'mockuser')
configure_mock_auth(
provider, uid, email, additional_info: additional_info, response_object: response_object, name: name
)
original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth']
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]

View File

@ -40,7 +40,8 @@ RSpec.shared_context 'with FOSS query type fields' do
:usage_trends_measurements,
:user,
:users,
:work_item
:work_item,
:audit_event_definitions
]
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.configure do |config|
%i[saas saas_registration saas_sso_registration saas_subscription_registration].each do |metadata|
%i[saas saas_registration].each do |metadata|
config.before(:context, metadata) do
# Ensure Gitlab.com? returns true during context.
# This is needed for let_it_be which is shared across examples,