Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
defde9698e
commit
d9b0b3243e
20 changed files with 281 additions and 190 deletions
|
@ -1 +1 @@
|
||||||
e3bedb3507c01fbe8395dd76589e095d7da14e66
|
1b06a8764c22fc3960271b02470317077f284508
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
13.3.0
|
13.4.0
|
||||||
|
|
|
@ -743,14 +743,14 @@ export function moveToNeighboringCommit({ dispatch, state }, { direction }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setCurrentDiffFileIdFromNote = ({ commit, rootGetters }, noteId) => {
|
export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, noteId) => {
|
||||||
const note = rootGetters.notesById[noteId];
|
const note = rootGetters.notesById[noteId];
|
||||||
|
|
||||||
if (!note) return;
|
if (!note) return;
|
||||||
|
|
||||||
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
|
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
|
||||||
|
|
||||||
if (fileHash) {
|
if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) {
|
||||||
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
|
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,10 +15,10 @@ module AlertManagement
|
||||||
return error_no_permissions unless allowed?
|
return error_no_permissions unless allowed?
|
||||||
return error_issue_already_exists if alert.issue
|
return error_issue_already_exists if alert.issue
|
||||||
|
|
||||||
result = create_issue
|
result = create_incident
|
||||||
issue = result.payload[:issue]
|
return result unless result.success?
|
||||||
|
|
||||||
return error(result.message, issue) if result.error?
|
issue = result.payload[:issue]
|
||||||
return error(object_errors(alert), issue) unless associate_alert_with_issue(issue)
|
return error(object_errors(alert), issue) unless associate_alert_with_issue(issue)
|
||||||
|
|
||||||
SystemNoteService.new_alert_issue(alert, issue, user)
|
SystemNoteService.new_alert_issue(alert, issue, user)
|
||||||
|
@ -36,30 +36,19 @@ module AlertManagement
|
||||||
user.can?(:create_issue, project)
|
user.can?(:create_issue, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_issue
|
def create_incident
|
||||||
label_result = find_or_create_incident_label
|
::IncidentManagement::Incidents::CreateService.new(
|
||||||
|
|
||||||
issue = Issues::CreateService.new(
|
|
||||||
project,
|
project,
|
||||||
user,
|
user,
|
||||||
title: alert_presenter.title,
|
title: alert_presenter.title,
|
||||||
description: alert_presenter.issue_description,
|
description: alert_presenter.issue_description
|
||||||
label_ids: [label_result.payload[:label].id]
|
|
||||||
).execute
|
).execute
|
||||||
|
|
||||||
return error(object_errors(issue), issue) unless issue.valid?
|
|
||||||
|
|
||||||
success(issue)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def associate_alert_with_issue(issue)
|
def associate_alert_with_issue(issue)
|
||||||
alert.update(issue_id: issue.id)
|
alert.update(issue_id: issue.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def success(issue)
|
|
||||||
ServiceResponse.success(payload: { issue: issue })
|
|
||||||
end
|
|
||||||
|
|
||||||
def error(message, issue = nil)
|
def error(message, issue = nil)
|
||||||
ServiceResponse.error(payload: { issue: issue }, message: message)
|
ServiceResponse.error(payload: { issue: issue }, message: message)
|
||||||
end
|
end
|
||||||
|
@ -78,10 +67,6 @@ module AlertManagement
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_incident_label
|
|
||||||
IncidentManagement::CreateIncidentLabelService.new(project, user).execute
|
|
||||||
end
|
|
||||||
|
|
||||||
def object_errors(object)
|
def object_errors(object)
|
||||||
object.errors.full_messages.to_sentence
|
object.errors.full_messages.to_sentence
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,32 +3,30 @@
|
||||||
module IncidentManagement
|
module IncidentManagement
|
||||||
class CreateIssueService < BaseService
|
class CreateIssueService < BaseService
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
include IncidentManagement::Settings
|
||||||
|
|
||||||
def initialize(project, params)
|
def initialize(project, params)
|
||||||
super(project, User.alert_bot, params)
|
super(project, User.alert_bot, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
return error_with('setting disabled') unless incident_management_setting.create_issue?
|
return error('setting disabled') unless incident_management_setting.create_issue?
|
||||||
return error_with('invalid alert') unless alert.valid?
|
return error('invalid alert') unless alert.valid?
|
||||||
|
|
||||||
issue = create_issue
|
result = create_incident
|
||||||
return error_with(issue_errors(issue)) unless issue.valid?
|
return error(result.message, result.payload[:issue]) unless result.success?
|
||||||
|
|
||||||
success(issue: issue)
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_issue
|
def create_incident
|
||||||
label_result = find_or_create_incident_label
|
::IncidentManagement::Incidents::CreateService.new(
|
||||||
|
|
||||||
Issues::CreateService.new(
|
|
||||||
project,
|
project,
|
||||||
current_user,
|
current_user,
|
||||||
title: issue_title,
|
title: issue_title,
|
||||||
description: issue_description,
|
description: issue_description
|
||||||
label_ids: [label_result.payload[:label].id]
|
|
||||||
).execute
|
).execute
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -46,10 +44,6 @@ module IncidentManagement
|
||||||
].compact.join(horizontal_line)
|
].compact.join(horizontal_line)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_incident_label
|
|
||||||
IncidentManagement::CreateIncidentLabelService.new(project, current_user).execute
|
|
||||||
end
|
|
||||||
|
|
||||||
def alert_summary
|
def alert_summary
|
||||||
alert.issue_summary_markdown
|
alert.issue_summary_markdown
|
||||||
end
|
end
|
||||||
|
@ -68,21 +62,10 @@ module IncidentManagement
|
||||||
incident_management_setting.issue_template_content
|
incident_management_setting.issue_template_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def incident_management_setting
|
def error(message, issue = nil)
|
||||||
strong_memoize(:incident_management_setting) do
|
|
||||||
project.incident_management_setting ||
|
|
||||||
project.build_incident_management_setting
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def issue_errors(issue)
|
|
||||||
issue.errors.full_messages.to_sentence
|
|
||||||
end
|
|
||||||
|
|
||||||
def error_with(message)
|
|
||||||
log_error(%{Cannot create incident issue for "#{project.full_name}": #{message}})
|
log_error(%{Cannot create incident issue for "#{project.full_name}": #{message}})
|
||||||
|
|
||||||
error(message)
|
ServiceResponse.error(payload: { issue: issue }, message: message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
47
app/services/incident_management/incidents/create_service.rb
Normal file
47
app/services/incident_management/incidents/create_service.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module IncidentManagement
|
||||||
|
module Incidents
|
||||||
|
class CreateService < BaseService
|
||||||
|
def initialize(project, current_user, title:, description:)
|
||||||
|
super(project, current_user)
|
||||||
|
|
||||||
|
@title = title
|
||||||
|
@description = description
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
issue = Issues::CreateService.new(
|
||||||
|
project,
|
||||||
|
current_user,
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
label_ids: [find_or_create_incident_label.id]
|
||||||
|
).execute
|
||||||
|
|
||||||
|
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
|
||||||
|
|
||||||
|
success(issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :title, :description
|
||||||
|
|
||||||
|
def find_or_create_incident_label
|
||||||
|
IncidentManagement::CreateIncidentLabelService
|
||||||
|
.new(project, current_user)
|
||||||
|
.execute
|
||||||
|
.payload[:label]
|
||||||
|
end
|
||||||
|
|
||||||
|
def success(issue)
|
||||||
|
ServiceResponse.success(payload: { issue: issue })
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(message, issue = nil)
|
||||||
|
ServiceResponse.error(payload: { issue: issue }, message: message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,25 +12,19 @@ module IncidentManagement
|
||||||
def execute
|
def execute
|
||||||
return forbidden unless webhook_available?
|
return forbidden unless webhook_available?
|
||||||
|
|
||||||
issue = create_issue
|
create_incident
|
||||||
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
|
|
||||||
|
|
||||||
success(issue)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
alias_method :incident_payload, :params
|
alias_method :incident_payload, :params
|
||||||
|
|
||||||
def create_issue
|
def create_incident
|
||||||
label_result = find_or_create_incident_label
|
::IncidentManagement::Incidents::CreateService.new(
|
||||||
|
|
||||||
Issues::CreateService.new(
|
|
||||||
project,
|
project,
|
||||||
current_user,
|
current_user,
|
||||||
title: issue_title,
|
title: issue_title,
|
||||||
description: issue_description,
|
description: issue_description
|
||||||
label_ids: [label_result.payload[:label].id]
|
|
||||||
).execute
|
).execute
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,10 +36,6 @@ module IncidentManagement
|
||||||
ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
|
ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_incident_label
|
|
||||||
::IncidentManagement::CreateIncidentLabelService.new(project, current_user).execute
|
|
||||||
end
|
|
||||||
|
|
||||||
def issue_title
|
def issue_title
|
||||||
incident_payload['title']
|
incident_payload['title']
|
||||||
end
|
end
|
||||||
|
@ -53,14 +43,6 @@ module IncidentManagement
|
||||||
def issue_description
|
def issue_description
|
||||||
Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription.new(incident_payload).to_s
|
Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription.new(incident_payload).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def success(issue)
|
|
||||||
ServiceResponse.success(payload: { issue: issue })
|
|
||||||
end
|
|
||||||
|
|
||||||
def error(message, issue = nil)
|
|
||||||
ServiceResponse.error(payload: { issue: issue }, message: message)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,9 +16,10 @@ module IncidentManagement
|
||||||
alert = find_alert(alert_id)
|
alert = find_alert(alert_id)
|
||||||
return unless alert
|
return unless alert
|
||||||
|
|
||||||
new_issue = create_issue_for(alert)
|
result = create_issue_for(alert)
|
||||||
return unless new_issue&.persisted?
|
return unless result.success?
|
||||||
|
|
||||||
|
new_issue = result.payload[:issue]
|
||||||
link_issue_with_alert(alert, new_issue.id)
|
link_issue_with_alert(alert, new_issue.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,7 +37,6 @@ module IncidentManagement
|
||||||
IncidentManagement::CreateIssueService
|
IncidentManagement::CreateIssueService
|
||||||
.new(alert.project, parsed_payload(alert))
|
.new(alert.project, parsed_payload(alert))
|
||||||
.execute
|
.execute
|
||||||
.dig(:issue)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_issue_with_alert(alert, issue_id)
|
def link_issue_with_alert(alert, issue_id)
|
||||||
|
|
5
changelogs/unreleased/sh-upgrade-gitlab-shell-13-4-0.yml
Normal file
5
changelogs/unreleased/sh-upgrade-gitlab-shell-13-4-0.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Update gitlab-shell to v13.4.0
|
||||||
|
merge_request: 37677
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -1,5 +1,13 @@
|
||||||
|
---
|
||||||
|
stage: Plan
|
||||||
|
group: Certify
|
||||||
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||||
|
---
|
||||||
|
|
||||||
# Reply by email
|
# Reply by email
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/1173) in GitLab 8.0.
|
||||||
|
|
||||||
GitLab can be set up to allow users to comment on issues and merge requests by
|
GitLab can be set up to allow users to comment on issues and merge requests by
|
||||||
replying to notification emails.
|
replying to notification emails.
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,17 @@ description: 'Learn how to contribute to GitLab.'
|
||||||
|
|
||||||
# Contributor and Development Docs
|
# Contributor and Development Docs
|
||||||
|
|
||||||
|
Learn the processes and technical information needed for contributing to GitLab.
|
||||||
|
|
||||||
|
This content is intended for members of the GitLab Team as well as community contributors.
|
||||||
|
Content specific to the GitLab Team should instead be included in the [Handbook](https://about.gitlab.com/handbook/).
|
||||||
|
|
||||||
|
For information on using GitLab to work on your own software projects, see the [GitLab user documentation](../user/index.md).
|
||||||
|
|
||||||
|
For information on working with GitLab's API, see the [API documentation](../api/README.md).
|
||||||
|
|
||||||
|
For information on how to install, configure, update, and upgrade your own GitLab instance, see the [administration documentation](../administration/index.md).
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Set up GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
|
- Set up GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Documentation deployment process
|
||||||
|
|
||||||
|
The [`dockerfiles` directory](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/)
|
||||||
|
contains all needed Dockerfiles to build and deploy <https://docs.gitlab.com>. It
|
||||||
|
is heavily inspired by Docker's
|
||||||
|
[Dockerfile](https://github.com/docker/docker.github.io/blob/06ed03db13895bfe867761b6fc2ad40acf6026dd/Dockerfile).
|
||||||
|
|
||||||
|
The following Dockerfiles are used.
|
||||||
|
|
||||||
|
| Dockerfile | Docker image | Description |
|
||||||
|
| ---------- | ------------ | ----------- |
|
||||||
|
| [`Dockerfile.bootstrap`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.bootstrap) | `gitlab-docs:bootstrap` | Contains all the dependencies that are needed to build the website. If the gems are updated and `Gemfile{,.lock}` changes, the image must be rebuilt. |
|
||||||
|
| [`Dockerfile.builder.onbuild`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.builder.onbuild) | `gitlab-docs:builder-onbuild` | Base image to build the docs website. It uses `ONBUILD` to perform all steps and depends on `gitlab-docs:bootstrap`. |
|
||||||
|
| [`Dockerfile.nginx.onbuild`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.nginx.onbuild) | `gitlab-docs:nginx-onbuild` | Base image to use for building documentation archives. It uses `ONBUILD` to perform all required steps to copy the archive, and relies upon its parent `Dockerfile.builder.onbuild` that is invoked when building single documentation archives (see the `Dockerfile` of each branch. |
|
||||||
|
| [`Dockerfile.archives`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.archives) | `gitlab-docs:archives` | Contains all the versions of the website in one archive. It copies all generated HTML files from every version in one location. |
|
||||||
|
|
||||||
|
## How to build the images
|
||||||
|
|
||||||
|
Although build images are built automatically via GitLab CI/CD, you can build
|
||||||
|
and tag all tooling images locally:
|
||||||
|
|
||||||
|
1. Make sure you have [Docker installed](https://docs.docker.com/install/).
|
||||||
|
1. Make sure you're in the `dockerfiles/` directory of the `gitlab-docs` repository.
|
||||||
|
1. Build the images:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:bootstrap -f Dockerfile.bootstrap ../
|
||||||
|
docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:builder-onbuild -f Dockerfile.builder.onbuild ../
|
||||||
|
docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:nginx-onbuild -f Dockerfile.nginx.onbuild ../
|
||||||
|
```
|
||||||
|
|
||||||
|
For each image, there's a manual job under the `images` stage in
|
||||||
|
[`.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/.gitlab-ci.yml) which can be invoked at will.
|
||||||
|
|
||||||
|
## Update an old Docker image with new upstream docs content
|
||||||
|
|
||||||
|
If there are any changes to any of the stable branches of the products that are
|
||||||
|
not included in the single Docker image, just rerun the pipeline (`https://gitlab.com/gitlab-org/gitlab-docs/pipelines/new`)
|
||||||
|
for the version in question.
|
||||||
|
|
||||||
|
## Porting new website changes to old versions
|
||||||
|
|
||||||
|
CAUTION: **Warning:**
|
||||||
|
Porting changes to older branches can have unintended effects as we're constantly
|
||||||
|
changing the backend of the website. Use only when you know what you're doing
|
||||||
|
and make sure to test locally.
|
||||||
|
|
||||||
|
The website will keep changing and being improved. In order to consolidate
|
||||||
|
those changes to the stable branches, we'd need to pick certain changes
|
||||||
|
from time to time.
|
||||||
|
|
||||||
|
If this is not possible or there are many changes, merge master into them:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git branch 12.0
|
||||||
|
git fetch origin master
|
||||||
|
git merge origin/master
|
||||||
|
```
|
|
@ -138,6 +138,8 @@ If you need to build and deploy the site immediately (must have maintainer level
|
||||||
1. In [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs), go to **{rocket}** **CI / CD > Schedules**.
|
1. In [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs), go to **{rocket}** **CI / CD > Schedules**.
|
||||||
1. For the `Build docs.gitlab.com every 4 hours` scheduled pipeline, click the **play** (**{play}**) button.
|
1. For the `Build docs.gitlab.com every 4 hours` scheduled pipeline, click the **play** (**{play}**) button.
|
||||||
|
|
||||||
|
Read more about the [deployment process](deployment_process.md).
|
||||||
|
|
||||||
## Using YAML data files
|
## Using YAML data files
|
||||||
|
|
||||||
The easiest way to achieve something similar to
|
The easiest way to achieve something similar to
|
||||||
|
|
|
@ -1,51 +1,21 @@
|
||||||
# GitLab Docs monthly release process
|
# GitLab Docs monthly release process
|
||||||
|
|
||||||
The [`dockerfiles` directory](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/)
|
|
||||||
contains all needed Dockerfiles to build and deploy the versioned website. It
|
|
||||||
is heavily inspired by Docker's
|
|
||||||
[Dockerfile](https://github.com/docker/docker.github.io/blob/06ed03db13895bfe867761b6fc2ad40acf6026dd/Dockerfile).
|
|
||||||
|
|
||||||
The following Dockerfiles are used.
|
|
||||||
|
|
||||||
| Dockerfile | Docker image | Description |
|
|
||||||
| ---------- | ------------ | ----------- |
|
|
||||||
| [`Dockerfile.bootstrap`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.bootstrap) | `gitlab-docs:bootstrap` | Contains all the dependencies that are needed to build the website. If the gems are updated and `Gemfile{,.lock}` changes, the image must be rebuilt. |
|
|
||||||
| [`Dockerfile.builder.onbuild`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.builder.onbuild) | `gitlab-docs:builder-onbuild` | Base image to build the docs website. It uses `ONBUILD` to perform all steps and depends on `gitlab-docs:bootstrap`. |
|
|
||||||
| [`Dockerfile.nginx.onbuild`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.nginx.onbuild) | `gitlab-docs:nginx-onbuild` | Base image to use for building documentation archives. It uses `ONBUILD` to perform all required steps to copy the archive, and relies upon its parent `Dockerfile.builder.onbuild` that is invoked when building single documentation archives (see the `Dockerfile` of each branch. |
|
|
||||||
| [`Dockerfile.archives`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.archives) | `gitlab-docs:archives` | Contains all the versions of the website in one archive. It copies all generated HTML files from every version in one location. |
|
|
||||||
|
|
||||||
## How to build the images
|
|
||||||
|
|
||||||
Although build images are built automatically via GitLab CI/CD, you can build
|
|
||||||
and tag all tooling images locally:
|
|
||||||
|
|
||||||
1. Make sure you have [Docker installed](https://docs.docker.com/install/).
|
|
||||||
1. Make sure you're in the `dockerfiles/` directory of the `gitlab-docs` repository.
|
|
||||||
1. Build the images:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:bootstrap -f Dockerfile.bootstrap ../
|
|
||||||
docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:builder-onbuild -f Dockerfile.builder.onbuild ../
|
|
||||||
docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:nginx-onbuild -f Dockerfile.nginx.onbuild ../
|
|
||||||
```
|
|
||||||
|
|
||||||
For each image, there's a manual job under the `images` stage in
|
|
||||||
[`.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/.gitlab-ci.yml) which can be invoked at will.
|
|
||||||
|
|
||||||
## Monthly release process
|
|
||||||
|
|
||||||
When a new GitLab version is released on the 22nd, we need to create the respective
|
When a new GitLab version is released on the 22nd, we need to create the respective
|
||||||
single Docker image, and update some files so that the dropdown works correctly.
|
single Docker image, and update some files so that the dropdown works correctly.
|
||||||
|
|
||||||
### 1. Add the chart version
|
## 1. Add the chart version
|
||||||
|
|
||||||
Since the charts use a different version number than all the other GitLab
|
Since the charts use a different version number than all the other GitLab
|
||||||
products, we need to add a
|
products, we need to add a
|
||||||
[version mapping](https://docs.gitlab.com/charts/installation/version_mappings.html):
|
[version mapping](https://docs.gitlab.com/charts/installation/version_mappings.html):
|
||||||
|
|
||||||
1. Check that there is a [stable branch created](https://gitlab.com/gitlab-org/charts/gitlab/-/branches)
|
NOTE: **Note:**
|
||||||
for the new chart version. If you're unsure or can't find it, drop a line in
|
The charts stable branch is not created automatically like the other products.
|
||||||
the `#g_delivery` channel.
|
There's an [issue to track this](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/1442).
|
||||||
|
It is usually created on the 21st or the 22nd.
|
||||||
|
|
||||||
|
To add a new charts version:
|
||||||
|
|
||||||
1. Make sure you're in the root path of the `gitlab-docs` repository.
|
1. Make sure you're in the root path of the `gitlab-docs` repository.
|
||||||
1. Open `content/_data/chart_versions.yaml` and add the new stable branch version using the
|
1. Open `content/_data/chart_versions.yaml` and add the new stable branch version using the
|
||||||
version mapping. Note that only the `major.minor` version is needed.
|
version mapping. Note that only the `major.minor` version is needed.
|
||||||
|
@ -56,7 +26,7 @@ It can be handy to create the future mappings since they are pretty much known.
|
||||||
In that case, when a new GitLab version is released, you don't have to repeat
|
In that case, when a new GitLab version is released, you don't have to repeat
|
||||||
this first step.
|
this first step.
|
||||||
|
|
||||||
### 2. Create an image for a single version
|
## 2. Create an image for a single version
|
||||||
|
|
||||||
The single docs version must be created before the release merge request, but
|
The single docs version must be created before the release merge request, but
|
||||||
this needs to happen when the stable branches for all products have been created.
|
this needs to happen when the stable branches for all products have been created.
|
||||||
|
@ -89,7 +59,7 @@ docker run -it --rm -p 4000:4000 docs:12.0
|
||||||
|
|
||||||
Visit `http://localhost:4000/12.0/` to see if everything works correctly.
|
Visit `http://localhost:4000/12.0/` to see if everything works correctly.
|
||||||
|
|
||||||
### 3. Create the release merge request
|
## 3. Create the release merge request
|
||||||
|
|
||||||
NOTE: **Note:**
|
NOTE: **Note:**
|
||||||
To be [automated](https://gitlab.com/gitlab-org/gitlab-docs/-/issues/750).
|
To be [automated](https://gitlab.com/gitlab-org/gitlab-docs/-/issues/750).
|
||||||
|
@ -135,7 +105,7 @@ version and rotates the old one:
|
||||||
git push origin release-12-0
|
git push origin release-12-0
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Update the dropdown for all online versions
|
## 4. Update the dropdown for all online versions
|
||||||
|
|
||||||
The versions dropdown is in a way "hardcoded". When the site is built, it looks
|
The versions dropdown is in a way "hardcoded". When the site is built, it looks
|
||||||
at the contents of `content/_data/versions.yaml` and based on that, the dropdown
|
at the contents of `content/_data/versions.yaml` and based on that, the dropdown
|
||||||
|
@ -144,14 +114,18 @@ dropdown will list one or more releases behind. Remember that the new changes of
|
||||||
the dropdown are included in the unmerged `release-X-Y` branch.
|
the dropdown are included in the unmerged `release-X-Y` branch.
|
||||||
|
|
||||||
The content of `content/_data/versions.yaml` needs to change for all online
|
The content of `content/_data/versions.yaml` needs to change for all online
|
||||||
versions:
|
versions (stable branches `X.Y` of the `gitlab-docs` project):
|
||||||
|
|
||||||
1. Run the Rake task that will create all the respective merge requests needed to
|
1. Run the Rake task that will create all the respective merge requests needed to
|
||||||
update the dropdowns and will be set to automatically be merged when their
|
update the dropdowns and will be set to automatically be merged when their
|
||||||
pipelines succeed. The `release-X-Y` branch needs to be present locally,
|
pipelines succeed:
|
||||||
and you need to have switched to it, otherwise the Rake task will fail:
|
|
||||||
|
NOTE: **Note:**
|
||||||
|
The `release-X-Y` branch needs to be present locally,
|
||||||
|
and you need to have switched to it, otherwise the Rake task will fail.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
git checkout release-X-Y
|
||||||
./bin/rake release:dropdowns
|
./bin/rake release:dropdowns
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -162,46 +136,22 @@ versions:
|
||||||
TIP: **Tip:**
|
TIP: **Tip:**
|
||||||
In case a pipeline fails, see [troubleshooting](#troubleshooting).
|
In case a pipeline fails, see [troubleshooting](#troubleshooting).
|
||||||
|
|
||||||
### 5. Merge the release merge request
|
## 5. Merge the release merge request
|
||||||
|
|
||||||
The dropdown merge requests should have now been merged into their respective
|
The dropdown merge requests should have now been merged into their respective
|
||||||
version (stable branch), which will trigger another pipeline. At this point,
|
version (stable `X.Y` branch), which will trigger another pipeline. At this point,
|
||||||
you need to only babysit the pipelines and make sure they don't fail:
|
you need to only babysit the pipelines and make sure they don't fail:
|
||||||
|
|
||||||
1. Check the pipelines page: `https://gitlab.com/gitlab-org/gitlab-docs/pipelines`
|
1. Check the [pipelines page](https://gitlab.com/gitlab-org/gitlab-docs/pipelines)
|
||||||
and make sure all stable branches have green pipelines.
|
and make sure all stable branches have green pipelines.
|
||||||
1. After all the pipelines of the online versions succeed, merge the release merge request.
|
1. After all the pipelines of the online versions succeed, merge the release merge request.
|
||||||
1. Finally, from `https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules` run
|
1. Finally, run the
|
||||||
the `Build docker images weekly` pipeline that will build the `:latest` and `:archives` Docker images.
|
[`Build docker images weekly` pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules)
|
||||||
|
that will build the `:latest` and `:archives` Docker images.
|
||||||
|
|
||||||
Once the scheduled pipeline succeeds, the docs site will be deployed with all
|
Once the scheduled pipeline succeeds, the docs site will be deployed with all
|
||||||
new versions online.
|
new versions online.
|
||||||
|
|
||||||
## Update an old Docker image with new upstream docs content
|
|
||||||
|
|
||||||
If there are any changes to any of the stable branches of the products that are
|
|
||||||
not included in the single Docker image, just rerun the pipeline (`https://gitlab.com/gitlab-org/gitlab-docs/pipelines/new`)
|
|
||||||
for the version in question.
|
|
||||||
|
|
||||||
## Porting new website changes to old versions
|
|
||||||
|
|
||||||
CAUTION: **Warning:**
|
|
||||||
Porting changes to older branches can have unintended effects as we're constantly
|
|
||||||
changing the backend of the website. Use only when you know what you're doing
|
|
||||||
and make sure to test locally.
|
|
||||||
|
|
||||||
The website will keep changing and being improved. In order to consolidate
|
|
||||||
those changes to the stable branches, we'd need to pick certain changes
|
|
||||||
from time to time.
|
|
||||||
|
|
||||||
If this is not possible or there are many changes, merge master into them:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git branch 12.0
|
|
||||||
git fetch origin master
|
|
||||||
git merge origin/master
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
Releasing a new version is a long process that involves many moving parts.
|
Releasing a new version is a long process that involves many moving parts.
|
||||||
|
|
|
@ -1594,24 +1594,39 @@ describe('DiffsStoreActions', () => {
|
||||||
describe('setCurrentDiffFileIdFromNote', () => {
|
describe('setCurrentDiffFileIdFromNote', () => {
|
||||||
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
|
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
|
||||||
const commit = jest.fn();
|
const commit = jest.fn();
|
||||||
|
const state = { diffFiles: [{ file_hash: '123' }] };
|
||||||
const rootGetters = {
|
const rootGetters = {
|
||||||
getDiscussion: () => ({ diff_file: { file_hash: '123' } }),
|
getDiscussion: () => ({ diff_file: { file_hash: '123' } }),
|
||||||
notesById: { '1': { discussion_id: '2' } },
|
notesById: { '1': { discussion_id: '2' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1');
|
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
|
||||||
|
|
||||||
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, '123');
|
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, '123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when discussion has no diff_file', () => {
|
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when discussion has no diff_file', () => {
|
||||||
const commit = jest.fn();
|
const commit = jest.fn();
|
||||||
|
const state = { diffFiles: [{ file_hash: '123' }] };
|
||||||
const rootGetters = {
|
const rootGetters = {
|
||||||
getDiscussion: () => ({ id: '1' }),
|
getDiscussion: () => ({ id: '1' }),
|
||||||
notesById: { '1': { discussion_id: '2' } },
|
notesById: { '1': { discussion_id: '2' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1');
|
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
|
||||||
|
|
||||||
|
expect(commit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when diff file does not exist', () => {
|
||||||
|
const commit = jest.fn();
|
||||||
|
const state = { diffFiles: [{ file_hash: '123' }] };
|
||||||
|
const rootGetters = {
|
||||||
|
getDiscussion: () => ({ diff_file: { file_hash: '124' } }),
|
||||||
|
notesById: { '1': { discussion_id: '2' } },
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
|
||||||
|
|
||||||
expect(commit).not.toHaveBeenCalled();
|
expect(commit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -172,7 +172,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do
|
||||||
let(:filepath) { "group_exports/visibility_levels/#{visibility_level}" }
|
let(:filepath) { "group_exports/visibility_levels/#{visibility_level}" }
|
||||||
|
|
||||||
it "imports all subgroups as #{visibility_level}" do
|
it "imports all subgroups as #{visibility_level}" do
|
||||||
expect(group.children.map(&:visibility_level)).to eq(expected_visibilities)
|
expect(group.children.map(&:visibility_level)).to match_array(expected_visibilities)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -88,7 +88,6 @@ RSpec.describe AlertManagement::CreateAlertIssueService do
|
||||||
|
|
||||||
it_behaves_like 'creating an alert issue'
|
it_behaves_like 'creating an alert issue'
|
||||||
it_behaves_like 'setting an issue attributes'
|
it_behaves_like 'setting an issue attributes'
|
||||||
it_behaves_like 'create alert issue sets issue labels'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the alert is generic' do
|
context 'when the alert is generic' do
|
||||||
|
@ -97,7 +96,6 @@ RSpec.describe AlertManagement::CreateAlertIssueService do
|
||||||
|
|
||||||
it_behaves_like 'creating an alert issue'
|
it_behaves_like 'creating an alert issue'
|
||||||
it_behaves_like 'setting an issue attributes'
|
it_behaves_like 'setting an issue attributes'
|
||||||
it_behaves_like 'create alert issue sets issue labels'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when issue cannot be created' do
|
context 'when issue cannot be created' do
|
||||||
|
|
|
@ -25,10 +25,10 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
create(:project_incident_management_setting, project: project)
|
create(:project_incident_management_setting, project: project)
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { service.execute }
|
subject(:execute) { service.execute }
|
||||||
|
|
||||||
context 'when create_issue enabled' do
|
context 'when create_issue enabled' do
|
||||||
let(:issue) { subject[:issue] }
|
let(:issue) { execute.payload[:issue] }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
setting.update!(create_issue: true)
|
setting.update!(create_issue: true)
|
||||||
|
@ -36,7 +36,7 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
|
|
||||||
context 'without issue_template_content' do
|
context 'without issue_template_content' do
|
||||||
it 'creates an issue with alert summary only' do
|
it 'creates an issue with alert summary only' do
|
||||||
expect(subject).to include(status: :success)
|
expect(execute).to be_success
|
||||||
|
|
||||||
expect(issue.author).to eq(user)
|
expect(issue.author).to eq(user)
|
||||||
expect(issue.title).to eq(alert_title)
|
expect(issue.title).to eq(alert_title)
|
||||||
|
@ -61,7 +61,8 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
.to receive(:log_error)
|
.to receive(:log_error)
|
||||||
.with(error_message(issue_error))
|
.with(error_message(issue_error))
|
||||||
|
|
||||||
expect(subject).to include(status: :error, message: issue_error)
|
expect(execute).to be_error
|
||||||
|
expect(execute.message).to eq(issue_error)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
let(:template_content) { 'some content' }
|
let(:template_content) { 'some content' }
|
||||||
|
|
||||||
it 'creates an issue appending issue template' do
|
it 'creates an issue appending issue template' do
|
||||||
expect(subject).to include(status: :success)
|
expect(execute).to be_success
|
||||||
|
|
||||||
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
|
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
|
||||||
expect(separator_count(issue.description)).to eq(1)
|
expect(separator_count(issue.description)).to eq(1)
|
||||||
|
@ -95,7 +96,7 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates an issue interpreting quick actions' do
|
it 'creates an issue interpreting quick actions' do
|
||||||
expect(subject).to include(status: :success)
|
expect(execute).to be_success
|
||||||
|
|
||||||
expect(issue.description).to include(plain_text)
|
expect(issue.description).to include(plain_text)
|
||||||
expect(issue.due_date).to be_present
|
expect(issue.due_date).to be_present
|
||||||
|
@ -128,7 +129,7 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'includes both templates' do
|
it 'includes both templates' do
|
||||||
expect(subject).to include(status: :success)
|
expect(execute).to be_success
|
||||||
|
|
||||||
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
|
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
|
||||||
expect(issue.description).to include(template_content)
|
expect(issue.description).to include(template_content)
|
||||||
|
@ -162,7 +163,7 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
it 'creates an issue' do
|
it 'creates an issue' do
|
||||||
query_title = "#{gitlab_alert.title} #{gitlab_alert.computed_operator} #{gitlab_alert.threshold}"
|
query_title = "#{gitlab_alert.title} #{gitlab_alert.computed_operator} #{gitlab_alert.threshold}"
|
||||||
|
|
||||||
expect(subject).to include(status: :success)
|
expect(execute).to be_success
|
||||||
|
|
||||||
expect(issue.author).to eq(user)
|
expect(issue.author).to eq(user)
|
||||||
expect(issue.title).to eq(alert_presenter.full_title)
|
expect(issue.title).to eq(alert_presenter.full_title)
|
||||||
|
@ -181,7 +182,8 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
.to receive(:log_error)
|
.to receive(:log_error)
|
||||||
.with(error_message('invalid alert'))
|
.with(error_message('invalid alert'))
|
||||||
|
|
||||||
expect(subject).to eq(status: :error, message: 'invalid alert')
|
expect(execute).to be_error
|
||||||
|
expect(execute.message).to eq('invalid alert')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -197,10 +199,6 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
it_behaves_like 'invalid alert'
|
it_behaves_like 'invalid alert'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "label `incident`" do
|
|
||||||
it_behaves_like 'create alert issue sets issue labels'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when create_issue disabled' do
|
context 'when create_issue disabled' do
|
||||||
|
@ -213,7 +211,8 @@ RSpec.describe IncidentManagement::CreateIssueService do
|
||||||
.to receive(:log_error)
|
.to receive(:log_error)
|
||||||
.with(error_message('setting disabled'))
|
.with(error_message('setting disabled'))
|
||||||
|
|
||||||
expect(subject).to eq(status: :error, message: 'setting disabled')
|
expect(execute).to be_error
|
||||||
|
expect(execute.message).to eq('setting disabled')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe IncidentManagement::Incidents::CreateService do
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
let_it_be(:user) { User.alert_bot }
|
||||||
|
let(:description) { 'Incident description' }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
subject(:create_incident) { described_class.new(project, user, title: title, description: description).execute }
|
||||||
|
|
||||||
|
context 'when incident has title and description' do
|
||||||
|
let(:title) { 'Incident title' }
|
||||||
|
let(:new_issue) { Issue.last! }
|
||||||
|
let(:label_title) { IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title] }
|
||||||
|
|
||||||
|
it 'responds with success' do
|
||||||
|
expect(create_incident).to be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates an incident issue' do
|
||||||
|
expect { create_incident }.to change(Issue, :count).by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'created issue has correct attributes' do
|
||||||
|
create_incident
|
||||||
|
|
||||||
|
expect(new_issue.title).to eq(title)
|
||||||
|
expect(new_issue.description).to eq(description)
|
||||||
|
expect(new_issue.author).to eq(user)
|
||||||
|
expect(new_issue.labels.map(&:title)).to eq([label_title])
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when incident label does not exists' do
|
||||||
|
it 'creates incident label' do
|
||||||
|
expect { create_incident }.to change { project.labels.where(title: label_title).count }.by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when incident label already exists' do
|
||||||
|
let!(:label) { create(:label, project: project, title: label_title) }
|
||||||
|
|
||||||
|
it 'does not create new labels' do
|
||||||
|
expect { create_incident }.not_to change(Label, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when incident has no title' do
|
||||||
|
let(:title) { '' }
|
||||||
|
|
||||||
|
it 'does not create an issue' do
|
||||||
|
expect { create_incident }.not_to change(Issue, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'responds with errors' do
|
||||||
|
expect(create_incident).to be_error
|
||||||
|
expect(create_incident.message).to eq("Title can't be blank")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'result payload contains an Issue object' do
|
||||||
|
expect(create_incident.payload[:issue]).to be_kind_of(Issue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,19 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.shared_examples 'create alert issue sets issue labels' do
|
|
||||||
let(:title) { IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title] }
|
|
||||||
let!(:label) { create(:label, project: project, title: title) }
|
|
||||||
let(:label_service) { instance_double(IncidentManagement::CreateIncidentLabelService, execute: label_service_response) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(IncidentManagement::CreateIncidentLabelService).to receive(:new).with(project, user).and_return(label_service)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when create incident label responds with success' do
|
|
||||||
let(:label_service_response) { ServiceResponse.success(payload: { label: label }) }
|
|
||||||
|
|
||||||
it 'adds label to issue' do
|
|
||||||
expect(issue.labels).to eq([label])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in a new issue