Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1bc5af7661
commit
c93374099a
39 changed files with 682 additions and 335 deletions
|
@ -96,7 +96,8 @@ In which enterprise tier should this feature go? See https://about.gitlab.com/ha
|
|||
|
||||
### Links / references
|
||||
|
||||
<!-- Label reminders - you should have one of each of the following labels if you can figure out the correct ones -->
|
||||
<!-- Label reminders - you should have one of each of the following labels.
|
||||
Read the descriptions on https://gitlab.com/gitlab-org/gitlab/-/labels to find the correct ones -->
|
||||
/label ~devops:: ~group: ~Category:
|
||||
|
||||
/label ~feature
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
/label ~"feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Starter"/~"GitLab Premium"/~"GitLab Ultimate"
|
||||
|
||||
|
||||
<!-- Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section.
|
||||
<!-- Read the labels descriptions on https://gitlab.com/gitlab-org/gitlab/-/labels to find the appropriate labels. Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section.
|
||||
|
||||
Other sections to consider adding:
|
||||
|
||||
|
|
|
@ -150,7 +150,7 @@ module IssuableCollections
|
|||
common_attributes + [:project, project: :namespace]
|
||||
when 'MergeRequest'
|
||||
common_attributes + [
|
||||
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
|
||||
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers,
|
||||
source_project: :route, head_pipeline: :project, target_project: :namespace
|
||||
]
|
||||
end
|
||||
|
|
|
@ -406,7 +406,7 @@ class IssuableFinder
|
|||
elsif params.filter_by_any_assignee?
|
||||
items.assigned
|
||||
elsif params.assignee
|
||||
items.assigned_to(params.assignee)
|
||||
items_assigned_to(items, params.assignee)
|
||||
elsif params.assignee_id? || params.assignee_username? # assignee not found
|
||||
items.none
|
||||
else
|
||||
|
@ -414,6 +414,10 @@ class IssuableFinder
|
|||
end
|
||||
end
|
||||
|
||||
def items_assigned_to(items, user)
|
||||
items.assigned_to(user)
|
||||
end
|
||||
|
||||
def by_negated_assignee(items)
|
||||
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
|
||||
if not_params.assignees.present?
|
||||
|
|
|
@ -164,6 +164,13 @@ class MergeRequestsFinder < IssuableFinder
|
|||
end
|
||||
# rubocop: enable CodeReuse/Finder
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def items_assigned_to(items, user)
|
||||
assignee_or_reviewer = MergeRequest.from_union([super, items.reviewer_assigned_to(user)])
|
||||
items.where(id: assignee_or_reviewer)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def by_deployments(items)
|
||||
env = params[:environment]
|
||||
before = params[:deployed_before]
|
||||
|
|
|
@ -303,6 +303,19 @@ class MergeRequest < ApplicationRecord
|
|||
includes(:metrics)
|
||||
end
|
||||
|
||||
scope :reviewer_assigned_to, ->(user) do
|
||||
mr_reviewers_table = MergeRequestReviewer.arel_table
|
||||
|
||||
inner_sql = mr_reviewers_table
|
||||
.project(Arel::Nodes::True.new)
|
||||
.where(
|
||||
mr_reviewers_table[:merge_request_id].eq(MergeRequest.arel_table[:id])
|
||||
.and(mr_reviewers_table[:user_id].eq(user.id))
|
||||
).exists
|
||||
|
||||
where(inner_sql)
|
||||
end
|
||||
|
||||
after_save :keep_around_commit, unless: :importing?
|
||||
|
||||
alias_attribute :project, :target_project
|
||||
|
|
|
@ -129,7 +129,7 @@ class MergeRequestWidgetEntity < Grape::Entity
|
|||
end
|
||||
|
||||
expose :security_reports_docs_path do |merge_request|
|
||||
help_page_path('user/application_security/sast/index.md', anchor: 'reports-json-format')
|
||||
help_page_path('user/application_security/index.md', anchor: 'viewing-security-scan-information-in-merge-requests')
|
||||
end
|
||||
|
||||
private
|
||||
|
|
84
app/services/issuable/import_csv/base_service.rb
Normal file
84
app/services/issuable/import_csv/base_service.rb
Normal file
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Issuable
|
||||
module ImportCsv
|
||||
class BaseService
|
||||
def initialize(user, project, csv_io)
|
||||
@user = user
|
||||
@project = project
|
||||
@csv_io = csv_io
|
||||
@results = { success: 0, error_lines: [], parse_error: false }
|
||||
end
|
||||
|
||||
def execute
|
||||
process_csv
|
||||
email_results_to_user
|
||||
|
||||
@results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_csv
|
||||
with_csv_lines.each do |row, line_no|
|
||||
issuable_attributes = {
|
||||
title: row[:title],
|
||||
description: row[:description]
|
||||
}
|
||||
|
||||
if create_issuable(issuable_attributes).persisted?
|
||||
@results[:success] += 1
|
||||
else
|
||||
@results[:error_lines].push(line_no)
|
||||
end
|
||||
end
|
||||
rescue ArgumentError, CSV::MalformedCSVError
|
||||
@results[:parse_error] = true
|
||||
end
|
||||
|
||||
def with_csv_lines
|
||||
csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
|
||||
verify_headers!(csv_data)
|
||||
|
||||
csv_parsing_params = {
|
||||
col_sep: detect_col_sep(csv_data.lines.first),
|
||||
headers: true,
|
||||
header_converters: :symbol
|
||||
}
|
||||
|
||||
CSV.new(csv_data, csv_parsing_params).each.with_index(2)
|
||||
end
|
||||
|
||||
def verify_headers!(data)
|
||||
headers = data.lines.first.downcase
|
||||
return if headers.include?('title') && headers.include?('description')
|
||||
|
||||
raise CSV::MalformedCSVError
|
||||
end
|
||||
|
||||
def detect_col_sep(header)
|
||||
if header.include?(",")
|
||||
","
|
||||
elsif header.include?(";")
|
||||
";"
|
||||
elsif header.include?("\t")
|
||||
"\t"
|
||||
else
|
||||
raise CSV::MalformedCSVError
|
||||
end
|
||||
end
|
||||
|
||||
def create_issuable(attributes)
|
||||
create_issuable_class.new(@project, @user, attributes).execute
|
||||
end
|
||||
|
||||
def email_results_to_user
|
||||
# defined in ImportCsvService
|
||||
end
|
||||
|
||||
def create_issuable_class
|
||||
# defined in ImportCsvService
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,69 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Issues
|
||||
class ImportCsvService
|
||||
def initialize(user, project, csv_io)
|
||||
@user = user
|
||||
@project = project
|
||||
@csv_io = csv_io
|
||||
@results = { success: 0, error_lines: [], parse_error: false }
|
||||
end
|
||||
|
||||
class ImportCsvService < Issuable::ImportCsv::BaseService
|
||||
def execute
|
||||
record_import_attempt
|
||||
process_csv
|
||||
email_results_to_user
|
||||
|
||||
@results
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def record_import_attempt
|
||||
Issues::CsvImport.create!(user: @user, project: @project)
|
||||
end
|
||||
|
||||
def process_csv
|
||||
csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
|
||||
|
||||
csv_parsing_params = {
|
||||
col_sep: detect_col_sep(csv_data.lines.first),
|
||||
headers: true,
|
||||
header_converters: :symbol
|
||||
}
|
||||
|
||||
CSV.new(csv_data, csv_parsing_params).each.with_index(2) do |row, line_no|
|
||||
issue_attributes = {
|
||||
title: row[:title],
|
||||
description: row[:description]
|
||||
}
|
||||
|
||||
issue = Issues::CreateService.new(@project, @user, issue_attributes).execute
|
||||
|
||||
if issue.persisted?
|
||||
@results[:success] += 1
|
||||
else
|
||||
@results[:error_lines].push(line_no)
|
||||
end
|
||||
end
|
||||
rescue ArgumentError, CSV::MalformedCSVError
|
||||
@results[:parse_error] = true
|
||||
super
|
||||
end
|
||||
|
||||
def email_results_to_user
|
||||
Notify.import_issues_csv_email(@user.id, @project.id, @results).deliver_later
|
||||
end
|
||||
|
||||
def detect_col_sep(header)
|
||||
if header.include?(",")
|
||||
","
|
||||
elsif header.include?(";")
|
||||
";"
|
||||
elsif header.include?("\t")
|
||||
"\t"
|
||||
else
|
||||
raise CSV::MalformedCSVError
|
||||
end
|
||||
private
|
||||
|
||||
def create_issuable_class
|
||||
Issues::CreateService
|
||||
end
|
||||
|
||||
def record_import_attempt
|
||||
Issues::CsvImport.create!(user: @user, project: @project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
-# We currently only support `alert`, `notice`, `success`, 'toast'
|
||||
- icons = {'alert' => 'error', 'notice' => 'information-o', 'success' => 'check-circle'};
|
||||
.flash-container.flash-container-page.sticky
|
||||
.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' } }
|
||||
- flash.each do |key, value|
|
||||
- if key == 'toast' && value
|
||||
.js-toast-message{ data: { message: value } }
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information.
|
||||
%ul.list-unstyled.navbar-sub-nav
|
||||
- if dashboard_nav_link?(:projects)
|
||||
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_value: "" } }) do
|
||||
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do
|
||||
%button{ type: 'button', data: { toggle: "dropdown" } }
|
||||
= _('Projects')
|
||||
= sprite_icon('chevron-down', css_class: 'caret-down')
|
||||
|
@ -10,7 +10,7 @@
|
|||
= render "layouts/nav/projects_dropdown/show"
|
||||
|
||||
- if dashboard_nav_link?(:groups)
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "d-none d-md-block home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown", track_value: "" } }) do
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "d-none d-md-block home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do
|
||||
%button{ type: 'button', data: { toggle: "dropdown" } }
|
||||
= _('Groups')
|
||||
= sprite_icon('chevron-down', css_class: 'caret-down')
|
||||
|
@ -18,7 +18,7 @@
|
|||
= render "layouts/nav/groups_dropdown/show"
|
||||
|
||||
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
|
||||
%li.header-more.dropdown{ **tracking_attrs('main_navigation', 'click_more_link', 'navigation') }
|
||||
= nav_link(html_options: { id: 'nav-more-dropdown', class: "header-more dropdown", data: { track_label: "more_dropdown", track_event: "click_more_link" } }) do
|
||||
%a{ href: "#", data: { toggle: "dropdown", qa_selector: 'more_dropdown' } }
|
||||
= _('More')
|
||||
= sprite_icon('chevron-down', css_class: 'caret-down')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.file-header-content
|
||||
= blob_icon blob.mode, blob.name
|
||||
|
||||
%strong.file-title-name
|
||||
%strong.file-title-name{ data: { qa_selector: 'file_name_content' } }
|
||||
= blob.name
|
||||
|
||||
= copy_file_path_button(blob.path)
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
= author_avatar(commit, size: 40, has_tooltip: false)
|
||||
|
||||
.commit-detail.flex-list
|
||||
.commit-content.qa-commit-content
|
||||
.commit-content{ data: { qa_selector: 'commit_content' } }
|
||||
- if view_details && merge_request
|
||||
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)]
|
||||
- else
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
= link_icon
|
||||
= i
|
||||
- highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil
|
||||
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight } }
|
||||
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } }
|
||||
%pre.code.highlight
|
||||
%code
|
||||
= blob.present.highlight
|
||||
|
|
|
@ -184,7 +184,7 @@ to authenticate with the API:
|
|||
- [Maven Repository](../user/packages/maven_repository/index.md#authenticate-with-a-ci-job-token-in-maven)
|
||||
- [NPM Repository](../user/packages/npm_registry/index.md#authenticate-with-a-ci-job-token)
|
||||
- [Nuget Repository](../user/packages/nuget_repository/index.md)
|
||||
- [PyPI Repository](../user/packages/pypi_repository/index.md#using-gitlab-ci-with-pypi-packages)
|
||||
- [PyPI Repository](../user/packages/pypi_repository/index.md#publish-a-pypi-package-by-using-cicd)
|
||||
- [Generic packages](../user/packages/generic_packages/index.md#publish-a-generic-package-by-using-cicd)
|
||||
- [Get job artifacts](job_artifacts.md#get-job-artifacts)
|
||||
- [Pipeline triggers](pipeline_triggers.md) (using the `token=` parameter)
|
||||
|
|
|
@ -362,13 +362,13 @@ Kubernetes:
|
|||
|
||||
##### Docker executor inside GitLab Runner configuration
|
||||
|
||||
If you are an administrator of GitLab Runner and you always want to use
|
||||
If you are an administrator of GitLab Runner and you want to use
|
||||
the mirror for every `dind` service, update the
|
||||
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
|
||||
to specify a [volume
|
||||
mount](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section).
|
||||
|
||||
Given we have a file `/opt/docker/daemon.json` with the following
|
||||
For example, if you have a `/opt/docker/daemon.json` file with the following
|
||||
content:
|
||||
|
||||
```json
|
||||
|
@ -379,7 +379,7 @@ content:
|
|||
}
|
||||
```
|
||||
|
||||
Update the `config.toml` for GitLab Runner to mount the file to
|
||||
Update the `config.toml` file to mount the file to
|
||||
`/etc/docker/daemon.json`. This would mount the file for **every**
|
||||
container that is created by GitLab Runner. The configuration will be
|
||||
picked up by the `dind` service.
|
||||
|
@ -394,7 +394,57 @@ picked up by the `dind` service.
|
|||
volumes = ["/opt/docker/daemon.json:/etc/docker/daemon.json:ro"]
|
||||
```
|
||||
|
||||
### Use Docker socket binding
|
||||
##### Kubernetes executor inside GitLab Runner configuration
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3223) in GitLab Runner 13.6.
|
||||
|
||||
If you are an administrator of GitLab Runner and you want to use
|
||||
the mirror for every `dind` service, update the
|
||||
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
|
||||
to specify a [ConfigMap volume
|
||||
mount](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes).
|
||||
|
||||
For example, if you have a `/tmp/daemon.json` file with the following
|
||||
content:
|
||||
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://registry-mirror.example.com"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Create a [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) with the content
|
||||
of this file. You can do this with a command like:
|
||||
|
||||
```shell
|
||||
kubectl create configmap docker-daemon --namespace gitlab-runner --from-file /tmp/daemon.json
|
||||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
Make sure to use the namespace that GitLab Runner Kubernetes executor uses
|
||||
to create job pods in.
|
||||
|
||||
After the ConfigMap is created, you can update the `config.toml`
|
||||
file to mount the file to `/etc/docker/daemon.json`. This update
|
||||
mounts the file for **every** container that is created by GitLab Runner.
|
||||
The configuration is picked up by the `dind` service.
|
||||
|
||||
```toml
|
||||
[[runners]]
|
||||
...
|
||||
executor = "kubernetes"
|
||||
[runners.kubernetes]
|
||||
image = "alpine:3.12"
|
||||
privileged = true
|
||||
[[runners.kubernetes.volumes.config_map]]
|
||||
name = "docker-daemon"
|
||||
mount_path = "/etc/docker/daemon.json"
|
||||
sub_path = "daemon.json"
|
||||
```
|
||||
|
||||
#### Use Docker socket binding
|
||||
|
||||
The third approach is to bind-mount `/var/run/docker.sock` into the
|
||||
container so that Docker is available in the context of that image.
|
||||
|
|
|
@ -759,7 +759,7 @@ While you work on a test suite, you may want to run these specs in watch mode, s
|
|||
# Watch and rerun all specs matching the name icon
|
||||
yarn jest --watch icon
|
||||
|
||||
# Watch and rerun one specifc file
|
||||
# Watch and rerun one specific file
|
||||
yarn jest --watch path/to/spec/file.spec.js
|
||||
```
|
||||
|
||||
|
|
BIN
doc/user/application_security/img/security_widget_v13_6.png
Normal file
BIN
doc/user/application_security/img/security_widget_v13_6.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
|
@ -123,6 +123,24 @@ latest versions of the scanning tools without having to do anything. There are s
|
|||
with this approach, however, and there is a
|
||||
[plan to resolve them](https://gitlab.com/gitlab-org/gitlab/-/issues/9725).
|
||||
|
||||
## Viewing security scan information in merge requests **(CORE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4393) in GitLab Core 13.5.
|
||||
> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/273205) in 13.6.
|
||||
> - It's [deployed behind a feature flag](../feature_flags.md), enabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It can be enabled or disabled for a single project.
|
||||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-the-basic-security-widget). **(CORE ONLY)**
|
||||
|
||||
CAUTION: **Warning:**
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
|
||||
Merge requests which have run security scans let you know that the generated
|
||||
reports are available to download.
|
||||
|
||||
![Security widget](img/security_widget_v13_6.png)
|
||||
|
||||
## Interacting with the vulnerabilities
|
||||
|
||||
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.8.
|
||||
|
@ -594,3 +612,28 @@ Analyzer results are displayed in the [job logs](../../ci/pipelines/#expand-and-
|
|||
[Merge Request widget](sast/index.md#overview)
|
||||
or [Security Dashboard](security_dashboard/index.md).
|
||||
There is [an open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/235772) in which changes to this behavior are being discussed.
|
||||
|
||||
### Enable or disable the basic security widget **(CORE ONLY)**
|
||||
|
||||
The basic security widget is under development but ready for production use.
|
||||
It is deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../feature_flags.md)
|
||||
can opt to disable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
# For the instance
|
||||
Feature.enable(:core_security_mr_widget)
|
||||
# For a single project
|
||||
Feature.enable(:core_security_mr_widget, Project.find(<project id>))
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
# For the instance
|
||||
Feature.disable(:core_security_mr_widget)
|
||||
# For a single project
|
||||
Feature.disable(:core_security_mr_widget, Project.find(<project id>))
|
||||
```
|
||||
|
|
|
@ -77,8 +77,10 @@ neither stable nor versioned yet. For this reason, GitLab only guarantees compat
|
|||
between corresponding major.minor (X.Y) versions of GitLab and its cluster side
|
||||
component, `agentk`.
|
||||
|
||||
Upgrade your agent installations together with GitLab upgrades: if you install
|
||||
GitLab version 13.6, use version 13.6.x versions of `agentk`.
|
||||
Upgrade your agent installations together with GitLab upgrades. To decide which version of `agentk`to install follow:
|
||||
|
||||
1. Open the [GITLAB_KAS_VERSION](https://gitlab.com/gitlab-org/gitlab/-/blob/master/GITLAB_KAS_VERSION) file from the GitLab Repository, which contains the latest `agentk` version associated with the `master` branch.
|
||||
1. Change the `master` branch and select the Git tag associated with your version. For instance, you could change it to GitLab [v13.5.3-ee release](https://gitlab.com/gitlab-org/gitlab/-/blob/v13.5.3-ee/GITLAB_KAS_VERSION)
|
||||
|
||||
The available `agentk` versions can be found in
|
||||
[its container registry](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/container_registry/eyJuYW1lIjoiZ2l0bGFiLW9yZy9jbHVzdGVyLWludGVncmF0aW9uL2dpdGxhYi1hZ2VudC9hZ2VudGsiLCJ0YWdzX3BhdGgiOiIvZ2l0bGFiLW9yZy9jbHVzdGVyLWludGVncmF0aW9uL2dpdGxhYi1hZ2VudC9yZWdpc3RyeS9yZXBvc2l0b3J5LzEyMjMyMDUvdGFncz9mb3JtYXQ9anNvbiIsImlkIjoxMjIzMjA1LCJjbGVhbnVwX3BvbGljeV9zdGFydGVkX2F0IjpudWxsfQ==).
|
||||
|
|
|
@ -134,7 +134,7 @@ For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & S
|
|||
|
||||
Under Okta's **Single sign-on URL** field, check the option **Use this for Recipient URL and Destination URL**.
|
||||
|
||||
We recommend:
|
||||
For NameID, the following settings are recommended; for SCIM, the following settings are required:
|
||||
|
||||
- **Application username** (NameID) set to **Custom** `user.getInternalProperty("id")`.
|
||||
- **Name ID Format** set to **Persistent**.
|
||||
|
|
|
@ -121,8 +121,12 @@ Once synchronized, changing the field mapped to `id` and `externalId` may cause
|
|||
|
||||
### Okta configuration steps
|
||||
|
||||
The SAML application that was created during [Single sign-on](index.md#okta-setup-notes) setup for [Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/overview/) now needs to be set up for SCIM.
|
||||
Before proceeding, be sure to complete the [GitLab configuration](#gitlab-configuration) process.
|
||||
Before you start this section, complete the [GitLab configuration](#gitlab-configuration) process.
|
||||
Make sure that you've also set up a SAML application for [Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/overview/),
|
||||
as described in the [Okta setup notes](index.md#okta-setup-notes)
|
||||
|
||||
Make sure that the Okta setup matches our documentation exactly, especially the NameID
|
||||
configuration. Otherwise, the Okta SCIM app may not work properly.
|
||||
|
||||
1. Sign in to Okta.
|
||||
1. If you see an **Admin** button in the top right, click the button. This will
|
||||
|
|
|
@ -38,7 +38,7 @@ Learn more about using CI/CD to build:
|
|||
- [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd)
|
||||
- [NuGet packages](../nuget_repository/index.md#publish-a-nuget-package-by-using-cicd)
|
||||
- [Conan packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd)
|
||||
- [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages)
|
||||
- [PyPI packages](../pypi_repository/index.md#publish-a-pypi-package-by-using-cicd)
|
||||
- [Generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd)
|
||||
|
||||
If you use CI/CD to build a package, extended activity information is displayed
|
||||
|
|
|
@ -4,42 +4,28 @@ group: Package
|
|||
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
|
||||
---
|
||||
|
||||
# GitLab PyPI Repository
|
||||
# PyPI packages in the Package Registry
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208747) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
|
||||
|
||||
With the GitLab PyPI Repository, every project can have its own space to store PyPI packages.
|
||||
Publish PyPI packages in your project’s Package Registry. Then install the
|
||||
packages whenever you need to use them as a dependency.
|
||||
|
||||
The GitLab PyPI Repository works with:
|
||||
|
||||
- [pip](https://pypi.org/project/pip/)
|
||||
- [twine](https://pypi.org/project/twine/)
|
||||
|
||||
## Setting up your development environment
|
||||
|
||||
You need a recent version of [pip](https://pypi.org/project/pip/) and [twine](https://pypi.org/project/twine/).
|
||||
|
||||
## Enabling the PyPI Repository
|
||||
|
||||
NOTE: **Note:**
|
||||
This option is available only if your GitLab administrator has
|
||||
[enabled support for the Package Registry](../../../administration/packages/index.md).
|
||||
|
||||
After the PyPI Repository is enabled, it is available for all new projects
|
||||
by default. To enable it for existing projects, or if you want to disable it:
|
||||
|
||||
1. Navigate to your project's **Settings > General > Visibility, project features, permissions**.
|
||||
1. Find the Packages feature and enable or disable it.
|
||||
1. Click on **Save changes** for the changes to take effect.
|
||||
|
||||
You should then be able to see the **Packages & Registries** section on the left sidebar.
|
||||
|
||||
## Getting started
|
||||
## Build a PyPI package
|
||||
|
||||
This section covers creating a new example PyPI package to upload. This is a
|
||||
quickstart to test out the **GitLab PyPI Registry**. If you already understand how
|
||||
to build and publish your own packages, move on to the [next section](#adding-the-gitlab-pypi-repository-as-a-source).
|
||||
quickstart to test out the **GitLab PyPI Registry**. If you already understand
|
||||
how to build and publish your own packages, move on to the [next section](#authenticate-with-the-package-registry).
|
||||
|
||||
### Install pip and twine
|
||||
|
||||
You need a recent version of [pip](https://pypi.org/project/pip/) and [twine](https://pypi.org/project/twine/).
|
||||
|
||||
### Create a project
|
||||
|
||||
|
@ -97,8 +83,8 @@ Type "help", "copyright", "credits" or "license" for more information.
|
|||
Hello from MyPyPiPackage
|
||||
```
|
||||
|
||||
Once we've verified that the sample project is working as above, we can next
|
||||
work on creating a package.
|
||||
After we've verified that the sample project is working as previously described,
|
||||
we can next work on creating a package.
|
||||
|
||||
### Create a package
|
||||
|
||||
|
@ -111,13 +97,13 @@ touch setup.py
|
|||
|
||||
This file contains all the information about our package. For more information
|
||||
about this file, see [creating setup.py](https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py).
|
||||
GitLab identifies packages based on
|
||||
Becaue GitLab identifies packages based on
|
||||
[Python normalized names (PEP-503)](https://www.python.org/dev/peps/pep-0503/#normalized-names),
|
||||
so ensure your package name meets these requirements.
|
||||
See the [installation section](#install-packages) for more details.
|
||||
ensure your package name meets these requirements. See the [installation section](#publish-a-pypi-package-by-using-cicd)
|
||||
for more details.
|
||||
|
||||
For this guide, we don't need to extensively fill out this file, simply add the
|
||||
below to your `setup.py`:
|
||||
For this guide, we don't need to extensively fill out this file. Add the
|
||||
following to your `setup.py`:
|
||||
|
||||
```python
|
||||
import setuptools
|
||||
|
@ -138,7 +124,7 @@ setuptools.setup(
|
|||
)
|
||||
```
|
||||
|
||||
Save the file, then execute the setup like so:
|
||||
Save the file, and then execute the setup:
|
||||
|
||||
```shell
|
||||
python3 setup.py sdist bdist_wheel
|
||||
|
@ -157,18 +143,21 @@ And confirm your output matches the below:
|
|||
mypypipackage-0.0.1-py3-none-any.whl mypypipackage-0.0.1.tar.gz
|
||||
```
|
||||
|
||||
Our package is now all set up and ready to be uploaded to the **GitLab PyPI
|
||||
Package Registry**. Before we do so, we next need to set up authentication.
|
||||
The package is now all set up and is ready to be uploaded to the
|
||||
_GitLab PyPI Package Registry_. Before we do so, we next need to set up
|
||||
authentication.
|
||||
|
||||
## Adding the GitLab PyPI Repository as a source
|
||||
## Authenticate with the Package Registry
|
||||
|
||||
### Authenticating with a personal access token
|
||||
### Authenticate with a personal access token
|
||||
|
||||
You need the following:
|
||||
|
||||
- A personal access token. You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
|
||||
- A personal access token. You can generate a
|
||||
[personal access token](../../../user/profile/personal_access_tokens.md)
|
||||
with the scope set to `api` for repository authentication.
|
||||
- A suitable name for your source.
|
||||
- Your project ID which can be found on the home page of your project.
|
||||
- Your project ID, which is found on the home page of your project.
|
||||
|
||||
Edit your `~/.pypirc` file and add the following:
|
||||
|
||||
|
@ -183,13 +172,15 @@ username = __token__
|
|||
password = <your personal access token>
|
||||
```
|
||||
|
||||
### Authenticating with a deploy token
|
||||
### Authenticate with a deploy token
|
||||
|
||||
You need the following:
|
||||
|
||||
- A deploy token. You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the `read_package_registry` and/or `write_package_registry` scopes for repository authentication.
|
||||
- A deploy token. You can generate a [deploy token](./../../project/deploy_tokens/index.md)
|
||||
with the `read_package_registry` or `write_package_registry` scopes for
|
||||
repository authentication.
|
||||
- A suitable name for your source.
|
||||
- Your project ID which can be found on the home page of your project.
|
||||
- Your project ID, which is found on the home page of your project.
|
||||
|
||||
Edit your `~/.pypirc` file and add the following:
|
||||
|
||||
|
@ -204,16 +195,18 @@ username = <deploy token username>
|
|||
password = <deploy token>
|
||||
```
|
||||
|
||||
## Uploading packages
|
||||
## Publish a PyPI package
|
||||
|
||||
When uploading packages, note that:
|
||||
When publishing packages, note that:
|
||||
|
||||
- The maximum allowed size is 50 Megabytes.
|
||||
- You cannot upload the same version of a package multiple times. If you try, you receive the error `Validation failed: File name has already been taken`.
|
||||
- You can't upload the same version of a package multiple times. If you try,
|
||||
you'll receive the error `Validation failed: File name has already been taken`.
|
||||
|
||||
### Ensure your version string is valid
|
||||
|
||||
If your version string (for example, `0.0.1`) is invalid, it will be rejected. GitLab uses the following regex to validate the version string.
|
||||
If your version string (for example, `0.0.1`) is invalid, it will be rejected.
|
||||
GitLab uses the following regex to validate the version string.
|
||||
|
||||
```ruby
|
||||
\A(?:
|
||||
|
@ -227,13 +220,14 @@ If your version string (for example, `0.0.1`) is invalid, it will be rejected. G
|
|||
)\z}xi
|
||||
```
|
||||
|
||||
You can play around with the regex and try your version strings on [this regular expression editor](https://rubular.com/r/FKM6d07ouoDaFV).
|
||||
You can experiment with the regex and try your version strings using this
|
||||
[regular expression editor](https://rubular.com/r/FKM6d07ouoDaFV).
|
||||
|
||||
For more details about the regex used, please check the [documentation here](https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions))
|
||||
For more details about the regex used, review this [documentation](https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions).
|
||||
|
||||
### Upload packages with Twine
|
||||
### Publish a PyPI package by using twine
|
||||
|
||||
If you were following the guide above, then the `MyPyPiPackage` package should
|
||||
If you were following the steps on this page, the `MyPyPiPackage` package should
|
||||
be ready to be uploaded. Run the following command:
|
||||
|
||||
```shell
|
||||
|
@ -260,8 +254,8 @@ you can upload to the repository with the authentication inline:
|
|||
TWINE_PASSWORD=<personal_access_token or deploy_token> TWINE_USERNAME=<username or deploy_token_username> python3 -m twine upload --repository-url https://gitlab.com/api/v4/projects/<project_id>/packages/pypi dist/*
|
||||
```
|
||||
|
||||
If you did not follow the guide above, then you need to ensure your package
|
||||
has been properly built and you [created a PyPI package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/).
|
||||
If you didn't use the steps on this page, you need to ensure your package has
|
||||
been properly built, and that you [created a PyPI package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/).
|
||||
|
||||
You can then upload your package using the following command:
|
||||
|
||||
|
@ -272,9 +266,42 @@ python -m twine upload --repository <source_name> dist/<package_file>
|
|||
Where:
|
||||
|
||||
- `<package_file>` is your package filename, ending in `.tar.gz` or `.whl`.
|
||||
- `<source_name>` is the [source name used during setup](#adding-the-gitlab-pypi-repository-as-a-source).
|
||||
- `<source_name>` is the [source name used during setup](#authenticate-with-the-package-registry).
|
||||
|
||||
## Install packages
|
||||
### Publish a PyPI package by using CI/CD
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202012) in GitLab 13.4.
|
||||
|
||||
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you
|
||||
can use `CI_JOB_TOKEN` in place of the personal access token or deploy a token
|
||||
in your commands.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
image: python:latest
|
||||
|
||||
run:
|
||||
script:
|
||||
- pip install twine
|
||||
- python setup.py sdist bdist_wheel
|
||||
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
||||
```
|
||||
|
||||
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check into GitLab:
|
||||
|
||||
```ini
|
||||
[distutils]
|
||||
index-servers =
|
||||
gitlab
|
||||
|
||||
[gitlab]
|
||||
repository = https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
|
||||
username = gitlab-ci-token
|
||||
password = ${env.CI_JOB_TOKEN}
|
||||
```
|
||||
|
||||
## Install a PyPI package
|
||||
|
||||
Install the latest version of a package using the following command:
|
||||
|
||||
|
@ -310,35 +337,3 @@ GitLab looks for packages using
|
|||
so the characters `-`, `_`, and `.` are all treated the same and repeated characters are removed.
|
||||
A `pip install` request for `my.package` looks for packages that match any of
|
||||
the three characters, such as `my-package`, `my_package`, and `my....package`.
|
||||
|
||||
## Using GitLab CI with PyPI packages
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202012) in GitLab 13.4.
|
||||
|
||||
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you can use
|
||||
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
image: python:latest
|
||||
|
||||
run:
|
||||
script:
|
||||
- pip install twine
|
||||
- python setup.py sdist bdist_wheel
|
||||
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
||||
```
|
||||
|
||||
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check into GitLab:
|
||||
|
||||
```ini
|
||||
[distutils]
|
||||
index-servers =
|
||||
gitlab
|
||||
|
||||
[gitlab]
|
||||
repository = https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
|
||||
username = gitlab-ci-token
|
||||
password = ${env.CI_JOB_TOKEN}
|
||||
```
|
||||
|
|
1
qa/qa.rb
1
qa/qa.rb
|
@ -403,6 +403,7 @@ module QA
|
|||
|
||||
module Layout
|
||||
autoload :Banner, 'qa/page/layout/banner'
|
||||
autoload :Flash, 'qa/page/layout/flash'
|
||||
autoload :PerformanceBar, 'qa/page/layout/performance_bar'
|
||||
end
|
||||
|
||||
|
|
|
@ -13,11 +13,19 @@ module QA
|
|||
base.view 'app/views/shared/_commit_message_container.html.haml' do
|
||||
element :commit_message, "text_area_tag 'commit_message'" # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
base.view 'app/views/projects/commits/_commit.html.haml' do
|
||||
element :commit_content
|
||||
end
|
||||
end
|
||||
|
||||
def add_commit_message(message)
|
||||
fill_in 'commit_message', with: message
|
||||
end
|
||||
|
||||
def has_commit_message?(text)
|
||||
has_element?(:commit_content, text: text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,16 +7,25 @@ module QA
|
|||
include Shared::CommitMessage
|
||||
include Project::SubMenus::Settings
|
||||
include Project::SubMenus::Common
|
||||
include Layout::Flash
|
||||
|
||||
view 'app/helpers/blob_helper.rb' do
|
||||
element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern
|
||||
element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
view 'app/views/projects/blob/_header_content.html.haml' do
|
||||
element :file_name_content
|
||||
end
|
||||
|
||||
view 'app/views/projects/blob/_remove.html.haml' do
|
||||
element :delete_file_button, "button_tag 'Delete file'" # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
view 'app/views/shared/_file_highlight.html.haml' do
|
||||
element :file_content
|
||||
end
|
||||
|
||||
def click_edit
|
||||
click_on 'Edit'
|
||||
end
|
||||
|
@ -28,6 +37,18 @@ module QA
|
|||
def click_delete_file
|
||||
click_on 'Delete file'
|
||||
end
|
||||
|
||||
def has_file?(name)
|
||||
has_element?(:file_name_content, text: name)
|
||||
end
|
||||
|
||||
def has_no_file?(name)
|
||||
has_no_element?(:file_name_content, text: name)
|
||||
end
|
||||
|
||||
def has_file_content?(text)
|
||||
has_element?(:file_content, text: text)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
25
qa/qa/page/layout/flash.rb
Normal file
25
qa/qa/page/layout/flash.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Layout
|
||||
module Flash
|
||||
extend QA::Page::PageConcern
|
||||
|
||||
def self.included(base)
|
||||
super
|
||||
|
||||
base.view 'app/views/layouts/_flash.html.haml' do
|
||||
element :flash_container
|
||||
end
|
||||
end
|
||||
|
||||
def has_notice?(message)
|
||||
within_element(:flash_container) do
|
||||
has_text?(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -177,6 +177,10 @@ module QA
|
|||
has_element?(:file_name_content, text: file_name)
|
||||
end
|
||||
|
||||
def has_no_file?(file_name)
|
||||
has_no_element?(:file_name_content, text: file_name)
|
||||
end
|
||||
|
||||
def has_merge_button?
|
||||
refresh
|
||||
|
||||
|
|
|
@ -4,9 +4,11 @@ module QA
|
|||
module Page
|
||||
module Project
|
||||
class Show < Page::Base
|
||||
include Layout::Flash
|
||||
include Page::Component::ClonePanel
|
||||
include Page::Component::Breadcrumbs
|
||||
include Page::Project::SubMenus::Settings
|
||||
include Page::File::Shared::CommitMessage
|
||||
|
||||
view 'app/assets/javascripts/repository/components/preview/index.vue' do
|
||||
element :blob_viewer_content
|
||||
|
@ -121,6 +123,12 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
def has_no_file?(name)
|
||||
within_element(:file_tree_table) do
|
||||
has_no_element?(:file_name_link, text: name)
|
||||
end
|
||||
end
|
||||
|
||||
def has_name?(name)
|
||||
has_element?(:project_name_content, text: name)
|
||||
end
|
||||
|
@ -129,10 +137,6 @@ module QA
|
|||
has_element?(:blob_viewer_content, text: text)
|
||||
end
|
||||
|
||||
def last_commit_content
|
||||
find_element(:commit_content).text
|
||||
end
|
||||
|
||||
def new_merge_request
|
||||
wait_until(reload: true) do
|
||||
has_css?(element_selector_css(:create_merge_request))
|
||||
|
|
|
@ -13,6 +13,13 @@ module QA
|
|||
attribute :project do
|
||||
Project.fabricate! do |resource|
|
||||
resource.name = 'project-with-new-file'
|
||||
|
||||
# Creating the first file via the Wed IDE is tested in
|
||||
# browser_ui/3_create/web_ide/create_first_file_in_web_ide_spec.rb
|
||||
# So here we want to use the old blob viewer, which is not
|
||||
# available via the UI unless at least one file exists, which
|
||||
# is why we create the project with a readme file.
|
||||
resource.initialize_with_readme = true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -25,16 +32,13 @@ module QA
|
|||
def fabricate!
|
||||
project.visit!
|
||||
|
||||
Page::Project::Show.perform(&:create_first_new_file!)
|
||||
Page::Project::Show.perform(&:create_new_file!)
|
||||
|
||||
Page::Project::WebIDE::Edit.perform do |ide|
|
||||
ide.add_file(@name, @content)
|
||||
ide.commit_changes(@commit_message)
|
||||
ide.go_to_project
|
||||
end
|
||||
|
||||
Page::Project::Show.perform do |project|
|
||||
project.click_file(@name)
|
||||
Page::File::Form.perform do |form|
|
||||
form.add_name(@name)
|
||||
form.add_content(@content)
|
||||
form.add_commit_message(@commit_message)
|
||||
form.commit_changes
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
include HaveFileMatcher
|
||||
|
||||
RSpec.describe 'Create' do
|
||||
describe 'Files management' do
|
||||
it 'user creates, edits and deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/451' do
|
||||
Runtime::Browser.visit(:gitlab, Page::Main::Login)
|
||||
Page::Main::Login.perform(&:sign_in_using_credentials)
|
||||
Flow::Login.sign_in
|
||||
|
||||
# Create
|
||||
file_name = 'QA Test - File name'
|
||||
|
@ -18,9 +19,13 @@ module QA
|
|||
file.commit_message = commit_message_for_create
|
||||
end
|
||||
|
||||
expect(page).to have_content(file_name)
|
||||
expect(page).to have_content(file_content)
|
||||
expect(page).to have_content(commit_message_for_create)
|
||||
Page::File::Show.perform do |file|
|
||||
aggregate_failures 'file details' do
|
||||
expect(file).to have_file(file_name)
|
||||
expect(file).to have_file_content(file_content)
|
||||
expect(file).to have_commit_message(commit_message_for_create)
|
||||
end
|
||||
end
|
||||
|
||||
# Edit
|
||||
updated_file_content = 'QA Test - Updated file content'
|
||||
|
@ -28,29 +33,37 @@ module QA
|
|||
|
||||
Page::File::Show.perform(&:click_edit)
|
||||
|
||||
Page::File::Form.act do
|
||||
remove_content
|
||||
add_content(updated_file_content)
|
||||
add_commit_message(commit_message_for_update)
|
||||
commit_changes
|
||||
Page::File::Form.perform do |file|
|
||||
file.remove_content
|
||||
file.add_content(updated_file_content)
|
||||
file.add_commit_message(commit_message_for_update)
|
||||
file.commit_changes
|
||||
end
|
||||
|
||||
expect(page).to have_content('Your changes have been successfully committed.')
|
||||
expect(page).to have_content(updated_file_content)
|
||||
expect(page).to have_content(commit_message_for_update)
|
||||
Page::File::Show.perform do |file|
|
||||
aggregate_failures 'file details' do
|
||||
expect(file).to have_notice('Your changes have been successfully committed.')
|
||||
expect(file).to have_file_content(updated_file_content)
|
||||
expect(file).to have_commit_message(commit_message_for_update)
|
||||
end
|
||||
end
|
||||
|
||||
# Delete
|
||||
commit_message_for_delete = 'QA Test - Delete file'
|
||||
|
||||
Page::File::Show.act do
|
||||
click_delete
|
||||
add_commit_message(commit_message_for_delete)
|
||||
click_delete_file
|
||||
Page::File::Show.perform do |file|
|
||||
file.click_delete
|
||||
file.add_commit_message(commit_message_for_delete)
|
||||
file.click_delete_file
|
||||
end
|
||||
|
||||
expect(page).to have_content('The file has been successfully deleted.')
|
||||
expect(page).to have_content(commit_message_for_delete)
|
||||
expect(page).to have_no_content(file_name)
|
||||
Page::Project::Show.perform do |project|
|
||||
aggregate_failures 'file details' do
|
||||
expect(project).to have_notice('The file has been successfully deleted.')
|
||||
expect(project).to have_commit_message(commit_message_for_delete)
|
||||
expect(project).not_to have_file(file_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,7 @@ QA::Runtime::Browser.configure!
|
|||
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes
|
||||
|
||||
Dir[::File.join(__dir__, "support/helpers/*.rb")].sort.each { |f| require f }
|
||||
Dir[::File.join(__dir__, "support/matchers/*.rb")].sort.each { |f| require f }
|
||||
Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].sort.each { |f| require f }
|
||||
Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| require f }
|
||||
|
||||
|
|
13
qa/spec/support/matchers/have_file.rb
Normal file
13
qa/spec/support/matchers/have_file.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module HaveFileMatcher
|
||||
RSpec::Matchers.define :have_file do |file|
|
||||
match do |page_object|
|
||||
page_object.has_file?(file)
|
||||
end
|
||||
|
||||
match_when_negated do |page_object|
|
||||
page_object.has_no_file?(file)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -52,20 +52,29 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
end
|
||||
|
||||
context 'merge requests exist' do
|
||||
let_it_be(:author_user) { create(:user) }
|
||||
let(:label) { create(:label) }
|
||||
|
||||
let!(:assigned_merge_request) do
|
||||
create(:merge_request,
|
||||
assignees: [current_user],
|
||||
source_project: project,
|
||||
author: create(:user))
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
let!(:review_requested_merge_request) do
|
||||
create(:merge_request,
|
||||
reviewers: [current_user],
|
||||
source_branch: 'review',
|
||||
source_project: project,
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
let!(:assigned_merge_request_from_fork) do
|
||||
create(:merge_request,
|
||||
source_branch: 'markdown', assignees: [current_user],
|
||||
target_project: public_project, source_project: forked_project,
|
||||
author: create(:user))
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
let!(:authored_merge_request) do
|
||||
|
@ -94,7 +103,7 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
create(:merge_request,
|
||||
source_branch: 'fix',
|
||||
source_project: project,
|
||||
author: create(:user))
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -111,6 +120,10 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
expect(page).not_to have_content(labeled_merge_request.title)
|
||||
end
|
||||
|
||||
it 'shows review requested merge requests' do
|
||||
expect(page).to have_content(review_requested_merge_request.title)
|
||||
end
|
||||
|
||||
it 'shows authored merge requests', :js do
|
||||
reset_filters
|
||||
input_filtered_search("author:=#{current_user.to_reference}")
|
||||
|
|
|
@ -333,6 +333,8 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
context 'assignee filtering' do
|
||||
let_it_be(:user3) { create(:user) }
|
||||
|
||||
let(:issuables) { described_class.new(user, params).execute }
|
||||
|
||||
it_behaves_like 'assignee ID filter' do
|
||||
|
@ -351,7 +353,6 @@ RSpec.describe MergeRequestsFinder do
|
|||
merge_request3.assignees = [user2, user3]
|
||||
end
|
||||
|
||||
let_it_be(:user3) { create(:user) }
|
||||
let(:params) { { assignee_username: [user2.username, user3.username] } }
|
||||
let(:expected_issuables) { [merge_request3] }
|
||||
end
|
||||
|
@ -366,7 +367,6 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
it_behaves_like 'no assignee filter' do
|
||||
let_it_be(:user3) { create(:user) }
|
||||
let(:expected_issuables) { [merge_request4, merge_request5] }
|
||||
end
|
||||
|
||||
|
@ -374,30 +374,54 @@ RSpec.describe MergeRequestsFinder do
|
|||
let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
|
||||
end
|
||||
|
||||
context 'filtering by group milestone' do
|
||||
let(:group_milestone) { create(:milestone, group: group) }
|
||||
context 'with just reviewers' do
|
||||
it_behaves_like 'assignee username filter' do
|
||||
before do
|
||||
merge_request4.reviewers = [user3]
|
||||
merge_request4.assignees = []
|
||||
end
|
||||
|
||||
before do
|
||||
merge_request1.update!(milestone: group_milestone)
|
||||
merge_request2.update!(milestone: group_milestone)
|
||||
let(:params) { { assignee_username: [user3.username] } }
|
||||
let(:expected_issuables) { [merge_request4] }
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns merge requests assigned to that group milestone' do
|
||||
params = { milestone_title: group_milestone.title }
|
||||
context 'with an additional reviewer' do
|
||||
it_behaves_like 'assignee username filter' do
|
||||
before do
|
||||
merge_request3.assignees = [user3]
|
||||
merge_request4.reviewers = [user3]
|
||||
end
|
||||
|
||||
let(:params) { { assignee_username: [user3.username] } }
|
||||
let(:expected_issuables) { [merge_request3, merge_request4] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by group milestone' do
|
||||
let(:group_milestone) { create(:milestone, group: group) }
|
||||
|
||||
before do
|
||||
merge_request1.update!(milestone: group_milestone)
|
||||
merge_request2.update!(milestone: group_milestone)
|
||||
end
|
||||
|
||||
it 'returns merge requests assigned to that group milestone' do
|
||||
params = { milestone_title: group_milestone.title }
|
||||
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
|
||||
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
|
||||
end
|
||||
|
||||
context 'using NOT' do
|
||||
let(:params) { { not: { milestone_title: group_milestone.title } } }
|
||||
|
||||
it 'returns MRs not assigned to that group milestone' do
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
|
||||
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
|
||||
end
|
||||
|
||||
context 'using NOT' do
|
||||
let(:params) { { not: { milestone_title: group_milestone.title } } }
|
||||
|
||||
it 'returns MRs not assigned to that group milestone' do
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
|
||||
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
|
||||
end
|
||||
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -563,6 +587,27 @@ RSpec.describe MergeRequestsFinder do
|
|||
expect(mrs).to eq([mr2])
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not raise any exception with complex filters' do
|
||||
# available filters from MergeRequest dashboard UI
|
||||
params = {
|
||||
project_id: project1.id,
|
||||
scope: 'authored',
|
||||
state: 'opened',
|
||||
author_username: user.username,
|
||||
assignee_username: user.username,
|
||||
approver_usernames: [user.username],
|
||||
approved_by_usernames: [user.username],
|
||||
milestone_title: 'none',
|
||||
release_tag: 'none',
|
||||
label_names: 'none',
|
||||
my_reaction_emoji: 'none',
|
||||
draft: 'no'
|
||||
}
|
||||
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
expect { merge_requests.load }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '#row_count', :request_store do
|
||||
|
|
3
spec/fixtures/csv_no_headers.csv
vendored
Normal file
3
spec/fixtures/csv_no_headers.csv
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
Issue in 中文,Test description
|
||||
"Hello","World"
|
||||
"Title with quote""",Description
|
|
|
@ -5,126 +5,15 @@ require 'spec_helper'
|
|||
RSpec.describe Issues::ImportCsvService do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject do
|
||||
let(:service) do
|
||||
uploader = FileUploader.new(project)
|
||||
uploader.store!(file)
|
||||
|
||||
described_class.new(user, project, uploader).execute
|
||||
described_class.new(user, project, uploader)
|
||||
end
|
||||
|
||||
shared_examples_for 'an issue importer' do
|
||||
it 'records the import attempt' do
|
||||
expect { subject }
|
||||
.to change { Issues::CsvImport.where(project: project, user: user).count }
|
||||
.by 1
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'invalid file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
|
||||
|
||||
it 'returns invalid file error' do
|
||||
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
|
||||
|
||||
expect(subject[:success]).to eq(0)
|
||||
expect(subject[:parse_error]).to eq(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'an issue importer'
|
||||
end
|
||||
|
||||
context 'with a file generated by Gitlab CSV export' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') }
|
||||
|
||||
it 'imports the CSV without errors' do
|
||||
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
|
||||
|
||||
expect(subject[:success]).to eq(4)
|
||||
expect(subject[:error_lines]).to eq([])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issue attributes' do
|
||||
expect { subject }.to change { project.issues.count }.by 4
|
||||
|
||||
expect(project.issues.reload.last).to have_attributes(
|
||||
title: 'Test Title',
|
||||
description: 'Test Description'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'an issue importer'
|
||||
end
|
||||
|
||||
context 'comma delimited file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }
|
||||
|
||||
it 'imports CSV without errors' do
|
||||
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
|
||||
|
||||
expect(subject[:success]).to eq(3)
|
||||
expect(subject[:error_lines]).to eq([])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issue attributes' do
|
||||
expect { subject }.to change { project.issues.count }.by 3
|
||||
|
||||
expect(project.issues.reload.last).to have_attributes(
|
||||
title: 'Title with quote"',
|
||||
description: 'Description'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'an issue importer'
|
||||
end
|
||||
|
||||
context 'tab delimited file with error row' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') }
|
||||
|
||||
it 'imports CSV with some error rows' do
|
||||
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
|
||||
|
||||
expect(subject[:success]).to eq(2)
|
||||
expect(subject[:error_lines]).to eq([3])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issue attributes' do
|
||||
expect { subject }.to change { project.issues.count }.by 2
|
||||
|
||||
expect(project.issues.reload.last).to have_attributes(
|
||||
title: 'Hello',
|
||||
description: 'World'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'an issue importer'
|
||||
end
|
||||
|
||||
context 'semicolon delimited file with CRLF' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') }
|
||||
|
||||
it 'imports CSV with a blank row' do
|
||||
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
|
||||
|
||||
expect(subject[:success]).to eq(3)
|
||||
expect(subject[:error_lines]).to eq([4])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issue attributes' do
|
||||
expect { subject }.to change { project.issues.count }.by 3
|
||||
|
||||
expect(project.issues.reload.last).to have_attributes(
|
||||
title: 'Hello',
|
||||
description: 'World'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'an issue importer'
|
||||
end
|
||||
include_examples 'issuable import csv service', 'issue' do
|
||||
let(:issuables) { project.issues }
|
||||
let(:email_method) { :import_issues_csv_email }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'issuable import csv service' do |issuable_type|
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject { service.execute }
|
||||
|
||||
shared_examples_for 'an issuable importer' do
|
||||
if issuable_type == 'issue'
|
||||
it 'records the import attempt if resource is an issue' do
|
||||
expect { subject }
|
||||
.to change { Issues::CsvImport.where(project: project, user: user).count }
|
||||
.by 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'importer with email notification' do
|
||||
if issuable_type == 'issue'
|
||||
it 'notifies user of import result' do
|
||||
expect(Notify).to receive_message_chain(email_method, :deliver_later)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'invalid file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
|
||||
|
||||
it 'returns invalid file error' do
|
||||
expect(subject[:success]).to eq(0)
|
||||
expect(subject[:parse_error]).to eq(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'importer with email notification'
|
||||
it_behaves_like 'an issuable importer'
|
||||
end
|
||||
|
||||
context 'file without headers' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') }
|
||||
|
||||
it 'returns invalid file error' do
|
||||
expect(subject[:success]).to eq(0)
|
||||
expect(subject[:parse_error]).to eq(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'importer with email notification'
|
||||
it_behaves_like 'an issuable importer'
|
||||
end
|
||||
|
||||
context 'with a file generated by Gitlab CSV export' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') }
|
||||
|
||||
it 'imports the CSV without errors' do
|
||||
expect(subject[:success]).to eq(4)
|
||||
expect(subject[:error_lines]).to eq([])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issuable attributes' do
|
||||
expect { subject }.to change { issuables.count }.by 4
|
||||
|
||||
expect(issuables.reload.last).to have_attributes(
|
||||
title: 'Test Title',
|
||||
description: 'Test Description'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'importer with email notification'
|
||||
it_behaves_like 'an issuable importer'
|
||||
end
|
||||
|
||||
context 'comma delimited file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }
|
||||
|
||||
it 'imports CSV without errors' do
|
||||
expect(subject[:success]).to eq(3)
|
||||
expect(subject[:error_lines]).to eq([])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issuable attributes' do
|
||||
expect { subject }.to change { issuables.count }.by 3
|
||||
|
||||
expect(issuables.reload.last).to have_attributes(
|
||||
title: 'Title with quote"',
|
||||
description: 'Description'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'importer with email notification'
|
||||
it_behaves_like 'an issuable importer'
|
||||
end
|
||||
|
||||
context 'tab delimited file with error row' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') }
|
||||
|
||||
it 'imports CSV with some error rows' do
|
||||
expect(subject[:success]).to eq(2)
|
||||
expect(subject[:error_lines]).to eq([3])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issuable attributes' do
|
||||
expect { subject }.to change { issuables.count }.by 2
|
||||
|
||||
expect(issuables.reload.last).to have_attributes(
|
||||
title: 'Hello',
|
||||
description: 'World'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'importer with email notification'
|
||||
it_behaves_like 'an issuable importer'
|
||||
end
|
||||
|
||||
context 'semicolon delimited file with CRLF' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') }
|
||||
|
||||
it 'imports CSV with a blank row' do
|
||||
expect(subject[:success]).to eq(3)
|
||||
expect(subject[:error_lines]).to eq([4])
|
||||
expect(subject[:parse_error]).to eq(false)
|
||||
end
|
||||
|
||||
it 'correctly sets the issuable attributes' do
|
||||
expect { subject }.to change { issuables.count }.by 3
|
||||
|
||||
expect(issuables.reload.last).to have_attributes(
|
||||
title: 'Hello',
|
||||
description: 'World'
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'importer with email notification'
|
||||
it_behaves_like 'an issuable importer'
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue