Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-07 18:19:30 +00:00
parent 984dc66b07
commit 7fcb54624b
54 changed files with 468 additions and 428 deletions

View File

@ -290,7 +290,7 @@ gem 'autoprefixer-rails', '10.2.5.1'
gem 'terser', '1.0.2'
gem 'addressable', '~> 2.8'
gem 'tanuki_emoji', '~> 0.5'
gem 'tanuki_emoji', '~> 0.6'
gem 'gon', '~> 6.4.0'
gem 'request_store', '~> 1.5'
gem 'base32', '~> 0.3.0'

View File

@ -1261,7 +1261,7 @@ GEM
sys-filesystem (1.4.3)
ffi (~> 1.1)
sysexits (1.2.0)
tanuki_emoji (0.5.0)
tanuki_emoji (0.6.0)
temple (0.8.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
@ -1649,7 +1649,7 @@ DEPENDENCIES
stackprof (~> 0.2.15)
state_machines-activerecord (~> 0.8.0)
sys-filesystem (~> 1.4.3)
tanuki_emoji (~> 0.5)
tanuki_emoji (~> 0.6)
terser (= 1.0.2)
test-prof (~> 1.0.7)
test_file_finder (~> 0.1.3)

View File

@ -1,6 +1,7 @@
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { setUrlParams, relativePathToAbsolute, getBaseURL } from '~/lib/utils/url_utility';
import {
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
@ -56,7 +57,7 @@ export default {
},
computed: {
downloadUrl() {
return `${this.rawPath}?inline=false`;
return setUrlParams({ inline: false }, relativePathToAbsolute(this.rawPath, getBaseURL()));
},
copyDisabled() {
return this.activeViewer === RICH_BLOB_VIEWER;

View File

@ -13,23 +13,25 @@ export default class PersistentUserCallout {
this.featureId = featureId;
this.groupId = groupId;
this.deferLinks = parseBoolean(deferLinks);
this.closeButtons = this.container.querySelectorAll('.js-close');
this.init();
}
init() {
const closeButton = this.container.querySelector('.js-close');
const followLink = this.container.querySelector('.js-follow-link');
if (closeButton) {
this.handleCloseButtonCallout(closeButton);
if (this.closeButtons.length) {
this.handleCloseButtonCallout();
} else if (followLink) {
this.handleFollowLinkCallout(followLink);
}
}
handleCloseButtonCallout(closeButton) {
closeButton.addEventListener('click', (event) => this.dismiss(event));
handleCloseButtonCallout() {
this.closeButtons.forEach((closeButton) => {
closeButton.addEventListener('click', this.dismiss);
});
if (this.deferLinks) {
this.container.addEventListener('click', (event) => {
@ -47,7 +49,7 @@ export default class PersistentUserCallout {
followLink.addEventListener('click', (event) => this.registerCalloutWithLink(event));
}
dismiss(event, deferredLinkOptions = null) {
dismiss = (event, deferredLinkOptions = null) => {
event.preventDefault();
axios
@ -57,6 +59,9 @@ export default class PersistentUserCallout {
})
.then(() => {
this.container.remove();
this.closeButtons.forEach((closeButton) => {
closeButton.removeEventListener('click', this.dismiss);
});
if (deferredLinkOptions) {
const { href, target } = deferredLinkOptions;
@ -70,7 +75,7 @@ export default class PersistentUserCallout {
),
});
});
}
};
registerCalloutWithLink(event) {
event.preventDefault();

View File

@ -2,20 +2,12 @@
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { startCodeQualityWalkthrough, track } from '~/code_quality_walkthrough/utils';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { getExperimentData } from '~/experimentation/utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import PipelinesCiTemplates from './pipelines_ci_templates.vue';
export default {
i18n: {
title: s__('Pipelines|Build with confidence'),
description: s__(`Pipelines|GitLab CI/CD can automatically build,
test, and deploy your code. Let GitLab take care of time
consuming tasks, so you can spend more time creating.`),
aboutRunnersBtnText: s__('Pipelines|Learn about Runners'),
installRunnersBtnText: s__('Pipelines|Install GitLab Runners'),
codeQualityTitle: s__('Pipelines|Improve code quality with GitLab CI/CD'),
codeQualityDescription: s__(`Pipelines|To keep your codebase simple,
readable, and accessible to contributors, use GitLab CI/CD
@ -56,15 +48,9 @@ export default {
},
},
computed: {
ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md');
},
isCodeQualityExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('code_quality_walkthrough'));
},
isCiRunnerTemplatesExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('ci_runner_templates'));
},
},
mounted() {
startCodeQualityWalkthrough();
@ -73,10 +59,6 @@ export default {
trackClick() {
track('cta_clicked');
},
trackCiRunnerTemplatesClick(action) {
const tracking = new ExperimentTracking('ci_runner_templates');
tracking.event(action);
},
},
};
</script>
@ -98,33 +80,6 @@ export default {
</gl-empty-state>
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="isCiRunnerTemplatesExperimentActive" name="ci_runner_templates">
<template #control><pipelines-ci-templates /></template>
<template #candidate>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
>
<template #actions>
<gl-button
:href="ciRunnerSettingsPath"
variant="confirm"
@click="trackCiRunnerTemplatesClick('install_runners_button_clicked')"
>
{{ $options.i18n.installRunnersBtnText }}
</gl-button>
<gl-button
:href="ciHelpPagePath"
variant="default"
@click="trackCiRunnerTemplatesClick('learn_button_clicked')"
>
{{ $options.i18n.aboutRunnersBtnText }}
</gl-button>
</template>
</gl-empty-state>
</template>
</gitlab-experiment>
<pipelines-ci-templates
v-else-if="canSetCi"
:ci-runner-settings-path="ciRunnerSettingsPath"

View File

@ -1,7 +1,5 @@
import { s__, sprintf } from '~/locale';
export const EXPERIMENT_NAME = 'ci_runner_templates';
export const README_URL =
'https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/-/blob/main/easybuttons.md';

View File

@ -1,16 +1,10 @@
<script>
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
import awsCloudFormationImageUrl from 'images/aws-cloud-formation.png';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import Tracking from '~/tracking';
import { getBaseURL, objectToQuery } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import {
EXPERIMENT_NAME,
README_URL,
CF_BASE_URL,
TEMPLATES_BASE_URL,
EASY_BUTTONS,
} from './constants';
import { README_URL, CF_BASE_URL, TEMPLATES_BASE_URL, EASY_BUTTONS } from './constants';
export default {
components: {
@ -18,6 +12,7 @@ export default {
GlSprintf,
GlLink,
},
mixins: [Tracking.mixin()],
props: {
modalId: {
type: String,
@ -39,8 +34,9 @@ export default {
return CF_BASE_URL + objectToQuery(params);
},
trackCiRunnerTemplatesClick(stackName) {
const tracking = new ExperimentTracking(EXPERIMENT_NAME);
tracking.event(`template_clicked_${stackName}`);
this.track('template_clicked', {
label: stackName,
});
},
},
i18n: {

View File

@ -44,6 +44,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:task_num,
:title,
:discussion_locked,
:issue_iid,
label_ids: [],
assignee_ids: [],
reviewer_ids: [],

View File

@ -52,7 +52,6 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format|
format.html do
enable_code_quality_walkthrough_experiment
enable_ci_runner_templates_experiment
enable_runners_availability_section_experiment
end
format.json do
@ -320,19 +319,6 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
def enable_ci_runner_templates_experiment
experiment(:ci_runner_templates, namespace: project.root_ancestor) do |e|
e.exclude! unless current_user
e.exclude! unless can?(current_user, :create_pipeline, project)
e.exclude! if @pipelines_count.to_i > 0
e.exclude! if helpers.has_gitlab_ci?(project)
e.control {}
e.candidate {}
e.publish_to_database
end
end
def enable_runners_availability_section_experiment
return unless current_user
return unless can?(current_user, :create_pipeline, project)

View File

@ -10,6 +10,7 @@ module Users
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@ -47,7 +48,8 @@ module Users
!Gitlab.com? &&
current_user&.admin? &&
signup_enabled? &&
!user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
!user_dismissed?(REGISTRATION_ENABLED_CALLOUT) &&
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS.any? { |path| controller.controller_path.match?(path) }
end
def dismiss_two_factor_auth_recovery_settings_check

View File

@ -1,13 +1,17 @@
= form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= gitlab_ui_form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
- fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value}</code>"
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
= f.label :default_branch_name, _('Initial default branch name'), class: 'label-light'
= f.text_field :default_branch_name, placeholder: Gitlab::DefaultBranch.value, class: 'form-control gl-form-input'
%span.form-text.text-muted
= (s_("AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name } ).html_safe
= render 'shared/default_branch_protection', f: f
= render_if_exists 'admin/application_settings/group_owners_can_manage_default_branch_protection_setting', form: f
= f.submit _('Save changes'), class: 'gl-button btn-confirm'

View File

@ -2,9 +2,6 @@
= form_errors(@application_setting)
%fieldset
= render 'shared/default_branch_protection', f: f
= render_if_exists 'admin/application_settings/group_owners_can_manage_default_branch_protection_setting', form: f
= render 'shared/project_creation_levels', f: f, method: :default_project_creation, legend: s_('ProjectCreationLevel|Default project creation protection')
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
= render_if_exists 'admin/application_settings/default_delayed_project_deletion_setting', form: f

View File

@ -5,13 +5,13 @@
%section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Default initial branch name')
= _('Default branch')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= s_('AdminSettings|The default name for the initial branch of new repositories created in the instance.')
= s_('AdminSettings|Set the initial name and protections for the default branch of new repositories created in the instance.')
.settings-content
= render 'initial_branch_name'
= render 'default_branch'
%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header

View File

@ -35,7 +35,6 @@
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render 'groups/settings/default_branch_protection', f: f, group: @group
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
= render_if_exists 'groups/settings/prevent_forking', f: f, group: @group

View File

@ -1,22 +1,24 @@
%section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Default initial branch name')
= _('Default branch')
%button.gl-button.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= s_('GroupSettings|The default name for the initial branch of new repositories created in the group.')
= s_('GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group.')
.settings-content
= form_for @group, url: group_path(@group, anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= gitlab_ui_form_for @group, url: group_path(@group, anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@group)
- fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value(object: @group)}</code>"
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
= f.label :default_branch_name, _('Initial default branch name'), class: 'label-light'
= f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: Gitlab::DefaultBranch.value(object: @group), class: 'form-control'
%span.form-text.text-muted
= (s_("GroupSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name }).html_safe
= f.hidden_field :redirect_target, value: "repository_settings"
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm'
= render 'groups/settings/default_branch_protection', f: f, group: @group
= f.hidden_field :redirect_target, value: "repository_settings"
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm'

View File

@ -4,4 +4,4 @@
- deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.')
= render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
= render "initial_branch_name", group: @group
= render "default_branch", group: @group

View File

@ -1,14 +1,17 @@
- return unless show_registration_enabled_user_callout?
= render 'shared/global_alert',
title: _('Open registration is enabled on your instance.'),
title: _('Anyone can register for an account.'),
variant: :warning,
alert_class: 'js-registration-enabled-callout',
alert_data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: callouts_path },
close_button_data: { testid: 'close-registration-enabled-callout' } do
.gl-alert-body
= html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "<a href=\"#{help_page_path('user/admin_area/settings/sign_up_restrictions')}\" class=\"gl-link\">".html_safe, anchorClose: '</a>'.html_safe }
= _('Only allow anyone to register for accounts on GitLab instances that you intend to be used by anyone. Allowing anyone to register makes GitLab instances more vulnerable.')
.gl-alert-actions
= link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-confirm btn-md gl-button' do
%span.gl-button-text
= _('View setting')
= _('Turn off')
%button.btn.gl-alert-action.btn-default.btn-md.gl-button.js-close
%span.gl-button-text
= _('Acknowledge')

View File

@ -6,7 +6,7 @@
- create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request')
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
- create_mr_path = project_new_merge_request_path(@project, merge_request: { source_branch: @issue.to_branch_name, target_branch: @project.default_branch })
- create_mr_path = project_new_merge_request_path(@project, merge_request: { source_branch: @issue.to_branch_name, target_branch: @project.default_branch, issue_iid: @issue.iid })
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid, format: :json)
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')

View File

@ -1,4 +1,4 @@
%fieldset.form-group
%legend.h5.gl-border-none.gl-mt-0.gl-mb-3= _('Default branch protection')
.form-group
%legend.h5.gl-border-none.gl-mt-0.gl-mb-3= _('Initial default branch protection')
- Gitlab::Access.protection_options.each do |option|
= f.gitlab_ui_radio_component :default_branch_protection, option[:value], option[:label], help_text: option[:help_text]

View File

@ -1,8 +0,0 @@
---
name: ci_runner_templates
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58357
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326725
milestone: "14.0"
type: experiment
group: group::activation
default_enabled: false

View File

@ -210,6 +210,18 @@ This list of limitations only reflects the latest version of GitLab. If you are
There is a complete list of all GitLab [data types](replication/datatypes.md) and [existing support for replication and verification](replication/datatypes.md#limitations-on-replicationverification).
### View replication data on the primary site
If you try to view replication data on the primary site, you receive a warning that this may be inconsistent:
> Viewing projects and designs data from a primary site is not possible when using a unified URL. Visit the secondary site directly.
The only way to view projects replication data for a particular secondary site is to visit that secondary site directly. For example, `https://<IP of your secondary site>/admin/geo/replication/projects`.
An [epic exists](https://gitlab.com/groups/gitlab-org/-/epics/4623) to fix this limitation.
The only way to view designs replication data for a particular secondary site is to visit that secondary site directly. For example, `https://<IP of your secondary site>/admin/geo/replication/designs`.
An [epic exists](https://gitlab.com/groups/gitlab-org/-/epics/4624) to fix this limitation.
## Setup instructions
For setup instructions, see [Setting up Geo](setup/index.md).

View File

@ -140,6 +140,8 @@ for details.
To use TLS certificates with Let's Encrypt, you can manually point the domain to one of the Geo sites, generate
the certificate, then copy it to all other sites.
- [Viewing projects and designs data from a primary site is not possible when using a unified URL](../index.md#view-replication-data-on-the-primary-site).
## Behavior of secondary sites when the primary Geo site is down
Considering that web traffic is proxied to the primary, the behavior of the secondary sites differs when the primary

View File

@ -63,7 +63,8 @@ Example response:
"sync_object_storage": false,
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/sites/3/edit",
"web_geo_projects_url": "http://secondary.example.com/admin/geo/projects",
"web_geo_projects_url": "https://secondary.example.com/admin/geo/projects",
"web_geo_replication_details_url": "https://secondary.example.com/admin/geo/sites/3/replication/lfs_objects",
"_links": {
"self": "https://primary.example.com/api/v4/geo_nodes/3",
"status": "https://primary.example.com/api/v4/geo_nodes/3/status",
@ -72,6 +73,10 @@ Example response:
}
```
WARNING:
The `web_geo_projects_url` attribute is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80106)
for use in GitLab 14.9.
## Retrieve configuration about all Geo nodes
```plaintext
@ -130,6 +135,7 @@ Example response:
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/sites/2/edit",
"web_geo_projects_url": "https://secondary.example.com/admin/geo/projects",
"web_geo_replication_details_url": "https://secondary.example.com/admin/geo/sites/2/replication/lfs_objects",
"_links": {
"self":"https://primary.example.com/api/v4/geo_nodes/2",
"status":"https://primary.example.com/api/v4/geo_nodes/2/status",
@ -139,6 +145,10 @@ Example response:
]
```
WARNING:
The `web_geo_projects_url` attribute is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80106)
for use in GitLab 14.9.
## Retrieve configuration about a specific Geo node
```plaintext
@ -228,6 +238,7 @@ Example response:
"clone_protocol": "http",
"web_edit_url": "https://primary.example.com/admin/geo/sites/2/edit",
"web_geo_projects_url": "https://secondary.example.com/admin/geo/projects",
"web_geo_replication_details_url": "https://secondary.example.com/admin/geo/sites/2/replication/lfs_objects",
"_links": {
"self":"https://primary.example.com/api/v4/geo_nodes/2",
"status":"https://primary.example.com/api/v4/geo_nodes/2/status",
@ -236,6 +247,10 @@ Example response:
}
```
WARNING:
The `web_geo_projects_url` attribute is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80106)
for use in GitLab 14.9.
## Delete a Geo node
Removes the Geo node.

View File

@ -219,14 +219,14 @@ To update the Emoji aliases file (used for Emoji autocomplete), run the
following:
```shell
bundle exec rake gemojione:aliases
bundle exec rake tanuki_emoji:aliases
```
To update the Emoji digests file (used for Emoji autocomplete), run the
following:
```shell
bundle exec rake gemojione:digests
bundle exec rake tanuki_emoji:digests
```
This updates the file `fixtures/emojis/digests.json` based on the currently
@ -235,7 +235,7 @@ available Emoji.
To generate a sprite file containing all the Emoji, run:
```shell
bundle exec rake gemojione:sprite
bundle exec rake tanuki_emoji:sprite
```
If new emoji are added, the sprite sheet may change size. To compensate for

View File

@ -16,7 +16,7 @@ Service Ping consists of two kinds of data:
To implement a new metric in Service Ping, follow these steps:
1. [Implement the required counter](#types-of-counters)
1. [Name and place the metric](#name-and-place-the-metric)
1. [Name and place the metric](metrics_dictionary.md#metric-key_path)
1. [Test counters manually using your Rails console](#test-counters-manually-using-your-rails-console)
1. [Generate the SQL query](#generate-the-sql-query)
1. [Optimize queries with `#database-lab`](#optimize-queries-with-database-lab)
@ -660,29 +660,6 @@ We return fallback values in these cases:
| Timeouts, general failures | -1 |
| Standard errors in counters | -2 |
## Name and place the metric
Add the metric in one of the top-level keys:
- `settings`: for settings related metrics.
- `counts_weekly`: for counters that have data for the most recent 7 days.
- `counts_monthly`: for counters that have data for the most recent 28 days.
- `counts`: for counters that have data for all time.
### How to get a metric name suggestion
The metric YAML generator can suggest a metric name for you.
To generate a metric name suggestion, first instrument the metric at the provided `key_path`.
Then, generate the metric's YAML definition and
return to the instrumentation and update it.
1. Add the metric instrumentation to `lib/gitlab/usage_data.rb` inside one
of the [top-level keys](#name-and-place-the-metric), using any name you choose.
1. Run the [metrics YAML generator](metrics_dictionary.md#metrics-definition-and-validation).
1. Use the metric name suggestion to select a suitable metric name.
1. Update the instrumentation you created in the first step and change the metric name to the suggested name.
1. Update the metric's YAML definition with the correct `key_path`.
## Test counters manually using your Rails console
```ruby

View File

@ -54,6 +54,51 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| `options` | no | `object`: options information needed to calculate the metric value. |
| `skip_validation` | no | This should **not** be set. [Used for imported metrics until we review, update and make them valid](https://gitlab.com/groups/gitlab-org/-/epics/5425). |
### Metric key_path
The `key_path` of the metric is the location in the JSON Service Ping payload.
The `key_path` could be composed from multiple parts separated by `.` and it must be unique.
We recommend to add the metric in one of the top-level keys:
- `settings`: for settings related metrics.
- `counts_weekly`: for counters that have data for the most recent 7 days.
- `counts_monthly`: for counters that have data for the most recent 28 days.
- `counts`: for counters that have data for all time.
NOTE:
We can't control what the metric's `key_path` is, because some of them are generated dynamically in `usage_data.rb`.
For example, see [Redis HLL metrics](implement.md#redis-hll-counters).
### Metric name
To improve metric discoverability by a wider audience, each metric with
instrumentation added at an appointed `key_path` receives a `name` attribute
filled with the name suggestion, corresponding to the metric `data_source` and instrumentation.
Metric name suggestions can contain two types of elements:
1. **User input prompts**: enclosed by angle brackets (`< >`), these pieces should be replaced or
removed when you create a metrics YAML file.
1. **Fixed suggestion**: plaintext parts generated according to well-defined algorithms.
They are based on underlying instrumentation, and must not be changed.
For a metric name to be valid, it must not include any prompt, and fixed suggestions
must not be changed.
#### Generate a metric name suggestion
The metric YAML generator can suggest a metric name for you.
To generate a metric name suggestion, first instrument the metric at the provided `key_path`.
Then, generate the metric's YAML definition and
return to the instrumentation and update it.
1. Add the metric instrumentation class to `lib/gitlab/usage/metrics/instrumentations/`.
1. Add the metric logic in the instrumentation class.
1. Run the [metrics YAML generator](metrics_dictionary.md#metrics-definition-and-validation).
1. Use the metric name suggestion to select a suitable metric name.
1. Update the metric's YAML definition with the correct `key_path`.
### Metric statuses
Metric definitions can have one of the following statuses:
@ -81,21 +126,6 @@ which has a related schema in `/config/metrics/objects_schemas/topology_schema.j
- `all`: The metric data applies for the whole time the metric has been active (all-time interval). For example, the following metric counts all users that create issues: `/config/metrics/counts_all/20210216181115_issues.yml`.
- `none`: The metric collects a type of data that's not tracked over time, such as settings and configuration information. Therefore, a time interval is not applicable. For example, `uuid` has no time interval applicable: `config/metrics/license/20210201124933_uuid.yml`.
### Metric name
To improve metric discoverability by a wider audience, each metric with
instrumentation added at an appointed `key_path` receives a `name` attribute
filled with the name suggestion, corresponding to the metric `data_source` and instrumentation.
Metric name suggestions can contain two types of elements:
1. **User input prompts**: Enclosed by `<>`, these pieces should be replaced or
removed when you create a metrics YAML file.
1. **Fixed suggestion**: Plaintext parts generated according to well-defined algorithms.
They are based on underlying instrumentation, and should not be changed.
For a metric name to be valid, it must not include any prompt, and no fixed suggestions
should be changed.
### Data category
We use the following categories to classify a metric:

View File

@ -51,9 +51,9 @@ are regular backend changes.
#### The Product Intelligence **reviewer** should
- Perform a first-pass review on the merge request and suggest improvements to the author.
- Check the [metrics location](implement.md#name-and-place-the-metric) in
- Check the [metrics location](metrics_dictionary.md#metric-key_path) in
the Service Ping JSON payload.
- Suggest that the author checks the [naming suggestion](implement.md#how-to-get-a-metric-name-suggestion) while
- Suggest that the author checks the [naming suggestion](metrics_dictionary.md#generate-a-metric-name-suggestion) while
generating the metric's YAML definition.
- Add the `~database` label and ask for a [database review](../database_review.md) for
metrics that are based on Database.

View File

@ -170,6 +170,8 @@ The **Repository** settings contain:
- [Repository's custom initial branch name](../../project/repository/branches/default.md#instance-level-custom-initial-branch-name) -
Set a custom branch name for new repositories created in your instance.
- [Repository's initial default branch protection](../../project/repository/branches/default.md#instance-level-default-branch-protection) -
Configure the branch protections to apply to every repository's default branch.
- [Repository mirror](visibility_and_access_controls.md#enable-project-mirroring) -
Configure repository mirroring.
- [Repository storage](../../../administration/repository_storage_types.md) - Configure storage path settings.

View File

@ -17,54 +17,6 @@ To access the visibility and access control options:
1. On the left sidebar, select **Settings > General**.
1. Expand the **Visibility and access controls** section.
## Protect default branches
With this option, you can define [branch protections](../../project/protected_branches.md)
to apply to every repository's [default branch](../../project/repository/branches/default.md).
These protections specify the user roles with permission to push to default branches.
This setting applies only to each repository's default branch. To protect other branches,
you must configure [branch protection in the repository](../../project/protected_branches.md),
or configure [branch protection for groups](../../group/index.md#change-the-default-branch-protection-of-a-group).
To change the default branch protection for the entire instance:
1. Sign in to GitLab as a user with Administrator access level.
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Visibility and access controls** section.
1. Select a **Default branch protection**:
- **Not protected** - Both developers and maintainers can push new commits
and force push.
- **Protected against pushes** - Developers cannot push new commits, but are
allowed to accept merge requests to the branch. Maintainers can push to the branch.
- **Partially protected** - Both developers and maintainers can push new commits,
but cannot force push.
- **Fully protected** - Developers cannot push new commits, but maintainers can.
No one can force push.
1. To allow group owners to override the instance's default branch protection, select
[**Allow owners to manage default branch protection per group**](#prevent-overrides-of-default-branch-protection).
1. Select **Save changes**.
### Prevent overrides of default branch protection **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211944) in GitLab 13.0.
Instance-level protections for [default branch](../../project/repository/branches/default.md)
can be overridden on a per-group basis by the group's owner. In
[GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can
disable this privilege for group owners, enforcing the instance-level protection rule:
1. Sign in to GitLab as a user with Administrator access level.
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Visibility and access controls** section.
1. Clear the **Allow owners to manage default branch protection per group** checkbox.
1. Select **Save changes**.
NOTE:
GitLab administrators can still update the default branch protection of a group.
## Define which roles can create projects
Instance-level protections for project creation define which roles can

View File

@ -204,24 +204,17 @@ A to-do item is created for all the group and subgroup members.
## Change the default branch protection of a group
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7583) in GitLab 12.9.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7583) in GitLab 12.9.
> - [Settings moved and renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/340403) in GitLab 14.9.
By default, every group inherits the branch protection set at the global level.
To change this setting for a specific group:
To change this setting for a specific group, see [group level default branch protection](../project/repository/branches/default.md#group-level-default-branch-protection).
1. On the top bar, select **Menu > Groups**.
1. Select **Your Groups**.
1. Find the group and select it.
1. From the left menu, select **Settings > General**.
1. Expand the **Permissions and group features** section.
1. Select the desired option in the **Default branch protection** dropdown list.
1. Select **Save changes**.
To change this setting globally, see [Default branch protection](../admin_area/settings/visibility_and_access_controls.md#protect-default-branches).
To change this setting globally, see [initial default branch protection](../project/repository/branches/default.md#instance-level-default-branch-protection).
NOTE:
In [GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can choose to [disable group owners from updating the default branch protection](../admin_area/settings/visibility_and_access_controls.md#prevent-overrides-of-default-branch-protection).
In [GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can choose to [disable group owners from updating the default branch protection](../project/repository/branches/default.md#prevent-overrides-of-default-branch-protection).
## Add projects to a group

View File

@ -29,7 +29,7 @@ When a branch is protected, the default behavior enforces these restrictions on
### Set the default branch protection level
Administrators can set a default branch protection level in the
[Admin Area](../admin_area/settings/visibility_and_access_controls.md#protect-default-branches).
[Admin Area](../project/repository/branches/default.md#instance-level-default-branch-protection).
## Configure a protected branch

View File

@ -81,13 +81,83 @@ overrides it.
Users with at least the Owner role of groups and subgroups can configure the default branch name for a group:
1. Go to the group **Settings > Repository**.
1. Expand **Default initial branch name**.
1. Expand **Default branch**.
1. Change the default initial branch to a custom name of your choice.
1. Select **Save changes**.
Projects created in this group after you change the setting use the custom branch name,
unless a subgroup configuration overrides it.
## Protect initial default branches **(FREE SELF)**
GitLab administrators and group owners can define [branch protections](../../../project/protected_branches.md)
to apply to every repository's [default branch](#default-branch)
at the [instance level](#instance-level-default-branch-protection) and
[group level](#group-level-default-branch-protection) with one of the following options:
- **Not protected** - Both developers and maintainers can push new commits
and force push.
- **Protected against pushes** - Developers cannot push new commits, but are
allowed to accept merge requests to the branch. Maintainers can push to the branch.
- **Partially protected** - Both developers and maintainers can push new commits,
but cannot force push.
- **Fully protected** - Developers cannot push new commits, but maintainers can.
No one can force push.
### Instance-level default branch protection **(FREE SELF)**
This setting applies only to each repository's default branch. To protect other branches,
you must either:
- Configure [branch protection in the repository](../../../project/protected_branches.md).
- Configure [branch protection for groups](../../../group/index.md#change-the-default-branch-protection-of-a-group).
Administrators of self-managed instances can customize the initial default branch protection for projects hosted on that instance. Individual
groups and subgroups can override this instance-wide setting for their projects.
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Repository**.
1. Expand **Default branch**.
1. Select [**Initial default branch protection**](#protect-initial-default-branches).
1. To allow group owners to override the instance's default branch protection, select
[**Allow owners to manage default branch protection per group**](#prevent-overrides-of-default-branch-protection).
1. Select **Save changes**.
#### Prevent overrides of default branch protection **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211944) in GitLab 13.0.
Instance-level protections for default branches
can be overridden on a per-group basis by the group's owner. In
[GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can
disable this privilege for group owners, enforcing the instance-level protection rule:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Repository**.
1. Expand the **Default branch** section.
1. Clear the **Allow owners to manage default branch protection per group** checkbox.
1. Select **Save changes**.
NOTE:
GitLab administrators can still update the default branch protection of a group.
### Group-level default branch protection **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7583) in GitLab 12.9.
> - [Settings moved and renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/340403) in GitLab 14.9.
Instance-level protections for [default branch](#default-branch)
can be overridden on a per-group basis by the group's owner. In
[GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can
[enforce protection of initial default branches](#prevent-overrides-of-default-branch-protection)
which locks this setting for group owners.
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Repository**.
1. Expand **Default branch**.
1. Select [**Initial default branch protection**](#protect-initial-default-branches).
1. Select **Save changes**.
## Update the default branch name in your repository
WARNING:

View File

@ -1,6 +1,4 @@
{
":) ":"smile",
":( ":"disappointed",
"small_airplane":"airplane_small",
"right_anger_bubble":"anger_right",
"keycap_asterisk":"asterisk",
@ -54,6 +52,7 @@
"passenger_ship":"cruise_ship",
"dagger_knife":"dagger",
"desktop_computer":"desktop",
":( ":"disappointed",
"card_index_dividers":"dividers",
"dove_of_peace":"dove",
"drool":"drooling_face",
@ -488,6 +487,7 @@
"skull_and_crossbones":"skull_crossbones",
"slightly_frowning_face":"slight_frown",
"slightly_smiling_face":"slight_smile",
":) ":"smile",
"sneeze":"sneezing_face",
"speaking_head_in_silhouette":"speaking_head",
"left_speech_bubble":"speech_left",

View File

@ -3,12 +3,20 @@
namespace :tanuki_emoji do
desc 'Generates Emoji aliases fixtures'
task aliases: :environment do
ALLOWED_ALIASES = [':)', ':('].freeze
aliases = {}
TanukiEmoji.index.all.each do |emoji|
emoji.aliases.each do |emoji_alias|
aliases[TanukiEmoji::Character.format_name(emoji_alias)] = emoji.name
end
emoji.ascii_aliases.intersection(ALLOWED_ALIASES).each do |ascii_alias|
# We add an extra space at the end so that when a user types ":) "
# we'd still match this alias and not show "cocos (keeling) islands" as the first result.
# The initial ":" is ignored when matching because it's our emoji prefix in Markdown.
aliases[ascii_alias + ' '] = emoji.name
end
end
aliases_json_file = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')

View File

@ -462,9 +462,6 @@ msgstr ""
msgid "%{address} is an invalid IP address range"
msgstr ""
msgid "%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance."
msgstr ""
msgid "%{author_link} cloned %{original_issue} to %{new_issue}."
msgstr ""
@ -1986,6 +1983,9 @@ msgstr ""
msgid "AccountValidation|you may %{unsubscribe_link} at any time."
msgstr ""
msgid "Acknowledge"
msgstr ""
msgid "Action"
msgstr ""
@ -2589,6 +2589,9 @@ msgstr ""
msgid "AdminSettings|A Let's Encrypt account will be configured for this GitLab instance using this email address. You will receive emails to warn of expiring certificates. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "AdminSettings|Affects all new and existing groups."
msgstr ""
msgid "AdminSettings|All new projects can use the instance's shared runners by default."
msgstr ""
@ -2664,6 +2667,9 @@ msgstr ""
msgid "AdminSettings|Set a CI/CD template as the required pipeline configuration for all projects in the instance. Project CI/CD configuration merges into the required pipeline configuration when the pipeline runs. %{link_start}What is a required pipeline configuration?%{link_end}"
msgstr ""
msgid "AdminSettings|Set the initial name and protections for the default branch of new repositories created in the instance."
msgstr ""
msgid "AdminSettings|Set the maximum size of GitLab Pages per project (0 for unlimited). %{link_start}Learn more.%{link_end}"
msgstr ""
@ -2673,9 +2679,6 @@ msgstr ""
msgid "AdminSettings|The default domain to use for Auto Review Apps and Auto Deploy stages in all projects."
msgstr ""
msgid "AdminSettings|The default name for the initial branch of new repositories created in the instance."
msgstr ""
msgid "AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire."
msgstr ""
@ -3591,7 +3594,7 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
msgid "Allow owners to manage default branch protection per group"
msgid "Allow owners to manage default branch protection per group."
msgstr ""
msgid "Allow owners to manually add users outside of LDAP"
@ -4208,6 +4211,9 @@ msgstr ""
msgid "Any namespace"
msgstr ""
msgid "Anyone can register for an account."
msgstr ""
msgid "App ID"
msgstr ""
@ -11561,9 +11567,6 @@ msgstr ""
msgid "Default branch and protected branches"
msgstr ""
msgid "Default branch protection"
msgstr ""
msgid "Default delayed project deletion"
msgstr ""
@ -11582,9 +11585,6 @@ msgstr ""
msgid "Default first day of the week in calendars and date pickers."
msgstr ""
msgid "Default initial branch name"
msgstr ""
msgid "Default project deletion protection"
msgstr ""
@ -17659,15 +17659,15 @@ msgstr ""
msgid "GroupSettings|Select the project that contains your custom Insights file."
msgstr ""
msgid "GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group."
msgstr ""
msgid "GroupSettings|Set the maximum size of GitLab Pages for this group. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found."
msgstr ""
msgid "GroupSettings|The default name for the initial branch of new repositories created in the group."
msgstr ""
msgid "GroupSettings|The projects in this subgroup can be selected as templates for new projects created in the group. %{link_start}Learn more.%{link_end}"
msgstr ""
@ -19601,6 +19601,12 @@ msgstr ""
msgid "Inherited:"
msgstr ""
msgid "Initial default branch name"
msgstr ""
msgid "Initial default branch protection"
msgstr ""
msgid "Inline"
msgstr ""
@ -25632,6 +25638,9 @@ msgstr ""
msgid "Only admins can delete project"
msgstr ""
msgid "Only allow anyone to register for accounts on GitLab instances that you intend to be used by anyone. Allowing anyone to register makes GitLab instances more vulnerable."
msgstr ""
msgid "Only effective when remote storage is enabled. Set to 0 for no size limit."
msgstr ""
@ -25704,9 +25713,6 @@ msgstr ""
msgid "Open raw"
msgstr ""
msgid "Open registration is enabled on your instance."
msgstr ""
msgid "Open sidebar"
msgstr ""
@ -27008,18 +27014,12 @@ msgstr ""
msgid "Pipelines|Install GitLab Runner"
msgstr ""
msgid "Pipelines|Install GitLab Runners"
msgstr ""
msgid "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource."
msgstr ""
msgid "Pipelines|Last Used"
msgstr ""
msgid "Pipelines|Learn about Runners"
msgstr ""
msgid "Pipelines|Learn the basics of pipelines and .yml files"
msgstr ""
@ -40609,9 +40609,6 @@ msgstr ""
msgid "View seat usage"
msgstr ""
msgid "View setting"
msgstr ""
msgid "View supported languages and frameworks"
msgstr ""
@ -40636,6 +40633,9 @@ msgstr ""
msgid "Viewing commit"
msgstr ""
msgid "Viewing projects and designs data from a primary site is not possible when using a unified URL. Visit the secondary site directly. %{geo_help_url}"
msgstr ""
msgid "Violation"
msgstr ""

View File

@ -296,10 +296,6 @@ RSpec.describe Projects::PipelinesController do
it_behaves_like 'tracks assignment and records the subject', :code_quality_walkthrough, :namespace
end
context 'ci_runner_templates experiment' do
it_behaves_like 'tracks assignment and records the subject', :ci_runner_templates, :namespace
end
context 'runners_availability_section experiment' do
it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace
end

View File

@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe 'Registration enabled callout' do
let_it_be(:admin) { create(:admin) }
let_it_be(:non_admin) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:callout_title) { _('Anyone can register for an account.') }
context 'when "Sign-up enabled" setting is `true`' do
before do
@ -14,23 +16,42 @@ RSpec.describe 'Registration enabled callout' do
context 'when an admin is logged in' do
before do
sign_in(admin)
visit root_dashboard_path
end
it 'displays callout' do
expect(page).to have_content 'Open registration is enabled on your instance.'
expect(page).to have_link 'View setting', href: general_admin_application_settings_path(anchor: 'js-signup-settings')
it 'displays callout on admin and dashboard pages and root page' do
visit root_path
expect(page).to have_content callout_title
expect(page).to have_link _('Turn off'), href: general_admin_application_settings_path(anchor: 'js-signup-settings')
visit root_dashboard_path
expect(page).to have_content callout_title
visit admin_root_path
expect(page).to have_content callout_title
end
it 'does not display callout on pages other than root, admin, or dashboard' do
visit project_issues_path(project)
expect(page).not_to have_content callout_title
end
context 'when callout is dismissed', :js do
before do
visit admin_root_path
find('[data-testid="close-registration-enabled-callout"]').click
wait_for_requests
visit root_dashboard_path
end
it 'does not display callout' do
expect(page).not_to have_content 'Open registration is enabled on your instance.'
expect(page).not_to have_content callout_title
end
end
end
@ -42,7 +63,7 @@ RSpec.describe 'Registration enabled callout' do
end
it 'does not display callout' do
expect(page).not_to have_content 'Open registration is enabled on your instance.'
expect(page).not_to have_content callout_title
end
end
end

View File

@ -26,7 +26,7 @@ RSpec.describe 'Group Repository settings' do
end
end
context 'Default initial branch name' do
context 'Default branch' do
before do
visit group_settings_repository_path(group)
end
@ -37,8 +37,8 @@ RSpec.describe 'Group Repository settings' do
it 'renders the correct setting section content' do
within("#js-default-branch-name") do
expect(page).to have_content("Default initial branch name")
expect(page).to have_content("The default name for the initial branch of new repositories created in the group.")
expect(page).to have_content("Default branch")
expect(page).to have_content("Set the initial name and protections for the default branch of new repositories created in the group.")
end
end
end

View File

@ -32,7 +32,7 @@ RSpec.describe 'User searches group settings', :js do
visit group_settings_repository_path(group)
end
it_behaves_like 'can search settings', 'Deploy tokens', 'Default initial branch name'
it_behaves_like 'can search settings', 'Deploy tokens', 'Default branch'
end
context 'in CI/CD page' do

View File

@ -73,7 +73,9 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
expect(page).to have_content('New merge request')
expect(page).to have_content("From #{issue.to_branch_name} into #{project.default_branch}")
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: issue.to_branch_name, target_branch: project.default_branch }))
expect(page).to have_content("Closes ##{issue.iid}")
expect(page).to have_field("Title", with: "Draft: Resolve \"Cherry-Coloured Funk\"")
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: issue.to_branch_name, target_branch: project.default_branch, issue_iid: issue.iid }))
end
end
@ -96,7 +98,9 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
expect(page).to have_content('New merge request')
expect(page).to have_content("From #{branch_name} into #{project.default_branch}")
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: branch_name, target_branch: project.default_branch }))
expect(page).to have_content("Closes ##{issue.iid}")
expect(page).to have_field("Title", with: "Draft: Resolve \"Cherry-Coloured Funk\"")
expect(page).to have_current_path(project_new_merge_request_path(project, merge_request: { source_branch: branch_name, target_branch: project.default_branch, issue_iid: issue.iid }))
end
end

View File

@ -1009,6 +1009,29 @@ RSpec.describe 'File blob', :js do
stub_application_setting(static_objects_external_storage_url: 'https://cdn.gitlab.com')
end
context 'private project' do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:user) { create(:user, static_object_token: 'ABCD1234') }
before do
project.add_developer(user)
sign_in(user)
visit_blob('README.md')
end
it 'shows open raw and download buttons with external storage URL prepended and user token appended to their href' do
path = project_raw_path(project, 'master/README.md')
raw_uri = "https://cdn.gitlab.com#{path}?token=#{user.static_object_token}"
download_uri = "https://cdn.gitlab.com#{path}?token=#{user.static_object_token}&inline=false"
aggregate_failures do
expect(page).to have_link 'Open raw', href: raw_uri
expect(page).to have_link 'Download', href: download_uri
end
end
end
context 'public project' do
before do
visit_blob('README.md')
@ -1061,37 +1084,5 @@ RSpec.describe 'File blob', :js do
end
end
end
context 'when static objects external storage is enabled' do
# We need to unsre that this test runs with the refactor_blob_viewer feature flag enabled
# This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/351555
before do
stub_application_setting(static_objects_external_storage_url: 'https://cdn.gitlab.com')
end
context 'private project' do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:user) { create(:user) }
before do
project.add_developer(user)
sign_in(user)
visit_blob('README.md')
end
it 'shows open raw and download buttons with external storage URL prepended and user token appended to their href' do
path = project_raw_path(project, 'master/README.md')
raw_uri = "https://cdn.gitlab.com#{path}?token=#{user.static_object_token}"
download_uri = "https://cdn.gitlab.com#{path}?inline=false&token=#{user.static_object_token}"
aggregate_failures do
expect(page).to have_link 'Open raw', href: raw_uri
expect(page).to have_link 'Download', href: download_uri
end
end
end
end
end
end

View File

@ -27,7 +27,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
<default-actions-stub
activeviewer="simple"
rawpath="/flightjs/flight/snippets/51/raw"
rawpath="https://testing.com/flightjs/flight/snippets/51/raw"
/>
</div>
</div>

View File

@ -22,7 +22,7 @@ export const Blob = {
binary: false,
name: 'dummy.md',
path: 'foo/bar/dummy.md',
rawPath: '/flightjs/flight/snippets/51/raw',
rawPath: 'https://testing.com/flightjs/flight/snippets/51/raw',
size: 75,
simpleViewer: {
...SimpleViewerMock,

View File

@ -59,7 +59,7 @@ describe('CreateMergeRequestDropdown', () => {
describe('updateCreatePaths', () => {
it('escapes branch names correctly', () => {
dropdown.createBranchPath = `${TEST_HOST}/branches?branch_name=some-branch&issue=42`;
dropdown.createMrPath = `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=test&merge_request%5Btarget_branch%5D=master`;
dropdown.createMrPath = `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=test&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`;
dropdown.updateCreatePaths('branch', 'contains#hash');
@ -68,7 +68,7 @@ describe('CreateMergeRequestDropdown', () => {
);
expect(dropdown.createMrPath).toBe(
`${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master`,
`${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`,
);
});
});

View File

@ -21,7 +21,8 @@ describe('PersistentUserCallout', () => {
data-feature-id="${featureName}"
data-group-id="${groupId}"
>
<button type="button" class="js-close"></button>
<button type="button" class="js-close js-close-primary"></button>
<button type="button" class="js-close js-close-secondary"></button>
</div>
`;
@ -64,14 +65,15 @@ describe('PersistentUserCallout', () => {
}
describe('dismiss', () => {
let button;
const buttons = {};
let mockAxios;
let persistentUserCallout;
beforeEach(() => {
const fixture = createFixture();
const container = fixture.querySelector('.container');
button = fixture.querySelector('.js-close');
buttons.primary = fixture.querySelector('.js-close-primary');
buttons.secondary = fixture.querySelector('.js-close-secondary');
mockAxios = new MockAdapter(axios);
persistentUserCallout = new PersistentUserCallout(container);
jest.spyOn(persistentUserCallout.container, 'remove').mockImplementation(() => {});
@ -81,29 +83,33 @@ describe('PersistentUserCallout', () => {
mockAxios.restore();
});
it('POSTs endpoint and removes container when clicking close', () => {
it.each`
button
${'primary'}
${'secondary'}
`('POSTs endpoint and removes container when clicking $button close', async ({ button }) => {
mockAxios.onPost(dismissEndpoint).replyOnce(200);
button.click();
buttons[button].click();
return waitForPromises().then(() => {
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(
JSON.stringify({ feature_name: featureName, group_id: groupId }),
);
});
await waitForPromises();
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(
JSON.stringify({ feature_name: featureName, group_id: groupId }),
);
});
it('invokes Flash when the dismiss request fails', () => {
it('invokes Flash when the dismiss request fails', async () => {
mockAxios.onPost(dismissEndpoint).replyOnce(500);
button.click();
buttons.primary.click();
return waitForPromises().then(() => {
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while dismissing the alert. Refresh the page and try again.',
});
await waitForPromises();
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred while dismissing the alert. Refresh the page and try again.',
});
});
});
@ -132,37 +138,37 @@ describe('PersistentUserCallout', () => {
mockAxios.restore();
});
it('defers loading of a link until callout is dismissed', () => {
it('defers loading of a link until callout is dismissed', async () => {
const { href, target } = deferredLink;
mockAxios.onPost(dismissEndpoint).replyOnce(200);
deferredLink.click();
return waitForPromises().then(() => {
expect(windowSpy).toHaveBeenCalledWith(href, target);
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
await waitForPromises();
expect(windowSpy).toHaveBeenCalledWith(href, target);
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
it('does not dismiss callout on non-deferred links', () => {
it('does not dismiss callout on non-deferred links', async () => {
normalLink.click();
return waitForPromises().then(() => {
expect(windowSpy).not.toHaveBeenCalled();
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
});
await waitForPromises();
expect(windowSpy).not.toHaveBeenCalled();
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
});
it('does not follow link when notification is closed', () => {
it('does not follow link when notification is closed', async () => {
mockAxios.onPost(dismissEndpoint).replyOnce(200);
button.click();
return waitForPromises().then(() => {
expect(windowSpy).not.toHaveBeenCalled();
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
});
await waitForPromises();
expect(windowSpy).not.toHaveBeenCalled();
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
});
});
@ -187,30 +193,30 @@ describe('PersistentUserCallout', () => {
mockAxios.restore();
});
it('uses a link to trigger callout and defers following until callout is finished', () => {
it('uses a link to trigger callout and defers following until callout is finished', async () => {
const { href } = link;
mockAxios.onPost(dismissEndpoint).replyOnce(200);
link.click();
return waitForPromises().then(() => {
expect(window.location.assign).toBeCalledWith(href);
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
await waitForPromises();
expect(window.location.assign).toBeCalledWith(href);
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
it('invokes Flash when the dismiss request fails', () => {
it('invokes Flash when the dismiss request fails', async () => {
mockAxios.onPost(dismissEndpoint).replyOnce(500);
link.click();
return waitForPromises().then(() => {
expect(window.location.assign).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message:
'An error occurred while acknowledging the notification. Refresh the page and try again.',
});
await waitForPromises();
expect(window.location.assign).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message:
'An error occurred while acknowledging the notification. Refresh the page and try again.',
});
});
});

View File

@ -586,44 +586,6 @@ describe('Pipelines', () => {
});
});
describe('when the ci_runner_templates experiment is active', () => {
beforeAll(() => {
getExperimentData.mockImplementation((name) => name === 'ci_runner_templates');
});
describe('the control state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('control');
});
it('renders the CI/CD templates', () => {
expect(wrapper.findComponent(PipelinesCiTemplates).exists()).toBe(true);
});
});
describe('the candidate state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('candidate');
});
it('renders two buttons', () => {
expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
'Install GitLab Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
paths.ciRunnerSettingsPath,
);
expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
'Learn about Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
});
});
});
it('does not render filtered search', () => {
expect(findFilteredSearch().exists()).toBe(false);
});

View File

@ -1,19 +1,17 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { getBaseURL } from '~/lib/utils/url_utility';
import { mockTracking } from 'helpers/tracking_helper';
import {
EXPERIMENT_NAME,
CF_BASE_URL,
TEMPLATES_BASE_URL,
EASY_BUTTONS,
} from '~/vue_shared/components/runner_aws_deployments/constants';
import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
jest.mock('~/experimentation/experiment_tracking');
describe('RunnerAwsDeploymentsModal', () => {
let wrapper;
let trackingSpy;
const findEasyButtons = () => wrapper.findAllComponents(GlLink);
@ -65,12 +63,14 @@ describe('RunnerAwsDeploymentsModal', () => {
});
it('should track an event when clicked', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
findFirstButton().vm.$emit('click');
expect(ExperimentTracking).toHaveBeenCalledWith(EXPERIMENT_NAME);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
`template_clicked_${EASY_BUTTONS[0].stackName}`,
);
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
label: EASY_BUTTONS[0].stackName,
});
});
});
});

View File

@ -103,6 +103,7 @@ RSpec.describe Users::CalloutsHelper do
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
allow(helper.controller).to receive(:controller_path).and_return("admin/users")
end
it { is_expected.to be false }
@ -114,6 +115,7 @@ RSpec.describe Users::CalloutsHelper do
allow(helper).to receive(:current_user).and_return(user)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
allow(helper.controller).to receive(:controller_path).and_return("admin/users")
end
it { is_expected.to be false }
@ -125,6 +127,7 @@ RSpec.describe Users::CalloutsHelper do
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: false)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
allow(helper.controller).to receive(:controller_path).and_return("admin/users")
end
it { is_expected.to be false }
@ -136,17 +139,31 @@ RSpec.describe Users::CalloutsHelper do
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { true }
allow(helper.controller).to receive(:controller_path).and_return("admin/users")
end
it { is_expected.to be false }
end
context 'when not gitlab.com, `current_user` is an admin, signup is enabled, and user has not dismissed callout' do
context 'when controller path is not allowed' do
before do
allow(::Gitlab).to receive(:com?).and_return(false)
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
allow(helper.controller).to receive(:controller_path).and_return("projects/issues")
end
it { is_expected.to be false }
end
context 'when not gitlab.com, `current_user` is an admin, signup is enabled, user has not dismissed callout, and controller path is allowed' do
before do
allow(::Gitlab).to receive(:com?).and_return(false)
allow(helper).to receive(:current_user).and_return(admin)
stub_application_setting(signup_enabled: true)
allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
allow(helper.controller).to receive(:controller_path).and_return("admin/users")
end
it { is_expected.to be true }

View File

@ -21,8 +21,9 @@ RSpec.describe 'admin/application_settings/repository.html.haml' do
it 'renders the correct setting section content' do
render
expect(rendered).to have_content("Default initial branch name")
expect(rendered).to have_content("The default name for the initial branch of new repositories created in the instance.")
expect(rendered).to have_content("Initial default branch name")
expect(rendered).to have_content("Set the initial name and protections for the default branch of new repositories created in the instance.")
expect(rendered).to have_content("Initial default branch protection")
end
end
end

View File

@ -167,3 +167,16 @@ func Retry(t testing.TB, timeout time.Duration, fn func() error) {
}
t.Fatalf("test timeout after %v; last error: %v", timeout, err)
}
func SetupStaticFileHelper(t *testing.T, fpath, content, directory string) string {
cwd, err := os.Getwd()
require.NoError(t, err, "get working directory")
absDocumentRoot := path.Join(cwd, directory)
require.NoError(t, os.MkdirAll(path.Join(absDocumentRoot, path.Dir(fpath)), 0755), "create document root")
staticFile := path.Join(absDocumentRoot, fpath)
require.NoError(t, ioutil.WriteFile(staticFile, []byte(content), 0666), "write file content")
return absDocumentRoot
}

View File

@ -0,0 +1 @@
testdata/public

View File

@ -385,11 +385,10 @@ func configureRoutes(u *upstream) {
u.route("", "^/oauth/geo/(auth|callback|logout)$", defaultUpstream),
// Admin Area > Geo routes
u.route("", "^/admin/geo$", defaultUpstream),
u.route("", "^/admin/geo/", defaultUpstream),
u.route("", "^/admin/geo/replication/projects", defaultUpstream),
u.route("", "^/admin/geo/replication/designs", defaultUpstream),
// Geo API routes
u.route("", "^/api/v4/geo_nodes", defaultUpstream),
u.route("", "^/api/v4/geo_replication", defaultUpstream),
u.route("", "^/api/v4/geo/proxy_git_ssh", defaultUpstream),
u.route("", "^/api/v4/geo/graphql", defaultUpstream),
@ -397,6 +396,16 @@ func configureRoutes(u *upstream) {
// Internal API routes
u.route("", "^/api/v4/internal", defaultUpstream),
u.route(
"", `^/assets/`,
static.ServeExisting(
u.URLPrefix,
staticpages.CacheExpireMax,
assetsNotFoundHandler,
),
withoutTracing(), // Tracing on assets is very noisy
),
// Don't define a catch-all route. If a route does not match, then we know
// the request should be proxied.
}

View File

@ -2,8 +2,26 @@ package upstream
import (
"testing"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
)
func TestAdminGeoPathsWithGeoProxy(t *testing.T) {
testCases := []testCase{
{"Regular admin/geo", "/admin/geo", "Geo primary received request to path /admin/geo"},
{"Specific object replication", "/admin/geo/replication/object_type", "Geo primary received request to path /admin/geo/replication/object_type"},
{"Specific object replication per-site", "/admin/geo/sites/2/replication/object_type", "Geo primary received request to path /admin/geo/sites/2/replication/object_type"},
{"Projects replication per-site", "/admin/geo/sites/2/replication/projects", "Geo primary received request to path /admin/geo/sites/2/replication/projects"},
{"Designs replication per-site", "/admin/geo/sites/2/replication/designs", "Geo primary received request to path /admin/geo/sites/2/replication/designs"},
{"Projects replication", "/admin/geo/replication/projects", "Local Rails server received request to path /admin/geo/replication/projects"},
{"Projects replication subpaths", "/admin/geo/replication/projects/2", "Local Rails server received request to path /admin/geo/replication/projects/2"},
{"Designs replication", "/admin/geo/replication/designs", "Local Rails server received request to path /admin/geo/replication/designs"},
{"Designs replication subpaths", "/admin/geo/replication/designs/3", "Local Rails server received request to path /admin/geo/replication/designs/3"},
}
runTestCasesWithGeoProxyEnabled(t, testCases)
}
func TestProjectNotExistingGitHttpPullWithGeoProxy(t *testing.T) {
testCases := []testCase{
{"secondary info/refs", "/group/project.git/info/refs", "Local Rails server received request to path /group/project.git/info/refs"},
@ -45,3 +63,15 @@ func TestProjectNotExistingGitSSHPushWithGeoProxy(t *testing.T) {
runTestCasesWithGeoProxyEnabled(t, testCases)
}
func TestAssetsServedLocallyWithGeoProxy(t *testing.T) {
path := "/assets/static.txt"
content := "local geo asset"
testhelper.SetupStaticFileHelper(t, path, content, testDocumentRoot)
testCases := []testCase{
{"assets path", "/assets/static.txt", "local geo asset"},
}
runTestCasesWithGeoProxyEnabled(t, testCases)
}

View File

@ -138,7 +138,7 @@ func TestDeniedXSendfileDownload(t *testing.T) {
func TestAllowedStaticFile(t *testing.T) {
content := "PUBLIC"
require.NoError(t, setupStaticFile("static file.txt", content))
setupStaticFile(t, "static file.txt", content)
proxied := false
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) {
@ -164,7 +164,7 @@ func TestAllowedStaticFile(t *testing.T) {
func TestStaticFileRelativeURL(t *testing.T) {
content := "PUBLIC"
require.NoError(t, setupStaticFile("static.txt", content), "create public/static.txt")
setupStaticFile(t, "static.txt", content)
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), http.HandlerFunc(http.NotFound))
defer ts.Close()
@ -182,7 +182,7 @@ func TestStaticFileRelativeURL(t *testing.T) {
func TestAllowedPublicUploadsFile(t *testing.T) {
content := "PRIVATE but allowed"
require.NoError(t, setupStaticFile("uploads/static file.txt", content), "create public/uploads/static file.txt")
setupStaticFile(t, "uploads/static file.txt", content)
proxied := false
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) {
@ -208,7 +208,7 @@ func TestAllowedPublicUploadsFile(t *testing.T) {
func TestDeniedPublicUploadsFile(t *testing.T) {
content := "PRIVATE"
require.NoError(t, setupStaticFile("uploads/static.txt", content), "create public/uploads/static.txt")
setupStaticFile(t, "uploads/static.txt", content)
proxied := false
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, _ *http.Request) {
@ -241,7 +241,7 @@ This is a static error page for code 499
</body>
</html>
`
require.NoError(t, setupStaticFile("499.html", errorPageBody))
setupStaticFile(t, "499.html", errorPageBody)
ts := testhelper.TestServerWithHandler(nil, func(w http.ResponseWriter, _ *http.Request) {
upstreamError := "499"
// This is the point of the test: the size of the upstream response body
@ -266,7 +266,7 @@ This is a static error page for code 499
func TestGzipAssets(t *testing.T) {
path := "/assets/static.txt"
content := "asset"
require.NoError(t, setupStaticFile(path, content))
setupStaticFile(t, path, content)
buf := &bytes.Buffer{}
gzipWriter := gzip.NewWriter(buf)
@ -274,7 +274,7 @@ func TestGzipAssets(t *testing.T) {
require.NoError(t, err)
require.NoError(t, gzipWriter.Close())
contentGzip := buf.String()
require.NoError(t, setupStaticFile(path+".gz", contentGzip))
setupStaticFile(t, path+".gz", contentGzip)
proxied := false
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) {
@ -319,7 +319,7 @@ func TestGzipAssets(t *testing.T) {
func TestAltDocumentAssets(t *testing.T) {
path := "/assets/static.txt"
content := "asset"
require.NoError(t, setupAltStaticFile(path, content))
setupAltStaticFile(t, path, content)
buf := &bytes.Buffer{}
gzipWriter := gzip.NewWriter(buf)
@ -327,7 +327,7 @@ func TestAltDocumentAssets(t *testing.T) {
require.NoError(t, err)
require.NoError(t, gzipWriter.Close())
contentGzip := buf.String()
require.NoError(t, setupAltStaticFile(path+".gz", contentGzip))
setupAltStaticFile(t, path+".gz", contentGzip)
proxied := false
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) {
@ -712,25 +712,12 @@ func TestRejectUnknownMethod(t *testing.T) {
require.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
}
func setupStaticFile(fpath, content string) error {
return setupStaticFileHelper(fpath, content, testDocumentRoot)
func setupStaticFile(t *testing.T, fpath, content string) {
absDocumentRoot = testhelper.SetupStaticFileHelper(t, fpath, content, testDocumentRoot)
}
func setupAltStaticFile(fpath, content string) error {
return setupStaticFileHelper(fpath, content, testAltDocumentRoot)
}
func setupStaticFileHelper(fpath, content, directory string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
absDocumentRoot = path.Join(cwd, directory)
if err := os.MkdirAll(path.Join(absDocumentRoot, path.Dir(fpath)), 0755); err != nil {
return err
}
staticFile := path.Join(absDocumentRoot, fpath)
return ioutil.WriteFile(staticFile, []byte(content), 0666)
func setupAltStaticFile(t *testing.T, fpath, content string) {
absDocumentRoot = testhelper.SetupStaticFileHelper(t, fpath, content, testAltDocumentRoot)
}
func prepareDownloadDir(t *testing.T) {
@ -896,7 +883,7 @@ This is a static error page for code 503
</body>
</html>
`
require.NoError(t, setupStaticFile("503.html", errorPageBody))
setupStaticFile(t, "503.html", errorPageBody)
ts := testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Gitlab-Custom-Error", "1")