Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-04-04 21:13:26 +00:00
parent eacc15ffb1
commit a3b88e15d8
57 changed files with 920 additions and 479 deletions

View File

@ -160,10 +160,21 @@ detect-tests:
filter_rspec_matched_foss_tests ${RSPEC_MATCHING_TESTS_PATH} ${RSPEC_MATCHING_TESTS_FOSS_PATH};
filter_rspec_matched_ee_tests ${RSPEC_MATCHING_TESTS_PATH} ${RSPEC_MATCHING_TESTS_EE_PATH};
echoinfo "Changed files: $(cat $RSPEC_CHANGED_FILES_PATH)";
echoinfo "Related FOSS RSpec tests: $(cat $RSPEC_MATCHING_TESTS_FOSS_PATH)";
echoinfo "Related EE RSpec tests: $(cat $RSPEC_MATCHING_TESTS_EE_PATH)";
echoinfo "Related JS files: $(cat $RSPEC_MATCHING_JS_FILES_PATH)";
echoinfo 'Changed files:'
echoinfo "$(tr ' ' '\n' < $RSPEC_CHANGED_FILES_PATH)"
echo ""
echoinfo 'Related FOSS RSpec tests:'
echoinfo "$(tr ' ' '\n' < $RSPEC_MATCHING_TESTS_FOSS_PATH)"
echo ""
echoinfo 'Related EE RSpec tests:'
echoinfo "$(tr ' ' '\n' < $RSPEC_MATCHING_TESTS_EE_PATH)"
echo ""
echoinfo 'Related JS files:'
echoinfo "$(tr ' ' '\n' < $RSPEC_MATCHING_JS_FILES_PATH)"
echo ""
fi
artifacts:
expire_in: 7d

View File

@ -10,8 +10,8 @@ import {
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import CiVerificationBadge from '../shared/ci_verification_badge.vue';
import CiResourceAbout from './ci_resource_about.vue';
import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
@ -22,9 +22,9 @@ export default {
},
components: {
AbuseCategorySelector,
CiIcon,
CiResourceAbout,
CiResourceHeaderSkeletonLoader,
CiVerificationBadge,
GlAvatar,
GlAvatarLink,
GlDisclosureDropdown,
@ -54,11 +54,6 @@ export default {
required: false,
default: 0,
},
pipelineStatus: {
type: Object,
required: false,
default: () => ({}),
},
resource: {
type: Object,
required: true,
@ -81,8 +76,8 @@ export default {
hasLatestVersion() {
return this.latestVersion?.name;
},
hasPipelineStatus() {
return this.pipelineStatus?.text;
isVerified() {
return this.resource?.verificationLevel !== 'UNVERIFIED';
},
latestVersion() {
return this.resource?.versions?.nodes[0] || {};
@ -139,11 +134,11 @@ export default {
{{ versionBadgeText }}
</gl-badge>
</span>
<ci-icon
v-if="hasPipelineStatus"
:status="pipelineStatus"
show-status-text
class="gl-mt-2"
<ci-verification-badge
v-if="isVerified"
:verification-level="resource.verificationLevel"
:resource-id="resource.id"
show-text
/>
</div>
</div>

View File

@ -14,6 +14,7 @@ import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { toNounSeriesText } from '~/lib/utils/grammar';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
import CiVerificationBadge from '../shared/ci_verification_badge.vue';
export default {
i18n: {
@ -22,6 +23,7 @@ export default {
releasedMessage: s__('CiCatalog|Released %{timeAgo} by %{author}'),
},
components: {
CiVerificationBadge,
GlAvatar,
GlBadge,
GlButton,
@ -80,6 +82,9 @@ export default {
hasReleasedVersion() {
return Boolean(this.latestVersion?.createdAt);
},
isVerified() {
return this.resource?.verificationLevel !== 'UNVERIFIED';
},
latestVersion() {
return this.resource?.versions?.nodes[0] || [];
},
@ -143,7 +148,14 @@ export default {
@click="navigateToDetailsPage"
/>
<div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
<span class="gl-font-sm gl-mb-1">{{ webPath }}</span>
<div>
<span class="gl-font-sm gl-mb-1">{{ webPath }}</span>
<ci-verification-badge
v-if="isVerified"
:resource-id="resource.id"
:verification-level="resource.verificationLevel"
/>
</div>
<div class="gl-display-flex gl-flex-wrap gl-gap-2 gl-mb-1">
<gl-link
class="gl-text-gray-900! gl-mr-1"

View File

@ -64,17 +64,8 @@ export default {
isLoadingSharedData() {
return this.$apollo.queries.resourceSharedData.loading;
},
versions() {
return this.resourceAdditionalDetails?.versions?.nodes || [];
},
version() {
return this.resourceAdditionalDetails?.versions?.nodes[0]?.name || '';
},
pipelineStatus() {
return (
this.resourceAdditionalDetails?.versions?.nodes[0]?.commit?.pipelines?.nodes[0]
?.detailedStatus || null
);
return this.resourceSharedData?.versions?.nodes[0]?.name || '';
},
},
i18n: {
@ -102,7 +93,6 @@ export default {
:open-merge-requests-count="resourceAdditionalDetails.openMergeRequestsCount"
:is-loading-details="isLoadingDetails"
:is-loading-shared-data="isLoadingSharedData"
:pipeline-status="pipelineStatus"
:resource="resourceSharedData"
/>
<ci-resource-details :resource-path="cleanFullPath" :version="version" />

View File

@ -0,0 +1,74 @@
<script>
import { GlIcon, GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { VerificationLevel } from '../../constants';
export default {
i18n: {
verificationLevelPopoverLink: s__('CiCatalog|Learn more about designated creators'),
},
VerificationLevel,
verificationHelpPagePath: helpPagePath('ci/components/index', { anchor: 'verified-components' }),
components: {
GlIcon,
GlLink,
GlPopover,
GlSprintf,
},
props: {
resourceId: {
type: String,
required: true,
},
showText: {
type: Boolean,
default: false,
required: false,
},
verificationLevel: {
type: String,
required: true,
},
},
computed: {
popoverTarget() {
return `${this.resourceId}-verification-icon`;
},
},
};
</script>
<template>
<span>
<span :id="popoverTarget">
<gl-icon
class="gl-text-blue-500 gl-ml-1"
:name="$options.VerificationLevel[verificationLevel].icon"
/>
<span
v-if="showText"
data-testid="verification-badge-text"
class="gl-text-blue-500 gl-font-weight-bold gl-cursor-default"
>
{{ $options.VerificationLevel[verificationLevel].badgeText }}
</span>
</span>
<gl-popover :target="popoverTarget" triggers="hover focus">
<div class="gl-display-flex gl-flex-direction-column gl-gap-4">
<span>
<gl-sprintf :message="$options.VerificationLevel[verificationLevel].popoverText">
<template #bold="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</span>
<gl-link :href="$options.verificationHelpPagePath" target="_blank">
{{ $options.i18n.verificationLevelPopoverLink }}
</gl-link>
</div>
</gl-popover>
</span>
</template>

View File

@ -1,4 +1,5 @@
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export const CATALOG_FEEDBACK_DISMISSED_KEY = 'catalog_feedback_dismissed';
@ -7,6 +8,19 @@ export const SCOPE = {
namespaces: 'NAMESPACES',
};
export const VerificationLevel = {
GITLAB: {
badgeText: s__('CiCatalog|GitLab-maintained'),
icon: 'tanuki-verified',
popoverText: s__('CiCatalog|Created and maintained by %{boldStart}GitLab%{boldEnd}'),
},
PARTNER: {
badgeText: s__('CiCatalog|Partner'),
icon: 'partner-verified',
popoverText: s__('CiCatalog|Created and maintained by a %{boldStart}GitLab Partner%{boldEnd}'),
},
};
export const SORT_OPTION_CREATED = 'CREATED';
export const SORT_OPTION_RELEASED = 'LATEST_RELEASED_AT';
export const SORT_ASC = 'ASC';

View File

@ -1,11 +1,11 @@
fragment CatalogResourceFields on CiCatalogResource {
id
webPath
description
icon
name
description
starCount
starrersPath
verificationLevel
versions(first: 1) {
nodes {
id
@ -26,4 +26,5 @@ fragment CatalogResourceFields on CiCatalogResource {
}
}
}
webPath
}

View File

@ -4,27 +4,5 @@ query getCiCatalogResourceDetails($fullPath: ID!) {
webPath
openIssuesCount
openMergeRequestsCount
versions(first: 1) {
nodes {
id
commit {
id
pipelines(first: 1) {
nodes {
id
detailedStatus {
id
detailsPath
icon
text
group
}
}
}
}
name
createdAt
}
}
}
}

View File

@ -0,0 +1,35 @@
<script>
import { GlAlert } from '@gitlab/ui';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { s__ } from '~/locale';
export default {
components: {
GlAlert,
UserCalloutDismisser,
},
i18n: {
header: s__('Deployment|What would you like to see here?'),
body: s__(
"Deployment|We know this page is a bit empty at the moment, but we're working hard to bring you a great experience! In the meantime, we'd love to hear your feedback.",
),
action: s__('Deployment|Leave feedback'),
},
FEEDBACK_ISSUE_LINK: 'https://gitlab.com/gitlab-org/gitlab/-/issues/450700',
};
</script>
<template>
<user-callout-dismisser feature-name="deployment_details_feedback">
<template #default="{ dismiss, shouldShowCallout }">
<gl-alert
v-if="shouldShowCallout"
:title="$options.i18n.header"
:primary-button-text="$options.i18n.action"
:primary-button-link="$options.FEEDBACK_ISSUE_LINK"
@dismiss="dismiss"
>
{{ $options.i18n.body }}
</gl-alert>
</template>
</user-callout-dismisser>
</template>

View File

@ -8,6 +8,7 @@ import environmentQuery from '../graphql/queries/environment.query.graphql';
import DeploymentHeader from './deployment_header.vue';
import DeploymentAside from './deployment_aside.vue';
import DeploymentDeployBlock from './deployment_deploy_block.vue';
import DetailsFeedback from './details_feedback.vue';
const DEPLOYMENT_QUERY_POLLING_INTERVAL = 3000;
@ -18,6 +19,7 @@ export default {
DeploymentHeader,
DeploymentAside,
DeploymentDeployBlock,
DetailsFeedback,
DeploymentApprovals: () =>
import('ee_component/deployments/components/deployment_approvals.vue'),
DeploymentTimeline: () => import('ee_component/deployments/components/deployment_timeline.vue'),
@ -99,6 +101,7 @@ export default {
:environment="environment"
:loading="$apollo.queries.deployment.loading"
/>
<details-feedback class="gl-mt-6 gl-w-90p" />
<deployment-approvals
v-if="hasApprovalSummary"
:approval-summary="deployment.approvalSummary"

View File

@ -1,84 +1,13 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { isLoggedIn } from '~/lib/utils/common_utils';
import ForksButton from '~/forks/components/forks_button.vue';
import MoreActionsDropdown from '~/groups_projects/components/more_actions_dropdown.vue';
import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
import StarCount from '~/stars/components/star_count.vue';
import HomePanelActions from './home_panel_actions.vue';
export default {
components: {
ForksButton,
GlButton,
MoreActionsDropdown,
NotificationsDropdown,
StarCount,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
adminPath: {
default: '',
},
canReadProject: {
default: false,
},
isProjectEmpty: {
default: false,
},
projectId: {
default: '',
},
},
data() {
return {
isLoggedIn: isLoggedIn(),
};
},
computed: {
canForkProject() {
return !this.isProjectEmpty && isLoggedIn() && this.canReadProject;
},
copyProjectId() {
return sprintf(s__('ProjectPage|Project ID: %{id}'), { id: this.projectId });
},
},
i18n: {
adminButtonTooltip: __('View project in admin area'),
HomePanelActions,
},
};
</script>
<template>
<div
class="gl-align-items-center gl-display-flex gl-flex-wrap gl-gap-3 gl-justify-content-md-end project-repo-buttons"
>
<gl-button
v-if="adminPath"
v-gl-tooltip
:aria-label="$options.i18n.adminButtonTooltip"
:href="adminPath"
:title="$options.i18n.adminButtonTooltip"
data-testid="admin-button"
icon="admin"
/>
<template v-if="isLoggedIn && canReadProject">
<notifications-dropdown />
</template>
<star-count />
<forks-button v-if="canForkProject" />
<template v-if="canReadProject">
<span class="gl-sr-only" itemprop="identifier" data-testid="project-id-content">
{{ copyProjectId }}
</span>
</template>
<more-actions-dropdown />
</div>
<home-panel-actions />
</template>

View File

@ -0,0 +1,84 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { isLoggedIn } from '~/lib/utils/common_utils';
import ForksButton from '~/forks/components/forks_button.vue';
import MoreActionsDropdown from '~/groups_projects/components/more_actions_dropdown.vue';
import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
import StarCount from '~/stars/components/star_count.vue';
export default {
components: {
ForksButton,
GlButton,
MoreActionsDropdown,
NotificationsDropdown,
StarCount,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
adminPath: {
default: '',
},
canReadProject: {
default: false,
},
isProjectEmpty: {
default: false,
},
projectId: {
default: '',
},
},
data() {
return {
isLoggedIn: isLoggedIn(),
};
},
computed: {
canForkProject() {
return !this.isProjectEmpty && isLoggedIn() && this.canReadProject;
},
copyProjectId() {
return sprintf(s__('ProjectPage|Project ID: %{id}'), { id: this.projectId });
},
},
i18n: {
adminButtonTooltip: __('View project in admin area'),
},
};
</script>
<template>
<div
class="gl-align-items-center gl-display-flex gl-flex-wrap gl-gap-3 gl-justify-content-md-end project-repo-buttons"
>
<gl-button
v-if="adminPath"
v-gl-tooltip
:aria-label="$options.i18n.adminButtonTooltip"
:href="adminPath"
:title="$options.i18n.adminButtonTooltip"
data-testid="admin-button"
icon="admin"
/>
<template v-if="isLoggedIn && canReadProject">
<notifications-dropdown />
</template>
<star-count />
<forks-button v-if="canForkProject" />
<template v-if="canReadProject">
<span class="gl-sr-only" itemprop="identifier" data-testid="project-id-content">
{{ copyProjectId }}
</span>
</template>
<more-actions-dropdown />
</div>
</template>

View File

@ -830,13 +830,6 @@ class Namespace < ApplicationRecord
Rails.cache.delete_multi(keys)
end
def write_projects_repository_config
all_projects.find_each do |project|
project.set_full_path
project.track_project_repository
end
end
def enforce_minimum_path_length?
path_changed? && !project_namespace?
end

View File

@ -2283,16 +2283,6 @@ class Project < ApplicationRecord
ensure_pages_metadatum.update!(onboarding_complete: true)
end
def set_full_path(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
# the import rake task.
repository.raw_repository.set_full_path(full_path: gl_full_path)
rescue Gitlab::Git::Repository::NoRepository => e
Gitlab::AppLogger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil
end
def after_import
repository.expire_content_cache
repository.remove_prohibited_branches
@ -2315,7 +2305,6 @@ class Project < ApplicationRecord
after_create_default_branch
join_pool_repository
refresh_markdown_cache!
set_full_path
end
def update_project_counter_caches

View File

@ -85,7 +85,8 @@ module Users
joining_a_project_alert: 83, # EE-only
transition_to_jihu_callout: 84,
summarize_code_changes: 85, # EE-only
duo_pro_trial_alert: 86 # EE-only
duo_pro_trial_alert: 86, # EE-only
deployment_details_feedback: 87
}
validates :feature_name,

View File

@ -97,7 +97,6 @@ module Projects
def update_repository_configuration
project.reload_repository!
project.set_full_path
project.track_project_repository
end

View File

@ -112,11 +112,6 @@ module Projects
if @project.import?
Gitlab::Tracking.event(self.class.name, 'import_project', user: current_user)
else
# Skip writing the config for project imports/forks because it
# will always fail since the Git directory doesn't exist until
# a background job creates it (see Project#add_import_job).
@project.set_full_path
end
unless @project.gitlab_project_import?

View File

@ -133,7 +133,7 @@ module Projects
project.old_path_with_namespace = @old_path
update_repository_configuration(@new_path)
update_repository_configuration
remove_issue_contacts
@ -196,8 +196,7 @@ module Projects
project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group?
end
def update_repository_configuration(full_path)
project.set_full_path(gl_full_path: full_path)
def update_repository_configuration
project.track_project_repository
end
@ -233,7 +232,7 @@ module Projects
def rollback_side_effects
project.reset
update_namespace_and_visibility(@old_namespace)
update_repository_configuration(@old_path)
update_repository_configuration
end
def execute_system_hooks

View File

@ -4,6 +4,6 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95689
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371669
milestone: '15.6'
type: development
group: group::incubation
group: group::mlops
default_enabled: true
log_state_changes: true

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
master_pipeline_status.check!

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require_relative '../../tooling/danger/master_pipeline_status'
module Danger
class MasterPipelineStatus < ::Danger::Plugin
include Tooling::Danger::MasterPipelineStatus
end
end

View File

@ -95,8 +95,8 @@ If a replica cannot start or rejoin the cluster, or when it lags behind and cann
sudo gitlab-ctl patroni reinitialize-replica --member gitlab-database-2.example.com
```
This can be run on any Patroni node, but be aware that `sudo gitlab-ctl patroni
reinitialize-replica` without `--member` restarts the server it is run on.
This can be run on any Patroni node, but be aware that `sudo gitlab-ctl patroni reinitialize-replica`
without `--member` restarts the server it is run on.
You should run it locally on the broken server to reduce the risk of
unintended data loss.
1. Monitor the logs:

View File

@ -33342,6 +33342,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumci_deprecation_warning_for_types_keyword"></a>`CI_DEPRECATION_WARNING_FOR_TYPES_KEYWORD` | Callout feature name for ci_deprecation_warning_for_types_keyword. |
| <a id="usercalloutfeaturenameenumcloud_licensing_subscription_activation_banner"></a>`CLOUD_LICENSING_SUBSCRIPTION_ACTIVATION_BANNER` | Callout feature name for cloud_licensing_subscription_activation_banner. |
| <a id="usercalloutfeaturenameenumcluster_security_warning"></a>`CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. |
| <a id="usercalloutfeaturenameenumdeployment_details_feedback"></a>`DEPLOYMENT_DETAILS_FEEDBACK` | Callout feature name for deployment_details_feedback. |
| <a id="usercalloutfeaturenameenumduo_chat_callout"></a>`DUO_CHAT_CALLOUT` | Callout feature name for duo_chat_callout. |
| <a id="usercalloutfeaturenameenumduo_pro_trial_alert"></a>`DUO_PRO_TRIAL_ALERT` | Callout feature name for duo_pro_trial_alert. |
| <a id="usercalloutfeaturenameenumfeature_flags_new_version"></a>`FEATURE_FLAGS_NEW_VERSION` | Callout feature name for feature_flags_new_version. |

View File

@ -2,6 +2,7 @@
stage: Create
group: Code Review
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
description: "Documentation for the REST API for merge request approvals in GitLab."
---
# Merge request approvals API

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Documentation for the REST API for Git tags in GitLab."
---
# Tags API

View File

@ -292,6 +292,17 @@ in the catalog. The project and its repository still exist, but are not visible
To publish the component project in the catalog again, you need to [publish a new release](#publish-a-new-release).
### Verified component creators
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/433443) in GitLab 16.11
Some CI/CD components are badged with an icon to show that the component was created
and is maintained by users verified by GitLab:
- GitLab-maintained (**{tanuki-verified}**): Components that are created and maintained by GitLab.
- GitLab Partner (**{partner-verified}**): Components that are created and maintained by
a GitLab-verified partner.
## Best practices
This section describes some best practices for creating high quality component projects.

View File

@ -15,9 +15,9 @@ When the state of a feature flag changes, the developer who made the change
## When to document features behind a feature flag
Every feature introduced to the codebase, even if it's behind a disabled feature flag,
Every feature introduced to the codebase, even if it's behind a disabled flag,
must be documented. For more information, see
[the discussion that led to this decision](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47917#note_459984428). [Experiment or Beta](../../policy/experiment-beta-support.md) features are usually behind a feature flag, and must also be documented. For more information, see [Document Experiment or Beta features](experiment_beta.md).
[the discussion that led to this decision](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47917#note_459984428). [Experiment or Beta](../../policy/experiment-beta-support.md) features are usually behind a feature flag and must also be documented. For more information, see [Document Experiment or Beta features](experiment_beta.md).
When the feature is [implemented in multiple merge requests](../feature_flags/index.md#feature-flags-in-gitlab-development),
discuss the plan with your technical writer.
@ -39,7 +39,7 @@ even when the feature is not fully functional or otherwise documented.
When you document feature flags, you must:
- [Add history text](#add-history-text).
- [Add a note at the start of the topic](#use-a-note-to-describe-the-state-of-the-feature-flag).
- [Use a note to describe the state of the feature flag](#use-a-note-to-describe-the-state-of-the-feature-flag).
## Add history text
@ -50,9 +50,9 @@ Possible history entries are:
```markdown
> - [Introduced](issue-link) in GitLab X.X [with a flag](../../administration/feature_flags.md) named `flag_name`. Disabled by default.
> - [Enabled on GitLab.com](issue-link) in GitLab X.X.
> - [Enabled on GitLab.com](issue-link) in GitLab X.X. Available to GitLab.com administrators only.
> - [Enabled on self-managed](issue-link) in GitLab X.X.
> - [Enabled on GitLab.com](issue-link) in GitLab X.X.
> - [Enabled on GitLab Dedicated](issue-link) in GitLab X.X.
> - [Generally available](issue-link) in GitLab X.Y. Feature flag `flag_name` removed.
```
@ -60,7 +60,8 @@ Possible history entries are:
Information about feature flags should be in a `FLAG` note at the start of the topic (just below the history).
The note has three parts, and follows this structure:
The note has three required parts and one optional part.
The note follows this exact structure and order:
```markdown
FLAG:
@ -73,7 +74,8 @@ FLAG:
A `FLAG` note renders on the GitLab documentation site as:
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `example_flag`.
On self-managed GitLab, by default this feature is not available.
To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `example_flag`.
On GitLab.com and GitLab Dedicated, this feature is not available.
This feature is not ready for production use.
@ -95,65 +97,78 @@ This feature is not ready for production use.
| If the feature is... | Use this text |
|---------------------------------------------|---------------|
| Available | `On GitLab.com, this feature is available. On GitLab Dedicated, this feature is not available.` |
| Available | `On GitLab.com, this feature is available.` |
| Available to GitLab.com administrators only | `On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.` |
| Unavailable | `On GitLab.com and GitLab Dedicated, this feature is not available.`|
| Unavailable | `On GitLab.com, this feature is not available.` |
### GitLab Dedicated availability information
| If the feature is... | Use this text |
|---------------------------------------------|---------------|
| Available | `On GitLab Dedicated, this feature is available.` |
| Unavailable | `On GitLab Dedicated, this feature is not available.`|
| Unavailable | `On GitLab Dedicated, this feature is not available.` |
- You can combine GitLab.com and GitLab Dedicated like this:
`On GitLab.com and GitLab Dedicated, this feature is not available.`
- If the feature is behind a feature flag that is disabled for self-managed,
- If the feature is behind a flag that is disabled for self-managed GitLab,
the feature is not available for GitLab Dedicated.
### Optional information
If needed, you can add this sentence:
`The feature is not ready for production use.`
`This feature is not ready for production use.`
## Feature flag documentation examples
The following examples show the progression of a feature flag.
The following examples show the progression of a feature flag. Update the history and the `FLAG` note with every change:
```markdown
> - Introduced in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
> - [Introduced](issue-link) in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
an administrator can [enable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`.
The feature is not ready for production use. On GitLab.com and GitLab Dedicated, this feature is not available.
On self-managed GitLab, by default this feature is not available.
To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`.
On GitLab.com and GitLab Dedicated, this feature is not available.
```
When the feature is enabled in production, you can update the history:
When the feature is enabled by default on self-managed GitLab:
```markdown
> - Introduced in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
> - [Enabled on self-managed](https://gitlab.com/issue/etc) GitLab 13.8.
> - [Introduced](issue-link) in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
> - [Enabled on self-managed and GitLab Dedicated](issue-link) in GitLab 13.8.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature per user,
an administrator can [disable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`.
On self-managed GitLab, by default this feature is available.
To hide the feature, an administrator can [disable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`.
On GitLab.com, this feature is not available. On GitLab Dedicated, this feature is available.
```
When the feature is enabled by default for all offerings:
```markdown
> - [Introduced](issue-link) in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
> - [Enabled on self-managed and GitLab Dedicated](issue-link) in GitLab 13.8.
> - [Enabled on GitLab.com](issue-link) in GitLab 13.9.
FLAG:
On self-managed GitLab, by default this feature is available.
To hide the feature, an administrator can [disable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`.
On GitLab.com and GitLab Dedicated, this feature is available.
```
And, when the feature is done and fully available to all users:
When the flag is removed, add the `Generally available` entry and delete the `FLAG` note:
```markdown
> - Introduced in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
> - [Enabled on self-managed](https://gitlab.com/issue/etc) in GitLab 13.8.
> - [Enabled on GitLab.com](https://gitlab.com/issue/etc) in GitLab 13.9.
> - [Introduced](issue-link) in GitLab 13.7 [with a flag](../../administration/feature_flags.md) named `forti_token_cloud`. Disabled by default.
> - [Enabled on self-managed and GitLab Dedicated](issue-link) in GitLab 13.8.
> - [Enabled on GitLab.com](issue-link) in GitLab 13.9.
> - [Generally available](issue-link) in GitLab 14.0. Feature flag `forti_token_cloud` removed.
```
## Simplify long history
The history can get long, but you can sometimes simplify or remove entries.
The history can get long, but you can sometimes simplify or delete entries.
Combine entries if they happened in the same release:
@ -161,8 +176,8 @@ Combine entries if they happened in the same release:
```markdown
> - [Introduced](issue-link) in GitLab 14.2 [with a flag](../../administration/feature_flags.md) named `ci_include_rules`. Disabled by default.
> - [Enabled on GitLab.com](issue-link) in GitLab 14.3.
> - [Enabled on self-managed](issue-link) in GitLab 14.3.
> - [Enabled on GitLab.com](issue-link) in GitLab 14.3.
> - [Enabled on GitLab Dedicated](issue-link) in GitLab 14.3.
```
@ -170,22 +185,23 @@ Combine entries if they happened in the same release:
```markdown
> - [Introduced](issue-link) in GitLab 14.2 [with a flag](../../administration/feature_flags.md) named `ci_include_rules`. Disabled by default.
> - [Enabled on GitLab.com, self-managed, and GitLab Dedicated](issue-link) in GitLab 14.3.
> - [Enabled on self-managed, GitLab.com, and GitLab Dedicated](issue-link) in GitLab 14.3.
```
Remove `Enabled on GitLab.com` entries when the feature is enabled by default for all offerings:
Delete `Enabled` entries when the feature is enabled by default for all offerings and the flag is removed:
- Before:
```markdown
> - [Introduced](issue-link) in GitLab 15.6 [with a flag](../../administration/feature_flags.md) named `ci_hooks_pre_get_sources_script`. Disabled by default.
> - [Enabled on GitLab.com](issue-link) in GitLab 15.9.
> - [Generally available](issue-link) in GitLab 15.10. Feature flag `ci_hooks_pre_get_sources_script` removed.
> - [Enabled on self-managed and GitLab Dedicated](issue-link) in GitLab 15.7.
> - [Enabled on GitLab.com](issue-link) in GitLab 15.8.
> - [Generally available](issue-link) in GitLab 15.9. Feature flag `ci_hooks_pre_get_sources_script` removed.
```
- After:
```markdown
> - [Introduced](issue-link) in GitLab 15.6 [with a flag](../../administration/feature_flags.md) named `ci_hooks_pre_get_sources_script`. Disabled by default.
> - [Generally available](issue-link) in GitLab 15.10. Feature flag `ci_hooks_pre_get_sources_script` removed.
> - [Generally available](issue-link) in GitLab 15.9. Feature flag `ci_hooks_pre_get_sources_script` removed.
```

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "How to install Git on your local machine."
---
# Installing Git

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Use the unstage command in Git to stop tracking your changes to a file."
---
# Unstage a file in Git

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Define approval rules and limits in GitLab with merge request approval settings. Options include preventing author approval, requiring re-authentication, and removing approvals on new commits."
---
# Merge request approval settings

View File

@ -2,6 +2,7 @@
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Set auto-merge on a merge request when you have reviewed its content, so it can merge without intervention when all merge checks pass."
---
# Auto-merge

View File

@ -2,6 +2,7 @@
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Suggest improvements to the code in a merge request, and commit those improvements to the merge request directly from your browser."
---
# Suggest changes

View File

@ -2,6 +2,7 @@
stage: Create
group: IDE
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: "Write and compile code in your browser, using a secure cloud-based environment."
---
# Remote development

View File

@ -2,6 +2,7 @@
stage: Create
group: Source Code
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
description: "Use snippets to store and share code, text, and files from your browser. Snippets support version control, commenting, and embedding."
---
# Snippets

View File

@ -1038,21 +1038,6 @@ module Gitlab
end
# rubocop:enable Metrics/ParameterLists
def set_full_path(full_path:)
return unless full_path.present?
# This guard avoids Gitaly log/error spam
raise NoRepository, 'repository does not exist' unless exists?
gitaly_repository_client.set_full_path(full_path)
end
def full_path
wrapped_gitaly_errors do
gitaly_repository_client.full_path
end
end
def disconnect_alternates
wrapped_gitaly_errors do
gitaly_repository_client.disconnect_alternates

View File

@ -251,33 +251,6 @@ module Gitlab
gitaly_client_call(@storage, :repository_service, :write_ref, request, timeout: GitalyClient.fast_timeout)
end
def set_full_path(path)
gitaly_client_call(
@storage,
:repository_service,
:set_full_path,
Gitaly::SetFullPathRequest.new(
repository: @gitaly_repo,
path: path
),
timeout: GitalyClient.fast_timeout
)
nil
end
def full_path
response = gitaly_client_call(
@storage,
:repository_service,
:full_path,
Gitaly::FullPathRequest.new(repository: @gitaly_repo),
timeout: GitalyClient.fast_timeout
)
response.path.presence
end
def find_license
request = Gitaly::FindLicenseRequest.new(repository: @gitaly_repo)

View File

@ -10774,6 +10774,12 @@ msgstr ""
msgid "CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier."
msgstr ""
msgid "CiCatalog|Created and maintained by %{boldStart}GitLab%{boldEnd}"
msgstr ""
msgid "CiCatalog|Created and maintained by a %{boldStart}GitLab Partner%{boldEnd}"
msgstr ""
msgid "CiCatalog|Discover CI/CD components that can improve your pipeline with additional functionality."
msgstr ""
@ -10783,12 +10789,18 @@ msgstr ""
msgid "CiCatalog|Get started with the CI/CD Catalog"
msgstr ""
msgid "CiCatalog|GitLab-maintained"
msgstr ""
msgid "CiCatalog|Go to the project"
msgstr ""
msgid "CiCatalog|How do I publish a component?"
msgstr ""
msgid "CiCatalog|Learn more about designated creators"
msgstr ""
msgid "CiCatalog|No component available"
msgstr ""
@ -10798,6 +10810,9 @@ msgstr ""
msgid "CiCatalog|No result found"
msgstr ""
msgid "CiCatalog|Partner"
msgstr ""
msgid "CiCatalog|Publish the CI/CD components in this project to the CI/CD Catalog"
msgstr ""
@ -17633,6 +17648,9 @@ msgstr ""
msgid "DeploymentApproval| Current approvals: %{current}"
msgstr ""
msgid "DeploymentApproval|After deployment #%{deploymentIid} has the required approvals, you can run the manual job. Rejecting will fail the manual job."
msgstr ""
msgid "DeploymentApproval|Approval options"
msgstr ""
@ -17645,9 +17663,6 @@ msgstr ""
msgid "DeploymentApproval|Approved by you %{time}"
msgstr ""
msgid "DeploymentApproval|Approving will run the manual job from deployment #%{deploymentIid}. Rejecting will fail the manual job."
msgstr ""
msgid "DeploymentApproval|Deployment approved"
msgstr ""
@ -17785,6 +17800,9 @@ msgstr ""
msgid "Deployment|Latest Deployed"
msgstr ""
msgid "Deployment|Leave feedback"
msgstr ""
msgid "Deployment|Needs Approval"
msgstr ""
@ -17850,6 +17868,12 @@ msgstr ""
msgid "Deployment|Waiting to be deployed."
msgstr ""
msgid "Deployment|We know this page is a bit empty at the moment, but we're working hard to bring you a great experience! In the meantime, we'd love to hear your feedback."
msgstr ""
msgid "Deployment|What would you like to see here?"
msgstr ""
msgid "Deprecated API rate limits"
msgstr ""

View File

@ -468,6 +468,10 @@ module QA
api_post_to(api_releases_path, tag_name: tag, ref: ref, **params)
end
def has_release?(tag)
releases.any? { |release| release[:tag_name] == tag }
end
def protected_branches
response = api_get_from(api_protected_branches_path)
parse_body(response)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :skip_live_env, product_group: :pipeline_authoring do
RSpec.describe 'Verify', product_group: :pipeline_authoring do
describe 'CI catalog' do
let(:project_count) { 3 }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner, :skip_live_env, product_group: :pipeline_authoring do
RSpec.describe 'Verify', :runner, product_group: :pipeline_authoring do
describe 'CI component' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:tag) { '1.0.0' }
@ -66,6 +66,9 @@ module QA
add_ci_file(component_project, 'templates/new-component.yml', component_content)
component_project.create_release(tag)
QA::Runtime::Logger.info("Waiting for #{component_project.name}'s release #{tag} to be available")
Support::Waiter.wait_until { component_project.has_release?(tag) }
test_project.visit!
add_ci_file(test_project, '.gitlab-ci.yml', ci_yml_content)
end
@ -76,9 +79,11 @@ module QA
it 'runs in project pipeline with correct inputs', :aggregate_failures,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/451582' do
Flow::Pipeline.visit_latest_pipeline(status: 'Passed')
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |show|
Support::Waiter.wait_until { show.has_passed? }
expect(show).to have_stage(test_stage), "Expected pipeline to have stage #{test_stage} but not found."
end

View File

@ -4,7 +4,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import CiVerificationBadge from '~/ci/catalog/components/shared/ci_verification_badge.vue';
import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
describe('CiResourceHeader', () => {
@ -26,8 +26,8 @@ describe('CiResourceHeader', () => {
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findVerificationBadge = () => wrapper.findComponent(CiVerificationBadge);
const findVersionBadge = () => wrapper.findComponent(GlBadge);
const findPipelineStatusBadge = () => wrapper.findComponent(CiIcon);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(CiResourceHeader, {
@ -102,42 +102,32 @@ describe('CiResourceHeader', () => {
});
});
describe('when the project has a release', () => {
const pipelineStatus = {
detailsPath: 'path/to/pipeline',
icon: 'status_success',
text: 'passed',
group: 'success',
};
describe('verification badge', () => {
describe('when the resource is not verified', () => {
beforeEach(() => {
createComponent();
});
it('does not render the verification badge', () => {
expect(findVerificationBadge().exists()).toBe(false);
});
});
describe.each`
hasPipelineBadge | describeText | testText | status
${true} | ${'is'} | ${'renders'} | ${pipelineStatus}
${false} | ${'is not'} | ${'does not render'} | ${{}}
`('and there $describeText a pipeline', ({ hasPipelineBadge, testText, status }) => {
verificationLevel | describeText
${'GITLAB'} | ${'GitLab'}
${'PARTNER'} | ${'partner'}
`('when the resource is $describeText maintained', ({ verificationLevel }) => {
beforeEach(() => {
createComponent({
props: {
pipelineStatus: status,
latestVersion: { name: '1.0.0', path: 'path/to/release' },
},
});
createComponent({ props: { resource: { ...resource, verificationLevel } } });
});
it('renders the version badge', () => {
expect(findVersionBadge().exists()).toBe(true);
it('renders the verification badge', () => {
expect(findVerificationBadge().exists()).toBe(true);
});
it(`${testText} the pipeline status badge`, () => {
expect(findPipelineStatusBadge().exists()).toBe(hasPipelineBadge);
if (hasPipelineBadge) {
expect(findPipelineStatusBadge().props()).toEqual({
showStatusText: true,
status: pipelineStatus,
showTooltip: true,
useLink: true,
});
}
it('displays the correct badge', () => {
expect(findVerificationBadge().props('verificationLevel')).toBe(verificationLevel);
});
});
});

View File

@ -7,6 +7,7 @@ import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { createRouter } from '~/ci/catalog/router/index';
import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiVerificationBadge from '~/ci/catalog/components/shared/ci_verification_badge.vue';
import { catalogSinglePageResponse } from '../../mock';
Vue.use(VueRouter);
@ -48,6 +49,7 @@ describe('CiResourcesListItem', () => {
const findResourceName = () => wrapper.findByTestId('ci-resource-link');
const findResourceDescription = () => wrapper.findByText(defaultProps.resource.description);
const findUserLink = () => wrapper.findByTestId('user-link');
const findVerificationBadge = () => wrapper.findComponent(CiVerificationBadge);
const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
const findFavorites = () => wrapper.findByTestId('stats-favorites');
@ -139,6 +141,36 @@ describe('CiResourcesListItem', () => {
});
});
describe('verification badge', () => {
describe('when the resource is not verified', () => {
beforeEach(() => {
createComponent();
});
it('does not render the verification badge', () => {
expect(findVerificationBadge().exists()).toBe(false);
});
});
describe.each`
verificationLevel | describeText
${'GITLAB'} | ${'GitLab'}
${'PARTNER'} | ${'partner'}
`('when the resource is $describeText maintained', ({ verificationLevel }) => {
beforeEach(() => {
createComponent({ props: { resource: { ...resource, verificationLevel } } });
});
it('renders the verification badge', () => {
expect(findVerificationBadge().exists()).toBe(true);
});
it('displays the correct badge', () => {
expect(findVerificationBadge().props('verificationLevel')).toBe(verificationLevel);
});
});
});
describe('release time', () => {
describe('when there is no release data', () => {
beforeEach(() => {

View File

@ -164,8 +164,6 @@ describe('CiResourceDetailsPage', () => {
isLoadingSharedData: false,
openIssuesCount: defaultAdditionalData.openIssuesCount,
openMergeRequestsCount: defaultAdditionalData.openMergeRequestsCount,
pipelineStatus:
defaultAdditionalData.versions.nodes[0].commit.pipelines.nodes[0].detailedStatus,
resource: defaultSharedData,
});
});
@ -179,7 +177,7 @@ describe('CiResourceDetailsPage', () => {
it('passes expected props', () => {
expect(findDetailsComponent().props()).toEqual({
resourcePath: cleanLeadingSeparator(defaultSharedData.webPath),
version: defaultAdditionalData.versions.nodes[0].name,
version: defaultSharedData.versions.nodes[0].name,
});
});
});

View File

@ -0,0 +1,85 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CiVerificationBadge from '~/ci/catalog/components/shared/ci_verification_badge.vue';
import { VerificationLevel } from '~/ci/catalog/constants';
describe('Catalog Verification Badge', () => {
let wrapper;
const defaultProps = {
resourceId: 'gid://gitlab/Ci::Catalog::Resource/36',
showText: true,
verificationLevel: 'GITLAB',
};
const findVerificationIcon = () => wrapper.findComponent(GlIcon);
const findLink = () => wrapper.findComponent(GlLink);
const findVerificationText = () => wrapper.findByTestId('verification-badge-text');
const createComponent = (props = defaultProps) => {
wrapper = extendedWrapper(
shallowMount(CiVerificationBadge, {
propsData: {
...props,
},
}),
);
};
describe('when the badge is rendered', () => {
beforeEach(() => {
createComponent();
});
it('renders an icon', () => {
expect(findVerificationIcon().exists()).toBe(true);
});
it('renders a link', () => {
expect(findLink().exists()).toBe(true);
});
});
describe('badge text', () => {
describe('when showText is true', () => {
beforeEach(() => {
createComponent();
});
it('renders badge text', () => {
expect(findVerificationText().exists()).toBe(true);
});
});
describe('when showText is false', () => {
beforeEach(() => {
createComponent({ ...defaultProps, showText: false });
});
it('does not render badge text', () => {
expect(findVerificationText().exists()).toBe(false);
});
});
});
describe.each`
verificationLevel | describeText
${'GITLAB'} | ${'GitLab'}
${'PARTNER'} | ${'partner'}
`('when the resource is $describeText maintained', ({ verificationLevel }) => {
beforeEach(() => {
createComponent({ ...defaultProps, verificationLevel });
});
it('renders the correct icon', () => {
expect(findVerificationIcon().props('name')).toBe(VerificationLevel[verificationLevel].icon);
});
it('displays the correct badge text', () => {
expect(findVerificationText().text()).toContain(
VerificationLevel[verificationLevel].badgeText,
);
});
});
});

View File

@ -96,6 +96,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-42/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -110,6 +111,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-41/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -124,6 +126,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-40/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -138,6 +141,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-39/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -152,6 +156,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-38/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -166,6 +171,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-37/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -180,6 +186,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-36/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -194,6 +201,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-35/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -208,6 +216,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-34/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -222,6 +231,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-33/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -236,6 +246,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-32/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -250,6 +261,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-31/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -264,6 +276,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-30/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -278,6 +291,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-29/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -292,6 +306,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-28/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -306,6 +321,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-27/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -320,6 +336,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-26/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -334,6 +351,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-25/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -348,6 +366,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-24/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -362,6 +381,7 @@ export const catalogResponseBody = {
description: 'A simple component',
starCount: 0,
starrersPath: '/frontend-fixtures/project-23/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -394,6 +414,7 @@ export const catalogSinglePageResponse = {
name: 'Project-45 Name',
description: 'A simple component',
starCount: 0,
verificationLevel: 'UNVERIFIED',
versions: {
__typename: 'CiCatalogResourceVersionConnection',
nodes: [
@ -424,6 +445,7 @@ export const catalogSinglePageResponse = {
name: 'Project-44 Name',
description: 'A simple component',
starCount: 0,
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -437,6 +459,7 @@ export const catalogSinglePageResponse = {
name: 'Project-43 Name',
description: 'A simple component',
starCount: 0,
verificationLevel: 'UNVERIFIED',
versions: {
nodes: [],
__typename: 'CiCatalogResourceVersionConnection',
@ -460,6 +483,7 @@ export const catalogSharedDataMock = {
name: 'Ruby',
starCount: 1,
starrersPath: '/path/to/project/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
__typename: 'CiCatalogResourceVersionConnection',
nodes: [
@ -494,40 +518,6 @@ export const catalogAdditionalDetailsMock = {
openIssuesCount: 4,
openMergeRequestsCount: 10,
readmeHtml: '<h1>Hello world</h1>',
versions: {
__typename: 'CiCatalogResourceVersionConnection',
nodes: [
{
__typename: 'CiCatalogResourceVersion',
id: 'gid://gitlab/Release/3',
commit: {
__typename: 'Commit',
id: 'gid://gitlab/CommitPresenter/afa936495f20e08c26ed4a67130ee2166f94fa6e',
pipelines: {
__typename: 'PipelineConnection',
nodes: [
{
__typename: 'Pipeline',
id: 'gid://gitlab/Ci::Pipeline/583',
detailedStatus: {
__typename: 'DetailedStatus',
id: 'success-583-583',
detailsPath: '/root/cicd-circular/-/pipelines/583',
icon: 'status_success',
text: 'passed',
group: 'success',
},
},
],
},
},
name: '1.0.2',
path: '/path/to/release',
author: { __typename: 'UserCore', id: 1, webUrl: 'profile/1', name: 'username' },
createdAt: '2022-08-23T17:19:09Z',
},
],
},
},
},
};

View File

@ -0,0 +1,53 @@
import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import DetailsFeedback from '~/deployments/components/details_feedback.vue';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/graphql_shared/utils');
describe('~/deployments/components/details_feedback.vue', () => {
let wrapper;
let dismiss;
let dismisserComponent;
const createComponent = ({ shouldShowCallout = true } = {}) => {
dismiss = jest.fn();
dismisserComponent = makeMockUserCalloutDismisser({
dismiss,
shouldShowCallout,
});
wrapper = shallowMount(DetailsFeedback, {
stubs: {
UserCalloutDismisser: dismisserComponent,
},
});
};
const findAlert = () => wrapper.findComponent(GlAlert);
it('shows an alert', () => {
createComponent();
expect(findAlert().exists()).toBe(true);
});
it('calls dismiss when the alert is dismissed', () => {
createComponent();
findAlert().vm.$emit('dismiss');
expect(dismiss).toHaveBeenCalled();
});
it('links to the feedback issue', () => {
createComponent();
expect(findAlert().props()).toMatchObject({
title: 'What would you like to see here?',
primaryButtonText: 'Leave feedback',
primaryButtonLink: 'https://gitlab.com/gitlab-org/gitlab/-/issues/450700',
});
});
it('hides the alert if already dismissed', () => {
createComponent({ shouldShowCallout: false });
expect(findAlert().exists()).toBe(false);
});
});

View File

@ -9,6 +9,7 @@ import { toggleQueryPollingByVisibility } from '~/graphql_shared/utils';
import ShowDeployment from '~/deployments/components/show_deployment.vue';
import DeploymentHeader from '~/deployments/components/deployment_header.vue';
import DeploymentDeployBlock from '~/deployments/components/deployment_deploy_block.vue';
import DetailsFeedback from '~/deployments/components/details_feedback.vue';
import deploymentQuery from '~/deployments/graphql/queries/deployment.query.graphql';
import environmentQuery from '~/deployments/graphql/queries/environment.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
@ -106,6 +107,10 @@ describe('~/deployments/components/show_deployment.vue', () => {
});
});
it('shows an alert asking for feedback', () => {
expect(wrapper.findComponent(DetailsFeedback).exists()).toBe(true);
});
it('shows the deployment block if the deployment job is manual', () => {
expect(wrapper.findComponent(DeploymentDeployBlock).props()).toEqual({
deployment: mockDeploymentFixture.data.project.deployment,

View File

@ -0,0 +1,80 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import HomePanelActions from '~/pages/projects/home_panel/components/home_panel_actions.vue';
import ForksButton from '~/forks/components/forks_button.vue';
import MoreActionsDropdown from '~/groups_projects/components/more_actions_dropdown.vue';
import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
import StarCount from '~/stars/components/star_count.vue';
describe('HomePanelActions', () => {
let wrapper;
const createComponent = ({ isLoggedIn = false, provide = {} } = {}) => {
if (isLoggedIn) {
window.gon.current_user_id = 1;
}
wrapper = shallowMountExtended(HomePanelActions, {
provide: {
...provide,
},
});
};
const findAdminButton = () => wrapper.find('[data-testid="admin-button"]');
const findForksButton = () => wrapper.findComponent(ForksButton);
const findMoreActionsDropdown = () => wrapper.findComponent(MoreActionsDropdown);
const findNotificationsDropdown = () => wrapper.findComponent(NotificationsDropdown);
const findStarCount = () => wrapper.findComponent(StarCount);
describe.each`
isLoggedIn | canReadProject | isProjectEmpty | adminPath | isForkButtonVisible | isMoreActionsDropdownVisible | isNotificationDropdownVisible | isStarCountVisible | isAdminButtonVisible
${true} | ${true} | ${true} | ${undefined} | ${false} | ${true} | ${true} | ${true} | ${false}
${true} | ${true} | ${true} | ${null} | ${false} | ${true} | ${true} | ${true} | ${false}
${true} | ${true} | ${true} | ${''} | ${false} | ${true} | ${true} | ${true} | ${false}
${true} | ${true} | ${false} | ${''} | ${true} | ${true} | ${true} | ${true} | ${false}
${true} | ${false} | ${true} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${true} | ${false} | ${false} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${true} | ${true} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${true} | ${true} | ${true}
${true} | ${true} | ${false} | ${'project/admin/path'} | ${true} | ${true} | ${true} | ${true} | ${true}
${true} | ${false} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${true} | ${false} | ${false} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${true} | ${true} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${true} | ${false} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${false} | ${true} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${false} | ${false} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${true} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${true} | ${false} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${false} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${false} | ${false} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
`(
'renders components',
({
isLoggedIn,
canReadProject,
isProjectEmpty,
adminPath,
isForkButtonVisible,
isMoreActionsDropdownVisible,
isNotificationDropdownVisible,
isStarCountVisible,
isAdminButtonVisible,
}) => {
it('as expected', () => {
createComponent({
isLoggedIn,
provide: {
adminPath,
canReadProject,
isProjectEmpty,
},
});
expect(findForksButton().exists()).toBe(isForkButtonVisible);
expect(findMoreActionsDropdown().exists()).toBe(isMoreActionsDropdownVisible);
expect(findNotificationsDropdown().exists()).toBe(isNotificationDropdownVisible);
expect(findStarCount().exists()).toBe(isStarCountVisible);
expect(findAdminButton().exists()).toBe(isAdminButtonVisible);
});
},
);
});

View File

@ -1,80 +1,19 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import HomePanel from '~/pages/projects/home_panel/components/home_panel.vue';
import ForksButton from '~/forks/components/forks_button.vue';
import MoreActionsDropdown from '~/groups_projects/components/more_actions_dropdown.vue';
import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
import StarCount from '~/stars/components/star_count.vue';
import HomePanelActions from '~/pages/projects/home_panel/components/home_panel_actions.vue';
describe('HomePanel', () => {
let wrapper;
const createComponent = ({ isLoggedIn = false, provide = {} } = {}) => {
if (isLoggedIn) {
window.gon.current_user_id = 1;
}
wrapper = shallowMountExtended(HomePanel, {
provide: {
...provide,
},
});
const createComponent = () => {
wrapper = shallowMountExtended(HomePanel);
};
const findAdminButton = () => wrapper.find('[data-testid="admin-button"]');
const findForksButton = () => wrapper.findComponent(ForksButton);
const findMoreActionsDropdown = () => wrapper.findComponent(MoreActionsDropdown);
const findNotificationsDropdown = () => wrapper.findComponent(NotificationsDropdown);
const findStarCount = () => wrapper.findComponent(StarCount);
const findHomePanelActions = () => wrapper.findComponent(HomePanelActions);
describe.each`
isLoggedIn | canReadProject | isProjectEmpty | adminPath | isForkButtonVisible | isMoreActionsDropdownVisible | isNotificationDropdownVisible | isStarCountVisible | isAdminButtonVisible
${true} | ${true} | ${true} | ${undefined} | ${false} | ${true} | ${true} | ${true} | ${false}
${true} | ${true} | ${true} | ${null} | ${false} | ${true} | ${true} | ${true} | ${false}
${true} | ${true} | ${true} | ${''} | ${false} | ${true} | ${true} | ${true} | ${false}
${true} | ${true} | ${false} | ${''} | ${true} | ${true} | ${true} | ${true} | ${false}
${true} | ${false} | ${true} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${true} | ${false} | ${false} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${true} | ${true} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${true} | ${true} | ${true}
${true} | ${true} | ${false} | ${'project/admin/path'} | ${true} | ${true} | ${true} | ${true} | ${true}
${true} | ${false} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${true} | ${false} | ${false} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${true} | ${true} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${true} | ${false} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${false} | ${true} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${false} | ${false} | ${''} | ${false} | ${true} | ${false} | ${true} | ${false}
${false} | ${true} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${true} | ${false} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${false} | ${true} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
${false} | ${false} | ${false} | ${'project/admin/path'} | ${false} | ${true} | ${false} | ${true} | ${true}
`(
'renders components',
({
isLoggedIn,
canReadProject,
isProjectEmpty,
adminPath,
isForkButtonVisible,
isMoreActionsDropdownVisible,
isNotificationDropdownVisible,
isStarCountVisible,
isAdminButtonVisible,
}) => {
it('as expected', () => {
createComponent({
isLoggedIn,
provide: {
adminPath,
canReadProject,
isProjectEmpty,
},
});
it('renders components as expected', () => {
createComponent();
expect(findForksButton().exists()).toBe(isForkButtonVisible);
expect(findMoreActionsDropdown().exists()).toBe(isMoreActionsDropdownVisible);
expect(findNotificationsDropdown().exists()).toBe(isNotificationDropdownVisible);
expect(findStarCount().exists()).toBe(isStarCountVisible);
expect(findAdminButton().exists()).toBe(isAdminButtonVisible);
});
},
);
expect(findHomePanelActions().exists()).toBe(true);
});
});

View File

@ -2242,52 +2242,6 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
end
end
describe '#set_full_path' do
let(:full_path) { 'some/path' }
before do
repository.set_full_path(full_path: full_path)
end
it 'writes full_path to gitaly' do
repository.set_full_path(full_path: "not-the/real-path.git")
expect(repository.full_path).to eq('not-the/real-path.git')
end
context 'it is given an empty path' do
it 'does not write it to disk' do
repository.set_full_path(full_path: "")
expect(repository.full_path).to eq(full_path)
end
end
context 'repository does not exist' do
it 'raises NoRepository and does not call SetFullPath' do
repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project')
expect(repository.gitaly_repository_client).not_to receive(:set_full_path)
expect do
repository.set_full_path(full_path: 'foo/bar.git')
end.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
end
describe '#full_path' do
let(:full_path) { 'some/path' }
before do
repository.set_full_path(full_path: full_path)
end
it 'returns the full path' do
expect(repository.full_path).to eq(full_path)
end
end
describe '#merge_to_ref' do
let(:repository) { mutable_repository }
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }

View File

@ -451,31 +451,6 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService, feature_category: :gital
end
end
describe '#set_full_path' do
let(:path) { 'repo/path' }
it 'sends a set_full_path message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:set_full_path)
.with(gitaly_request_with_params(path: path), kind_of(Hash))
.and_return(double)
client.set_full_path(path)
end
end
describe '#full_path' do
let(:path) { 'repo/path' }
it 'sends a full_path message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:full_path)
.and_return(double(path: path))
expect(client.full_path).to eq(path)
end
end
describe "#find_license" do
it 'sends a find_license request with medium timeout' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)

View File

@ -6022,7 +6022,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
expect(ProjectCacheWorker).to receive(:perform_async).with(project.id, [], [:repository_size, :wiki_size])
expect(DetectRepositoryLanguagesWorker).to receive(:perform_async).with(project.id)
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:perform_async).with(project.id)
expect(project).to receive(:set_full_path)
project.after_import
end
@ -6175,30 +6174,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
describe '#set_full_path' do
let_it_be(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
it 'writes full path in .git/config when key is missing' do
project.set_full_path
expect(repository.full_path).to eq project.full_path
end
it 'updates full path in .git/config when key is present' do
project.set_full_path(gl_full_path: 'old/path')
expect { project.set_full_path }.to change { repository.full_path }.from('old/path').to(project.full_path)
end
it 'does not raise an error with an empty repository' do
project = create(:project_empty_repo)
expect { project.set_full_path }.not_to raise_error
end
end
describe '#default_branch' do
context 'with default_branch_name' do
let_it_be_with_refind(:root_group) { create(:group) }

View File

@ -461,10 +461,6 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an
let(:imported_project) { create_project(user, { name: 'test', import_url: 'http://import-url', import_data: import_data }) }
it 'does not write repository config' do
expect_next_instance_of(Project) do |project|
expect(project).not_to receive(:set_full_path)
end
imported_project
expect(imported_project.project_namespace).to be_in_sync_with_project(imported_project)
end

View File

@ -449,7 +449,6 @@
- spec/features/profiles/list_users_comment_template_spec.rb
- spec/features/profiles/oauth_applications_spec.rb
- spec/features/profiles/password_spec.rb
- spec/features/profiles/personal_access_tokens_spec.rb
- spec/features/profiles/two_factor_auths_spec.rb
- spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
- spec/features/profiles/user_creates_comment_template_spec.rb

View File

@ -0,0 +1,150 @@
# frozen_string_literal: true
require 'gitlab/dangerfiles/spec_helper'
require 'fast_spec_helper'
require_relative '../../../tooling/danger/master_pipeline_status'
RSpec.describe Tooling::Danger::MasterPipelineStatus, feature_category: :tooling do
include_context 'with dangerfile'
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
let(:statuses) do
[
{
'name' => 'rubocop',
'stage' => 'linter',
'status' => 'failed',
'last_finished_at' => '2016-08-12 15:23:28 UTC',
'last_failed' => {
'web_url' => "rubocop_failed_web_url",
'finished_at' => "2024-03-25T18:49:52.259Z"
}
},
{
'name' => 'rspec',
'stage' => 'test',
'status' => 'failed',
'last_finished_at' => '2016-08-12 15:23:28 UTC',
'last_failed' => {
'web_url' => "rubocop_failed_web_url",
'finished_at' => "2024-03-25T18:49:52.259Z"
}
},
{
'name' => 'eslint',
'stage' => 'test',
'status' => 'success',
'last_finished_at' => '2016-08-12 15:23:28 UTC'
}
]
end
let(:jobs) do
[
{ 'name' => 'rubocop', 'stage' => 'linter', 'allow_failure' => false },
{ 'name' => 'rspec', 'stage' => 'test', 'allow_failure' => false },
{ 'name' => 'eslint', 'stage' => 'linter', 'allow_failure' => false }
]
end
let(:file_contents_response) { JSON.pretty_generate(statuses) } # rubocop:disable Gitlab/Json -- JSON is sufficient
let(:pipeline_jobs_response) { double('PipelineJobsResponse', auto_paginate: jobs) } # rubocop:disable RSpec/VerifiedDoubles -- type is not relevant
subject(:master_pipeline_status) { fake_danger.new(helper: fake_helper) }
before do
allow(master_pipeline_status).to receive_message_chain(:gitlab, :api, :pipeline_jobs) do
pipeline_jobs_response
end
allow(master_pipeline_status).to receive_message_chain(:gitlab, :api, :file_contents) do
file_contents_response
end
allow(fake_helper).to receive(:ci?).and_return(ci_env)
end
describe 'check!' do
context 'when not in ci environment' do
let(:ci_env) { false }
it 'does not add the warnings' do
expect(master_pipeline_status).not_to receive(:warn)
master_pipeline_status.check!
end
end
context 'when in ci environment' do
let(:ci_env) { true }
context 'when all tests are reported as passed in status page' do
let(:statuses) do
[
{
'name' => 'rubocop',
'stage' => 'linter',
'status' => 'success',
'last_finished_at' => '2024-03-25T18:49:52.259Z'
}
]
end
it 'does not raise any warning' do
expect(master_pipeline_status).not_to receive(:warn)
master_pipeline_status.check!
end
end
context 'when rubocop is reproted to have failed in the pipeline status page' do
it 'raises warnings for rubocop' do
expect(master_pipeline_status).to receive(:warn).with(
<<~MSG
The [master pipeline status page](#{described_class::STATUS_PAGE_URL}) reported failures in
* [rubocop](rubocop_failed_web_url)
* [rspec](rubocop_failed_web_url)
If these jobs fail in your merge request with the same errors, then they are not caused by your changes.
Please check for any on-going incidents in the [incident issue tracker](#{described_class::INCIDENT_TRACKER_URL}) or in the `#master-broken` Slack channel.
MSG
)
master_pipeline_status.check!
end
end
context 'when api returns error when fetching pipeline status' do
let(:file_contents_response) { raise StandardError, 'Failed to fetch file_contents' }
it 'does not raise any warning' do
expect(master_pipeline_status).not_to receive(:warn)
master_pipeline_status.check!
end
end
context 'when status file does not contain a valid JSON' do
let(:file_contents_response) { '{' }
it 'does not raise any warning' do
expect(master_pipeline_status).not_to receive(:warn)
master_pipeline_status.check!
end
end
context 'when api returns error when fetching pipeline jobs' do
let(:pipeline_jobs_response) { raise StandardError, 'Failed to fetch pipeline jobs' }
it 'does not raise any warning' do
expect(master_pipeline_status).not_to receive(:warn)
master_pipeline_status.check!
end
end
end
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
require 'gitlab'
module Tooling
module Danger
module MasterPipelineStatus
MASTER_PIPELINE_STATUS_PROJECT_ID = '40549124' # https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents
MASTER_PIPELINE_STATUS_BRANCH = 'master-pipeline-status'
MASTER_PIPELINE_STATUS_FILE_NAME = 'canonical-gitlab-master-pipeline-status.json'
STATUS_PAGE_URL = 'https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/raw/master-pipeline-status/canonical-gitlab-master-pipeline-status.json'
INCIDENT_TRACKER_URL = 'https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues'
def check!
return unless helper.ci?
failed_master_pipeline_jobs = master_pipeline_jobs_statuses.select { |job| job['status'] == 'failed' }
return if failed_master_pipeline_jobs.empty?
pipeline_jobs = pipeline_jobs(ENV['CI_PROJECT_ID'], ENV['CI_PIPELINE_ID'])
return if pipeline_jobs.empty?
job_statuses_to_warn = impacted_job_statuses(pipeline_jobs, failed_master_pipeline_jobs)
warn(construct_message(job_statuses_to_warn)) if job_statuses_to_warn.any?
end
private
def master_pipeline_jobs_statuses
status_file_content = begin
gitlab.api.file_contents(
MASTER_PIPELINE_STATUS_PROJECT_ID,
MASTER_PIPELINE_STATUS_FILE_NAME,
MASTER_PIPELINE_STATUS_BRANCH
)
rescue StandardError
'[]'
end
JSON.parse(status_file_content)
rescue JSON::ParserError
[]
end
def pipeline_jobs(project_id, pipeline_id)
gitlab.api.pipeline_jobs(project_id, pipeline_id).auto_paginate
rescue StandardError
[]
end
def impacted_job_statuses(pipeline_jobs, failed_jobs_statuses)
failed_jobs_statuses.select do |failed_job_status|
pipeline_jobs.any? do |job|
failed_job_status['name'] == job['name'] &&
failed_job_status['stage'] == job['stage'] &&
!job['allow_failure']
end
end
end
def construct_message(job_statuses_to_warn)
job_list = job_statuses_to_warn.map do |job|
"* [#{job['name']}](#{job.dig('last_failed', 'web_url')})"
end.join("\n")
<<~MSG
The [master pipeline status page](#{STATUS_PAGE_URL}) reported failures in
#{job_list}
If these jobs fail in your merge request with the same errors, then they are not caused by your changes.
Please check for any on-going incidents in the [incident issue tracker](#{INCIDENT_TRACKER_URL}) or in the `#master-broken` Slack channel.
MSG
end
end
end
end

View File

@ -8,6 +8,7 @@ module Tooling
ci_templates
datateam
feature_flag
master_pipeline_status
roulette
sidekiq_queues
specialization_labels