Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-02 18:12:13 +00:00
parent 125c8a6a81
commit 23f57fb31f
57 changed files with 1315 additions and 823 deletions

View File

@ -2455,7 +2455,7 @@ Database/MultipleDatabases:
- 'lib/gitlab/gitlab_import/importer.rb'
- 'lib/gitlab/health_checks/db_check.rb'
- 'lib/gitlab/import_export/base/relation_factory.rb'
- 'lib/gitlab/import_export/relation_tree_restorer.rb'
- 'lib/gitlab/import_export/group/relation_tree_restorer.rb'
- 'lib/gitlab/legacy_github_import/importer.rb'
- 'lib/gitlab/metrics/samplers/database_sampler.rb'
- 'lib/gitlab/seeder.rb'

View File

@ -1,5 +1,12 @@
<script>
import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import {
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlButton,
GlLink,
GlSprintf,
} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
@ -8,6 +15,9 @@ import { s__ } from '~/locale';
export default {
name: 'ManualVariablesForm',
components: {
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlButton,
GlLink,
GlSprintf,
@ -32,6 +42,9 @@ export default {
value: 'value',
},
i18n: {
header: s__('CiVariables|Variables'),
keyLabel: s__('CiVariables|Key'),
valueLabel: s__('CiVariables|Value'),
keyPlaceholder: s__('CiVariables|Input variable key'),
valuePlaceholder: s__('CiVariables|Input variable value'),
formHelpText: s__(
@ -40,9 +53,13 @@ export default {
},
data() {
return {
variables: [],
key: '',
secretValue: '',
variables: [
{
key: '',
secretValue: '',
id: uniqueId(),
},
],
triggerBtnDisabled: false,
};
},
@ -50,40 +67,32 @@ export default {
variableSettings() {
return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
},
watch: {
key(newVal) {
this.handleValueChange(newVal, this.$options.inputTypes.key);
},
secretValue(newVal) {
this.handleValueChange(newVal, this.$options.inputTypes.value);
preparedVariables() {
// we need to ensure no empty variables are passed to the API
// and secretValue should be snake_case when passed to the API
return this.variables
.filter((variable) => variable.key !== '')
.map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
},
},
methods: {
...mapActions(['triggerManualJob']),
handleValueChange(newValue, type) {
if (newValue !== '') {
this.createNewVariable(type);
this.resetForm();
canRemove(index) {
return index < this.variables.length - 1;
},
addEmptyVariable() {
const lastVar = this.variables[this.variables.length - 1];
if (lastVar.key === '') {
return;
}
},
createNewVariable(type) {
const newVariable = {
key: this.key,
secret_value: this.secretValue,
this.variables.push({
key: '',
secret_value: '',
id: uniqueId(),
};
this.variables.push(newVariable);
return this.$nextTick().then(() => {
this.$refs[`${this.$options.inputTypes[type]}-${newVariable.id}`][0].focus();
});
},
resetForm() {
this.key = '';
this.secretValue = '';
},
deleteVariable(id) {
this.variables.splice(
this.variables.findIndex((el) => el.id === id),
@ -93,112 +102,92 @@ export default {
trigger() {
this.triggerBtnDisabled = true;
this.triggerManualJob(this.variables);
this.triggerManualJob(this.preparedVariables);
},
},
};
</script>
<template>
<div class="col-12" data-testid="manual-vars-form">
<label>{{ s__('CiVariables|Variables') }}</label>
<div class="ci-table">
<div class="gl-responsive-table-row table-row-header pb-0 pt-0 border-0" role="row">
<div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Key') }}</div>
<div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Value') }}</div>
</div>
<div class="row gl-justify-content-center">
<div class="col-10" data-testid="manual-vars-form">
<label>{{ $options.i18n.header }}</label>
<div
v-for="variable in variables"
v-for="(variable, index) in variables"
:key="variable.id"
class="gl-responsive-table-row"
class="gl-display-flex gl-align-items-center gl-mb-4"
data-testid="ci-variable-row"
>
<div class="table-section section-50">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
<div class="table-mobile-content gl-mr-3">
<input
:ref="`${$options.inputTypes.key}-${variable.id}`"
v-model="variable.key"
:placeholder="$options.i18n.keyPlaceholder"
class="ci-variable-body-item form-control"
data-testid="ci-variable-key"
/>
</div>
</div>
<gl-form-input-group class="gl-mr-4 gl-flex-grow-1">
<template #prepend>
<gl-input-group-text>
{{ $options.i18n.keyLabel }}
</gl-input-group-text>
</template>
<gl-form-input
:ref="`${$options.inputTypes.key}-${variable.id}`"
v-model="variable.key"
:placeholder="$options.i18n.keyPlaceholder"
data-testid="ci-variable-key"
@change="addEmptyVariable"
/>
</gl-form-input-group>
<div class="table-section section-50">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
<div class="table-mobile-content gl-mr-3">
<input
:ref="`${$options.inputTypes.value}-${variable.id}`"
v-model="variable.secret_value"
:placeholder="$options.i18n.valuePlaceholder"
class="ci-variable-body-item form-control"
data-testid="ci-variable-value"
/>
</div>
</div>
<gl-form-input-group class="gl-flex-grow-2">
<template #prepend>
<gl-input-group-text>
{{ $options.i18n.valueLabel }}
</gl-input-group-text>
</template>
<gl-form-input
:ref="`${$options.inputTypes.value}-${variable.id}`"
v-model="variable.secretValue"
:placeholder="$options.i18n.valuePlaceholder"
data-testid="ci-variable-value"
/>
</gl-form-input-group>
<div class="table-section section-10">
<div class="table-mobile-header" role="rowheader"></div>
<div class="table-mobile-content justify-content-end">
<gl-button
category="tertiary"
icon="clear"
:aria-label="__('Delete variable')"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
</div>
</div>
<!-- delete variable button placeholder to not break flex layout -->
<div
v-if="!canRemove(index)"
class="gl-w-7 gl-mr-3"
data-testid="delete-variable-btn-placeholder"
></div>
<gl-button
v-if="canRemove(index)"
class="gl-flex-grow-0 gl-flex-basis-0"
category="tertiary"
variant="danger"
icon="clear"
:aria-label="__('Delete variable')"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
</div>
<div class="gl-responsive-table-row">
<div class="table-section section-50">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div>
<div class="table-mobile-content gl-mr-3">
<input
ref="inputKey"
v-model="key"
class="js-input-key form-control"
:placeholder="$options.i18n.keyPlaceholder"
/>
</div>
</div>
<div class="table-section section-50">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Value') }}</div>
<div class="table-mobile-content gl-mr-3">
<input
ref="inputSecretValue"
v-model="secretValue"
class="ci-variable-body-item form-control"
:placeholder="$options.i18n.valuePlaceholder"
/>
</div>
</div>
<div class="gl-text-center gl-mt-5">
<gl-sprintf :message="$options.i18n.formHelpText">
<template #link="{ content }">
<gl-link :href="variableSettings" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-button
class="gl-mt-5"
variant="info"
category="primary"
:aria-label="__('Trigger manual job')"
:disabled="triggerBtnDisabled"
data-testid="trigger-manual-job-btn"
@click="trigger"
>
{{ action.button_title }}
</gl-button>
</div>
</div>
<div class="gl-text-center gl-mt-3">
<gl-sprintf :message="$options.i18n.formHelpText">
<template #link="{ content }">
<gl-link :href="variableSettings" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
<div class="d-flex justify-content-center">
<gl-button
variant="info"
category="primary"
:aria-label="__('Trigger manual job')"
:disabled="triggerBtnDisabled"
data-testid="trigger-manual-job-btn"
@click="trigger"
>
{{ action.button_title }}
</gl-button>
</div>
</div>
</template>

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::HookLogsController < Admin::ApplicationController
include HooksExecution
include ::Integrations::HooksExecution
before_action :hook, only: [:show, :retry]
before_action :hook_log, only: [:show, :retry]

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::HooksController < Admin::ApplicationController
include HooksExecution
include ::Integrations::HooksExecution
before_action :hook_logs, only: :edit

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::IntegrationsController < Admin::ApplicationController
include IntegrationsActions
include ::Integrations::Actions
before_action :not_found, unless: -> { instance_level_integrations? }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module IntegrationsActions
module Integrations::Actions
extend ActiveSupport::Concern
included do

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module HooksExecution
module Integrations::HooksExecution
extend ActiveSupport::Concern
private

View File

@ -68,6 +68,15 @@ class Explore::ProjectsController < Explore::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
def topic
load_topic
return render_404 unless @topic
params[:topic] = @topic.name
@projects = load_projects
end
private
def load_project_counts
@ -86,6 +95,10 @@ class Explore::ProjectsController < Explore::ApplicationController
prepare_projects_for_rendering(projects)
end
def load_topic
@topic = Projects::Topic.find_by_name(params[:topic_name])
end
# rubocop: disable CodeReuse/ActiveRecord
def preload_associations(projects)
projects.includes(:route, :creator, :group, :project_feature, :topics, namespace: [:route, :owner])

View File

@ -3,7 +3,7 @@
module Groups
module Settings
class IntegrationsController < Groups::ApplicationController
include IntegrationsActions
include ::Integrations::Actions
before_action :authorize_admin_group!

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Projects::HookLogsController < Projects::ApplicationController
include HooksExecution
include ::Integrations::HooksExecution
before_action :authorize_admin_project!

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Projects::HooksController < Projects::ApplicationController
include HooksExecution
include ::Integrations::HooksExecution
# Authorize
before_action :authorize_admin_project!

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Security
module LatestPipelineInformation
private
def scanner_enabled?(scan_type)
latest_builds_reports.include?(scan_type)
end
def latest_builds_reports(only_successful_builds: false)
strong_memoize("latest_builds_reports_#{only_successful_builds}") do
builds = latest_security_builds
builds = builds.select { |build| build.status == 'success' } if only_successful_builds
builds.flat_map do |build|
build.options[:artifacts][:reports].keys
end
end
end
def latest_security_builds
return [] unless latest_default_branch_pipeline
::Security::SecurityJobsFinder.new(pipeline: latest_default_branch_pipeline).execute +
::Security::LicenseComplianceJobsFinder.new(pipeline: latest_default_branch_pipeline).execute
end
def latest_default_branch_pipeline
strong_memoize(:pipeline) { latest_pipeline }
end
def auto_devops_source?
latest_default_branch_pipeline&.auto_devops_source?
end
end
end

View File

@ -0,0 +1,31 @@
- @hide_top_links = false
- @no_container = true
- page_title @topic.name, _("Topics")
- max_topic_name_length = 50
= render_dashboard_ultimate_trial(current_user)
.gl-text-center.gl-bg-gray-10.gl-pb-2.gl-pt-6
.gl-pb-5.gl-align-items-center.gl-justify-content-center.gl-display-flex
.avatar-container.s60.gl-flex-shrink-0
= topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s60')
- if @topic.name.length > max_topic_name_length
%h1.gl-mt-3.str-truncated.has-tooltip{ title: @topic.name }
= truncate(@topic.name, length: max_topic_name_length)
- else
%h1.gl-mt-3
= @topic.name
- if @topic.description.present?
.topic-description.gl-ml-4.gl-mr-4
= markdown(@topic.description)
%div{ class: container_class }
.gl-py-5.gl-border-gray-100.gl-border-b-solid.gl-border-b-1
%h3.gl-m-0= _('Projects with this topic')
.top-area.gl-pt-2.gl-pb-2
.nav-controls
= render 'shared/projects/search_form'
= render 'shared/projects/dropdown'
= render 'filter'
= render 'projects', projects: @projects

View File

@ -8,7 +8,7 @@
= sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- project.topics_to_show.each do |topic|
- explore_project_topic_path = explore_projects_path(topic: topic)
- explore_project_topic_path = topic_explore_projects_path(topic_name: topic)
- if topic.length > max_project_topic_length
%a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
= truncate(topic, length: max_project_topic_length)
@ -21,7 +21,7 @@
- content = capture do
%span.gl-display-inline-flex.gl-flex-wrap
- project.topics_not_shown.each do |topic|
- explore_project_topic_path = explore_projects_path(topic: topic)
- explore_project_topic_path = topic_explore_projects_path(topic_name: topic)
- if topic.length > max_project_topic_length
%a{ class: "#{ project_topics_classes } gl-mb-3 str-truncated has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
= truncate(topic, length: max_project_topic_length)

View File

@ -5,6 +5,7 @@ namespace :explore do
collection do
get :trending
get :starred
get 'topics/:topic_name', action: :topic, as: :topic, constraints: { topic_name: /.+/ }
end
end

View File

@ -38,7 +38,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
namespace :security do
resource :configuration, only: [:show], controller: :configuration
resource :configuration, only: [:show], controller: :configuration do
resource :sast, only: [:show], controller: :sast_configuration
end
end
resources :artifacts, only: [:index, :destroy]

View File

@ -533,6 +533,7 @@ Plan.default.actual_limits.update!(pages_file_entries: 100)
The total number of registered runners is limited at the group and project levels. Each time a new runner is registered,
GitLab checks these limits against runners that have been active in the last 3 months.
A runner's registration fails if it exceeds the limit for the scope determined by the runner registration token.
If the limit value is set to zero, the limit is disabled.
- GitLab SaaS subscribers have different limits defined per plan, affecting all projects using that plan.
- Self-managed GitLab Premium and Ultimate limits are defined by a default plan that affects all projects:

View File

@ -1034,6 +1034,27 @@ Input type: `ConfigureSecretDetectionInput`
| <a id="mutationconfiguresecretdetectionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationconfiguresecretdetectionsuccesspath"></a>`successPath` | [`String`](#string) | Redirect path to use when the response is successful. |
### `Mutation.corpusCreate`
Available only when feature flag `corpus_management` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
Input type: `CorpusCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcorpuscreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcorpuscreatefullpath"></a>`fullPath` | [`ID!`](#id) | Project the corpus belongs to. |
| <a id="mutationcorpuscreatepackageid"></a>`packageId` | [`PackagesPackageID!`](#packagespackageid) | ID of the corpus package. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcorpuscreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcorpuscreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.createAlertIssue`
Input type: `CreateAlertIssueInput`

View File

@ -422,22 +422,36 @@ configurations. Local configurations in the `.gitlab-ci.yml` file override inclu
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/284883) in GitLab 13.8.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/294294) in GitLab 13.9.
> - [Support for project, group, and instance variables added](https://gitlab.com/gitlab-org/gitlab/-/issues/219065) in GitLab 14.2.
> - [Support for pipeline variables added](https://gitlab.com/gitlab-org/gitlab/-/issues/337633) in GitLab 14.5.
In `include` sections in your `.gitlab-ci.yml` file, you can use:
- `$CI_COMMIT_REF_NAME` [predefined variable](../variables/predefined_variables.md) in GitLab 14.2
and later.
- When used in `include`, the `CI_COMMIT_REF_NAME` variable returns the full
ref path, like `refs/heads/branch-name`. In other cases, this variable returns only
the branch name, like `branch-name`.
To use `CI_COMMIT_REF_NAME` in `include:rules`, you might need to use `if: $CI_COMMIT_REF_NAME =~ /main/`
(not `== main`). [An issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/337633)
to align `CI_COMMIT_REF_NAME` behavior in all cases.
- [Project variables](../variables/index.md#add-a-cicd-variable-to-a-project)
- [Group variables](../variables/index.md#add-a-cicd-variable-to-a-group)
- [Instance variables](../variables/index.md#add-a-cicd-variable-to-an-instance)
- Project [predefined variables](../variables/predefined_variables.md).
- Project [predefined variables](../variables/predefined_variables.md)
- In GitLab 14.2 and later, the `$CI_COMMIT_REF_NAME` [predefined variable](../variables/predefined_variables.md).
When used in `include`, the `CI_COMMIT_REF_NAME` variable returns the full
ref path, like `refs/heads/branch-name`. In `include:rules`, you might need to use
`if: $CI_COMMIT_REF_NAME =~ /main/` (not `== main`). This behavior is resolved in GitLab 14.5.
In GitLab 14.5 and later, you can also use:
- [Trigger variables](../triggers/index.md#making-use-of-trigger-variables).
- [Scheduled pipeline variables](../pipelines/schedules.md#using-variables).
- [Manual pipeline run variables](../variables/index.md#override-a-variable-when-running-a-pipeline-manually).
- Pipeline [predefined variables](../variables/predefined_variables.md).
YAML files are parsed before the pipeline is created, so the following pipeline predefined variables
are **not** available:
- `CI_PIPELINE_ID`
- `CI_PIPELINE_URL`
- `CI_PIPELINE_IID`
- `CI_PIPELINE_CREATED_AT`
For example:
```yaml
include:
@ -448,9 +462,6 @@ include:
For an example of how you can include these predefined variables, and the variables' impact on CI/CD jobs,
see this [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos).
There is a [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/337633)
that proposes expanding this feature to support more variables.
#### `rules` with `include`
> - Introduced in GitLab 14.2 [with a flag](../../administration/feature_flags.md) named `ci_include_rules`. Disabled by default.
@ -468,6 +479,9 @@ include:
- local: builds.yml
rules:
- if: '$INCLUDE_BUILDS == "true"'
- local: deploys.yml
rules:
- if: $CI_COMMIT_BRANCH == "main"
test:
stage: test

View File

@ -74,6 +74,13 @@ Possible version history entries are:
> - [Generally available](issue-link) in GitLab X.Y. [Feature flag <flag name>](issue-link) removed.
```
You can combine entries if they happened in the same release:
```markdown
> - Introduced in GitLab 14.2 [with a flag](../../administration/feature_flags.md) named `ci_include_rules`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/337507) in GitLab 14.3.
```
## Feature flag documentation examples
The following examples show the progression of a feature flag.

View File

@ -128,5 +128,7 @@ To remove a blocked IP:
keys *rack::attack*
```
By default, the [`keys` command is disabled](https://docs.gitlab.com/omnibus/settings/redis.html#renamed-commands).
1. Optionally, add [the IP to the allowlist](https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-rack-attack)
to prevent it being denylisted again.

View File

@ -125,7 +125,7 @@ is rejected.
NOTE:
The repository size limit includes repository files and LFS, but does not include artifacts, uploads,
wiki, packages, or snippets.
wiki, packages, or snippets. The repository size limit applies to both private and public projects.
For details on manually purging files, see [reducing the repository size using Git](../../project/repository/reducing_the_repo_size_using_git.md).

View File

@ -158,7 +158,7 @@ If you are near or over the repository size limit, you can either
NOTE:
`git push` and GitLab project imports are limited to 5 GB per request through
Cloudflare. Git LFS and imports other than a file upload are not affected by
this limit.
this limit. Repository limits apply to both public and private projects.
## IP range

View File

@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Custom group-level project templates **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.6.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6861) in GitLab 11.6.
[Group owners](../permissions.md#group-members-permissions) can set a subgroup to
be the source of project templates that are selectable when a new project is created

View File

@ -19,23 +19,23 @@ Using GitLab Group Migration, you can migrate existing top-level groups from Git
The following resources are migrated to the target instance:
- Groups ([Introduced in 13.7](https://gitlab.com/groups/gitlab-org/-/epics/4374))
- Groups ([Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4374) in 13.7)
- description
- attributes
- subgroups
- avatar ([Introduced in 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/322904))
- Group Labels ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/292429))
- avatar ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322904) in 14.0)
- Group Labels ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292429) in 13.9)
- title
- description
- color
- created_at ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/300007))
- updated_at ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/300007))
- Members ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/299415))
- created_at ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300007) in 13.10)
- updated_at ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300007) in 13.10)
- Members ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299415) in 13.9)
Group members are associated with the imported group if:
- The user already exists in the target GitLab instance and
- The user has a public email in the source GitLab instance that matches a
confirmed email in the target GitLab instance
- Epics ([Introduced in 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/250281))
- Epics ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250281) in 13.7)
- title
- description
- state (open / closed)
@ -43,12 +43,12 @@ The following resources are migrated to the target instance:
- due date
- epic order on boards
- confidentiality
- labels ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297460))
- author ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/298745))
- parent epic ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297459))
- emoji award ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297466))
- events ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/297465))
- Milestones ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/292427))
- labels ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/297460) in 13.9)
- author ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/298745) in 13.9)
- parent epic ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/297459) in 13.9)
- emoji award ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/297466) in 13.9)
- events ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/297465) in 13.10)
- Milestones ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292427) in 13.10)
- title
- description
- state (active / closed)
@ -56,8 +56,8 @@ The following resources are migrated to the target instance:
- due date
- created at
- updated at
- iid ([Introduced in 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/326157))
- Iterations ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/292428))
- iid ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326157) in 13.11)
- Iterations ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292428) in 13.10)
- iid
- title
- description
@ -66,7 +66,7 @@ The following resources are migrated to the target instance:
- due date
- created at
- updated at
- Badges ([Introduced in 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/292431))
- Badges ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292431) in 13.11)
- name
- link URL
- image URL

View File

@ -51,8 +51,8 @@ The following items are exported:
- Subgroups (including all the aforementioned data)
- Epics
- Events
- [Wikis](../../project/wiki/group.md) **(PREMIUM SELF)**
(Introduced in [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53247))
- [Wikis](../../project/wiki/group.md)
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53247) in GitLab 13.9)
The following items are **not** exported:

View File

@ -135,11 +135,11 @@ The Package Registry supports the following formats:
| Package type | GitLab version | Status |
| ------------ | -------------- |------- |
| [Maven](../maven_repository/index.md) | 11.3+ | Stable |
| [npm](../npm_registry/index.md) | 11.7+ | Stable |
| [NuGet](../nuget_repository/index.md) | 12.8+ | Stable |
| [PyPI](../pypi_repository/index.md) | 12.10+ | Stable |
| [Generic packages](../generic_packages/index.md) | 13.5+ | Stable |
| [Maven](../maven_repository/index.md) | 11.3+ | GA |
| [npm](../npm_registry/index.md) | 11.7+ | GA |
| [NuGet](../nuget_repository/index.md) | 12.8+ | GA |
| [PyPI](../pypi_repository/index.md) | 12.10+ | GA |
| [Generic packages](../generic_packages/index.md) | 13.5+ | GA |
| [Composer](../composer_repository/index.md) | 13.2+ | [Beta](https://gitlab.com/groups/gitlab-org/-/epics/6817) |
| [Conan](../conan_repository/index.md) | 12.6+ | [Beta](https://gitlab.com/groups/gitlab-org/-/epics/6816) |
| [Helm](../helm_repository/index.md) | 14.1+ | [Beta](https://gitlab.com/groups/gitlab-org/-/epics/6366) |
@ -147,11 +147,11 @@ The Package Registry supports the following formats:
| [Go](../go_proxy/index.md) | 13.1+ | [Alpha](https://gitlab.com/groups/gitlab-org/-/epics/3043) |
| [Ruby gems](../rubygems_registry/index.md) | 13.10+ | [Alpha](https://gitlab.com/groups/gitlab-org/-/epics/3200) |
Status:
[Status](https://about.gitlab.com/handbook/product/gitlab-the-product/#generally-available-ga):
- Alpha: behind a feature flag and not officially supported.
- Beta: several known issues that may prevent expected use.
- Stable: ready for production use.
- GA (Generally Available): ready for production use at any scale.
You can also use the [API](../../../api/packages.md) to administer the Package Registry.

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module BulkImports
module Projects
module Pipelines
class ExternalPullRequestsPipeline
include NdjsonPipeline
relation_name 'external_pull_requests'
extractor ::BulkImports::Common::Extractors::NdjsonExtractor, relation: relation
end
end
end
end

View File

@ -31,6 +31,10 @@ module BulkImports
pipeline: BulkImports::Projects::Pipelines::MergeRequestsPipeline,
stage: 4
},
external_pull_requests: {
pipeline: BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline,
stage: 4
},
uploads: {
pipeline: BulkImports::Common::Pipelines::UploadsPipeline,
stage: 5

View File

@ -19,11 +19,12 @@ module Gitlab
attr_reader :root, :context, :source_ref_path, :source
def initialize(config, project: nil, sha: nil, user: nil, parent_pipeline: nil, source_ref_path: nil, source: nil)
@context = build_context(project: project, sha: sha, user: user, parent_pipeline: parent_pipeline, ref: source_ref_path)
def initialize(config, project: nil, pipeline: nil, sha: nil, user: nil, parent_pipeline: nil, source: nil)
@source_ref_path = pipeline&.source_ref_path
@context = build_context(project: project, pipeline: pipeline, sha: sha, user: user, parent_pipeline: parent_pipeline)
@context.set_deadline(TIMEOUT_SECONDS)
@source_ref_path = source_ref_path
@source = source
@config = expand_config(config)
@ -108,16 +109,16 @@ module Gitlab
end
end
def build_context(project:, sha:, user:, parent_pipeline:, ref:)
def build_context(project:, pipeline:, sha:, user:, parent_pipeline:)
Config::External::Context.new(
project: project,
sha: sha || find_sha(project),
user: user,
parent_pipeline: parent_pipeline,
variables: build_variables(project: project, ref: ref))
variables: build_variables(project: project, pipeline: pipeline))
end
def build_variables(project:, ref:)
def build_variables(project:, pipeline:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless project
@ -126,18 +127,12 @@ module Gitlab
#
# See more detail in the docs: https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
variables.concat(project.predefined_variables)
variables.concat(pipeline_predefined_variables(ref: ref))
variables.concat(project.ci_instance_variables_for(ref: ref))
variables.concat(project.group.ci_variables_for(ref, project)) if project.group
variables.concat(project.ci_variables_for(ref: ref))
end
end
# https://gitlab.com/gitlab-org/gitlab/-/issues/337633 aims to add all predefined variables
# to this list, but only CI_COMMIT_REF_NAME is available right now to support compliance pipelines.
def pipeline_predefined_variables(ref:)
Gitlab::Ci::Variables::Collection.new.tap do |v|
v.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.concat(pipeline.predefined_variables) if pipeline
variables.concat(project.ci_instance_variables_for(ref: source_ref_path))
variables.concat(project.group.ci_variables_for(source_ref_path, project)) if project.group
variables.concat(project.ci_variables_for(ref: source_ref_path))
variables.concat(pipeline.variables) if pipeline
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline&.pipeline_schedule
end
end

View File

@ -14,7 +14,7 @@ module Gitlab
result = ::Gitlab::Ci::YamlProcessor.new(
@command.config_content, {
project: project,
source_ref_path: @pipeline.source_ref_path,
pipeline: @pipeline,
sha: @pipeline.sha,
source: @pipeline.source,
user: current_user,

View File

@ -0,0 +1,274 @@
# frozen_string_literal: true
module Gitlab
module ImportExport
module Group
class RelationTreeRestorer
def initialize( # rubocop:disable Metrics/ParameterLists
user:,
shared:,
relation_reader:,
members_mapper:,
object_builder:,
relation_factory:,
reader:,
importable:,
importable_attributes:,
importable_path:
)
@user = user
@shared = shared
@importable = importable
@relation_reader = relation_reader
@members_mapper = members_mapper
@object_builder = object_builder
@relation_factory = relation_factory
@reader = reader
@importable_attributes = importable_attributes
@importable_path = importable_path
end
def restore
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
update_params!
BulkInsertableAssociations.with_bulk_insert(enabled: bulk_insert_enabled) do
fix_ci_pipelines_not_sorted_on_legacy_project_json!
create_relations!
end
end
end
# ensure that we have latest version of the restore
@importable.reload # rubocop:disable Cop/ActiveRecordAssociationReload
true
rescue StandardError => e
@shared.error(e)
false
end
private
def bulk_insert_enabled
false
end
# Loops through the tree of models defined in import_export.yml and
# finds them in the imported JSON so they can be instantiated and saved
# in the DB. The structure and relationships between models are guessed from
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project/group.
def create_relations!
relations.each do |relation_key, relation_definition|
process_relation!(relation_key, relation_definition)
end
end
def process_relation!(relation_key, relation_definition)
@relation_reader.consume_relation(@importable_path, relation_key).each do |data_hash, relation_index|
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
end
end
def process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
relation_object = build_relation(relation_key, relation_definition, relation_index, data_hash)
return unless relation_object
return if relation_invalid_for_importable?(relation_object)
relation_object.assign_attributes(importable_class_sym => @importable)
import_failure_service.with_retry(action: 'relation_object.save!', relation_key: relation_key, relation_index: relation_index) do
relation_object.save!
log_relation_creation(@importable, relation_key, relation_object)
end
rescue StandardError => e
import_failure_service.log_import_failure(
source: 'process_relation_item!',
relation_key: relation_key,
relation_index: relation_index,
exception: e)
end
def import_failure_service
@import_failure_service ||= ImportFailureService.new(@importable)
end
def relations
@relations ||=
@reader
.attributes_finder
.find_relations_tree(importable_class_sym)
.deep_stringify_keys
end
def update_params!
params = @importable_attributes.except(*relations.keys.map(&:to_s))
params = params.merge(present_override_params)
# Cleaning all imported and overridden params
params = Gitlab::ImportExport::AttributeCleaner.clean(
relation_hash: params,
relation_class: importable_class,
excluded_keys: excluded_keys_for_relation(importable_class_sym))
@importable.assign_attributes(params)
modify_attributes
Gitlab::Timeless.timeless(@importable) do
@importable.save!
end
end
def present_override_params
# we filter out the empty strings from the overrides
# keeping the default values configured
override_params&.transform_values do |value|
value.is_a?(String) ? value.presence : value
end&.compact
end
def override_params
@importable_override_params ||= importable_override_params
end
def importable_override_params
if @importable.respond_to?(:import_data)
@importable.import_data&.data&.fetch('override_params', nil) || {}
else
{}
end
end
def modify_attributes
# no-op to be overridden on inheritance
end
def build_relations(relation_key, relation_definition, relation_index, data_hashes)
data_hashes
.map { |data_hash| build_relation(relation_key, relation_definition, relation_index, data_hash) }
.tap { |entries| entries.compact! }
end
def build_relation(relation_key, relation_definition, relation_index, data_hash)
# TODO: This is hack to not create relation for the author
# Rather make `RelationFactory#set_note_author` to take care of that
return data_hash if relation_key == 'author' || already_restored?(data_hash)
# create relation objects recursively for all sub-objects
relation_definition.each do |sub_relation_key, sub_relation_definition|
transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
end
relation = @relation_factory.create(**relation_factory_params(relation_key, relation_index, data_hash))
if relation && !relation.valid?
@shared.logger.warn(
message: "[Project/Group Import] Invalid object relation built",
relation_key: relation_key,
relation_index: relation_index,
relation_class: relation.class.name,
error_messages: relation.errors.full_messages.join(". ")
)
end
relation
end
# Since we update the data hash in place as we restore relation items,
# and since we also de-duplicate items, we might encounter items that
# have already been restored in a previous iteration.
def already_restored?(relation_item)
!relation_item.is_a?(Hash)
end
def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
sub_data_hash = data_hash[sub_relation_key]
return unless sub_data_hash
# if object is a hash we can create simple object
# as it means that this is 1-to-1 vs 1-to-many
current_item =
if sub_data_hash.is_a?(Array)
build_relations(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash).presence
else
build_relation(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash)
end
if current_item
data_hash[sub_relation_key] = current_item
else
data_hash.delete(sub_relation_key)
end
end
def relation_invalid_for_importable?(_relation_object)
false
end
def excluded_keys_for_relation(relation)
@reader.attributes_finder.find_excluded_keys(relation)
end
def importable_class
@importable.class
end
def importable_class_sym
importable_class.to_s.downcase.to_sym
end
def relation_factory_params(relation_key, relation_index, data_hash)
{
relation_index: relation_index,
relation_sym: relation_key.to_sym,
relation_hash: data_hash,
importable: @importable,
members_mapper: @members_mapper,
object_builder: @object_builder,
user: @user,
excluded_keys: excluded_keys_for_relation(relation_key)
}
end
# Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
# This should be removed once legacy JSON format is deprecated.
# Ndjson export file will fix the order during project export.
def fix_ci_pipelines_not_sorted_on_legacy_project_json!
return unless @relation_reader.legacy?
@relation_reader.sort_ci_pipelines_by_id
end
# Enable logging of each top-level relation creation when Importing
# into a Group if feature flag is enabled
def log_relation_creation(importable, relation_key, relation_object)
root_ancestor_group = importable.try(:root_ancestor)
return unless root_ancestor_group
return unless root_ancestor_group.instance_of?(::Group)
return unless Feature.enabled?(:log_import_export_relation_creation, root_ancestor_group)
@shared.logger.info(
importable_type: importable.class.to_s,
importable_id: importable.id,
relation_key: relation_key,
relation_id: relation_object.id,
author_id: relation_object.try(:author_id),
message: '[Project/Group Import] Created new object relation'
)
end
end
end
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class RelationTreeRestorer < ImportExport::Group::RelationTreeRestorer
# Relations which cannot be saved at project level (and have a group assigned)
GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze
private
def bulk_insert_enabled
true
end
def modify_attributes
@importable.reconcile_shared_runners_setting!
@importable.drop_visibility_level!
end
def relation_invalid_for_importable?(relation_object)
GROUP_MODELS.include?(relation_object.class) && relation_object.group_id
end
end
end
end
end

View File

@ -4,7 +4,7 @@ module Gitlab
module ImportExport
module Project
module Sample
class RelationTreeRestorer < ImportExport::RelationTreeRestorer
class RelationTreeRestorer < ImportExport::Project::RelationTreeRestorer
def initialize(...)
super(...)
@ -18,10 +18,10 @@ module Gitlab
end
def dates
return [] if relation_reader.legacy?
return [] if @relation_reader.legacy?
RelationFactory::DATE_MODELS.flat_map do |tag|
relation_reader.consume_relation(@importable_path, tag, mark_as_consumed: false).map do |model|
@relation_reader.consume_relation(@importable_path, tag, mark_as_consumed: false).map do |model|
model.first['due_date']
end
end

View File

@ -1,280 +0,0 @@
# frozen_string_literal: true
module Gitlab
module ImportExport
class RelationTreeRestorer
# Relations which cannot be saved at project level (and have a group assigned)
GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze
attr_reader :user
attr_reader :shared
attr_reader :importable
attr_reader :relation_reader
def initialize( # rubocop:disable Metrics/ParameterLists
user:, shared:, relation_reader:,
members_mapper:, object_builder:,
relation_factory:,
reader:,
importable:,
importable_attributes:,
importable_path:
)
@user = user
@shared = shared
@importable = importable
@relation_reader = relation_reader
@members_mapper = members_mapper
@object_builder = object_builder
@relation_factory = relation_factory
@reader = reader
@importable_attributes = importable_attributes
@importable_path = importable_path
end
def restore
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
update_params!
BulkInsertableAssociations.with_bulk_insert(enabled: project?) do
fix_ci_pipelines_not_sorted_on_legacy_project_json!
create_relations!
end
end
end
# ensure that we have latest version of the restore
@importable.reload # rubocop:disable Cop/ActiveRecordAssociationReload
true
rescue StandardError => e
@shared.error(e)
false
end
private
def project?
@importable.instance_of?(::Project)
end
# Loops through the tree of models defined in import_export.yml and
# finds them in the imported JSON so they can be instantiated and saved
# in the DB. The structure and relationships between models are guessed from
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project/group.
def create_relations!
relations.each do |relation_key, relation_definition|
process_relation!(relation_key, relation_definition)
end
end
def process_relation!(relation_key, relation_definition)
@relation_reader.consume_relation(@importable_path, relation_key).each do |data_hash, relation_index|
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
end
end
def process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
relation_object = build_relation(relation_key, relation_definition, relation_index, data_hash)
return unless relation_object
return if project? && group_model?(relation_object)
relation_object.assign_attributes(importable_class_sym => @importable)
import_failure_service.with_retry(action: 'relation_object.save!', relation_key: relation_key, relation_index: relation_index) do
relation_object.save!
log_relation_creation(@importable, relation_key, relation_object)
end
rescue StandardError => e
import_failure_service.log_import_failure(
source: 'process_relation_item!',
relation_key: relation_key,
relation_index: relation_index,
exception: e)
end
def import_failure_service
@import_failure_service ||= ImportFailureService.new(@importable)
end
def relations
@relations ||=
@reader
.attributes_finder
.find_relations_tree(importable_class_sym)
.deep_stringify_keys
end
def update_params!
params = @importable_attributes.except(*relations.keys.map(&:to_s))
params = params.merge(present_override_params)
# Cleaning all imported and overridden params
params = Gitlab::ImportExport::AttributeCleaner.clean(
relation_hash: params,
relation_class: importable_class,
excluded_keys: excluded_keys_for_relation(importable_class_sym))
@importable.assign_attributes(params)
modify_attributes
Gitlab::Timeless.timeless(@importable) do
@importable.save!
end
end
def present_override_params
# we filter out the empty strings from the overrides
# keeping the default values configured
override_params&.transform_values do |value|
value.is_a?(String) ? value.presence : value
end&.compact
end
def override_params
@importable_override_params ||= importable_override_params
end
def importable_override_params
if @importable.respond_to?(:import_data)
@importable.import_data&.data&.fetch('override_params', nil) || {}
else
{}
end
end
def modify_attributes
return unless project?
@importable.reconcile_shared_runners_setting!
@importable.drop_visibility_level!
end
def build_relations(relation_key, relation_definition, relation_index, data_hashes)
data_hashes
.map { |data_hash| build_relation(relation_key, relation_definition, relation_index, data_hash) }
.tap { |entries| entries.compact! }
end
def build_relation(relation_key, relation_definition, relation_index, data_hash)
# TODO: This is hack to not create relation for the author
# Rather make `RelationFactory#set_note_author` to take care of that
return data_hash if relation_key == 'author' || already_restored?(data_hash)
# create relation objects recursively for all sub-objects
relation_definition.each do |sub_relation_key, sub_relation_definition|
transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
end
relation = @relation_factory.create(**relation_factory_params(relation_key, relation_index, data_hash))
if relation && !relation.valid?
@shared.logger.warn(
message: "[Project/Group Import] Invalid object relation built",
relation_key: relation_key,
relation_index: relation_index,
relation_class: relation.class.name,
error_messages: relation.errors.full_messages.join(". ")
)
end
relation
end
# Since we update the data hash in place as we restore relation items,
# and since we also de-duplicate items, we might encounter items that
# have already been restored in a previous iteration.
def already_restored?(relation_item)
!relation_item.is_a?(Hash)
end
def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition, relation_index)
sub_data_hash = data_hash[sub_relation_key]
return unless sub_data_hash
# if object is a hash we can create simple object
# as it means that this is 1-to-1 vs 1-to-many
current_item =
if sub_data_hash.is_a?(Array)
build_relations(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash).presence
else
build_relation(
sub_relation_key,
sub_relation_definition,
relation_index,
sub_data_hash)
end
if current_item
data_hash[sub_relation_key] = current_item
else
data_hash.delete(sub_relation_key)
end
end
def group_model?(relation_object)
GROUP_MODELS.include?(relation_object.class) && relation_object.group_id
end
def excluded_keys_for_relation(relation)
@reader.attributes_finder.find_excluded_keys(relation)
end
def importable_class
@importable.class
end
def importable_class_sym
importable_class.to_s.downcase.to_sym
end
def relation_factory_params(relation_key, relation_index, data_hash)
{
relation_index: relation_index,
relation_sym: relation_key.to_sym,
relation_hash: data_hash,
importable: @importable,
members_mapper: @members_mapper,
object_builder: @object_builder,
user: @user,
excluded_keys: excluded_keys_for_relation(relation_key)
}
end
# Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
# This should be removed once legacy JSON format is deprecated.
# Ndjson export file will fix the order during project export.
def fix_ci_pipelines_not_sorted_on_legacy_project_json!
return unless relation_reader.legacy?
relation_reader.sort_ci_pipelines_by_id
end
# Enable logging of each top-level relation creation when Importing
# into a Group if feature flag is enabled
def log_relation_creation(importable, relation_key, relation_object)
root_ancestor_group = importable.try(:root_ancestor)
return unless root_ancestor_group
return unless root_ancestor_group.instance_of?(::Group)
return unless Feature.enabled?(:log_import_export_relation_creation, root_ancestor_group)
@shared.logger.info(
importable_type: importable.class.to_s,
importable_id: importable.id,
relation_key: relation_key,
relation_id: relation_object.id,
author_id: relation_object.try(:author_id),
message: '[Project/Group Import] Created new object relation'
)
end
end
end
end

View File

@ -25525,9 +25525,6 @@ msgstr ""
msgid "Pipeline|In progress"
msgstr ""
msgid "Pipeline|Key"
msgstr ""
msgid "Pipeline|Manual"
msgstr ""
@ -25618,9 +25615,6 @@ msgstr ""
msgid "Pipeline|Triggerer"
msgstr ""
msgid "Pipeline|Value"
msgstr ""
msgid "Pipeline|Variables"
msgstr ""
@ -27391,6 +27385,9 @@ msgstr ""
msgid "Projects with no vulnerabilities and security scanning enabled"
msgstr ""
msgid "Projects with this topic"
msgstr ""
msgid "Projects with write access"
msgstr ""

View File

@ -104,7 +104,7 @@
"apollo-upload-client": "^13.0.0",
"autosize": "^5.0.1",
"aws-sdk": "^2.637.0",
"axios": "^0.20.0",
"axios": "^0.24.0",
"babel-loader": "^8.2.2",
"babel-plugin-lodash": "^3.3.4",
"bootstrap": "4.5.3",
@ -219,8 +219,8 @@
"docdash": "^1.0.2",
"eslint": "7.32.0",
"eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.1",
"eslint-plugin-no-jquery": "2.6.0",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-no-jquery": "2.7.0",
"gettext-extractor": "^3.5.3",
"gettext-extractor-vue": "^5.0.0",
"glob": "^7.1.6",

View File

@ -9,7 +9,7 @@ RSpec.describe Admin::IntegrationsController do
sign_in(admin)
end
it_behaves_like IntegrationsActions do
it_behaves_like Integrations::Actions do
let(:integration_attributes) { { instance: true, project: nil } }
let(:routing_params) do

View File

@ -74,6 +74,28 @@ RSpec.describe Explore::ProjectsController do
end
end
end
describe 'GET #topic' do
context 'when topic does not exist' do
it 'renders a 404 error' do
get :topic, params: { topic_name: 'topic1' }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when topic exists' do
before do
create(:topic, name: 'topic1')
end
it 'renders the template' do
get :topic, params: { topic_name: 'topic1' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('topic')
end
end
end
end
shared_examples "blocks high page numbers" do

View File

@ -10,7 +10,7 @@ RSpec.describe Groups::Settings::IntegrationsController do
sign_in(user)
end
it_behaves_like IntegrationsActions do
it_behaves_like Integrations::Actions do
let(:integration_attributes) { { group: group, project: nil } }
let(:routing_params) do

View File

@ -18,7 +18,7 @@ RSpec.describe Projects::ServicesController do
project.add_maintainer(user)
end
it_behaves_like IntegrationsActions do
it_behaves_like Integrations::Actions do
let(:integration_attributes) { { project: project } }
let(:routing_params) do

View File

@ -204,7 +204,7 @@ RSpec.describe 'Dashboard Projects' do
visit dashboard_projects_path
expect(page).to have_selector('[data-testid="project_topic_list"]')
expect(page).to have_link('topic1', href: explore_projects_path(topic: 'topic1'))
expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User triggers manual job with variables', :js do
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
let!(:build) { create(:ci_build, :manual, pipeline: pipeline) }
before do
project.add_maintainer(user)
project.enable_ci
sign_in(user)
visit(project_job_path(project, build))
end
it 'passes values correctly' do
page.within(find("[data-testid='ci-variable-row']")) do
find("[data-testid='ci-variable-key']").set('key_name')
find("[data-testid='ci-variable-value']").set('key_value')
end
find("[data-testid='trigger-manual-job-btn']").click
wait_for_requests
expect(build.job_variables.as_json).to contain_exactly(
hash_including('key' => 'key_name', 'value' => 'key_value'))
end
end

View File

@ -133,7 +133,7 @@ RSpec.describe 'Project' do
visit path
expect(page).to have_selector('[data-testid="project_topic_list"]')
expect(page).to have_link('topic1', href: explore_projects_path(topic: 'topic1'))
expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
end
it 'shows up to 3 project topics' do
@ -142,9 +142,9 @@ RSpec.describe 'Project' do
visit path
expect(page).to have_selector('[data-testid="project_topic_list"]')
expect(page).to have_link('topic1', href: explore_projects_path(topic: 'topic1'))
expect(page).to have_link('topic2', href: explore_projects_path(topic: 'topic2'))
expect(page).to have_link('topic3', href: explore_projects_path(topic: 'topic3'))
expect(page).to have_link('topic1', href: topic_explore_projects_path(topic_name: 'topic1'))
expect(page).to have_link('topic2', href: topic_explore_projects_path(topic_name: 'topic2'))
expect(page).to have_link('topic3', href: topic_explore_projects_path(topic_name: 'topic3'))
expect(page).to have_content('+ 1 more')
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Topic show page' do
let_it_be(:topic) { create(:topic, name: 'my-topic', description: 'This is **my** topic https://google.com/ :poop: ```\ncode\n```', avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
context 'when topic does not exist' do
let(:path) { topic_explore_projects_path(topic_name: 'non-existing') }
it 'renders 404' do
visit path
expect(status_code).to eq(404)
end
end
context 'when topic exists' do
before do
visit topic_explore_projects_path(topic_name: topic.name)
end
it 'shows name, avatar and description as markdown' do
expect(page).to have_content(topic.name)
expect(page).to have_selector('.avatar-container > img.topic-avatar')
expect(find('.topic-description')).to have_selector('p > strong')
expect(find('.topic-description')).to have_selector('p > a[rel]')
expect(find('.topic-description')).to have_selector('p > gl-emoji')
expect(find('.topic-description')).to have_selector('p > code')
end
context 'with associated projects' do
let!(:project) { create(:project, :public, topic_list: topic.name) }
it 'shows project list' do
visit topic_explore_projects_path(topic_name: topic.name)
expect(find('.projects-list .project-name')).to have_content(project.name)
end
end
context 'without associated projects' do
it 'shows correct empty state message' do
expect(page).to have_content('Explore public groups to find projects to contribute to.')
end
end
end
end

View File

@ -4,8 +4,6 @@ import { TEST_HOST } from 'helpers/test_constants';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import axios from '~/lib/utils/axios_utils';
const { CancelToken } = axios;
describe('custom metrics form fields component', () => {
let wrapper;
let mockAxios;
@ -116,14 +114,14 @@ describe('custom metrics form fields component', () => {
it('receives and validates a persisted value', () => {
const query = 'persistedQuery';
const axiosPost = jest.spyOn(axios, 'post');
const source = CancelToken.source();
jest.spyOn(axios, 'post');
mountComponent({ metricPersisted: true, ...makeFormData({ query }) });
expect(axiosPost).toHaveBeenCalledWith(
expect(axios.post).toHaveBeenCalledWith(
validateQueryPath,
{ query },
{ cancelToken: source.token },
expect.objectContaining({ cancelToken: expect.anything() }),
);
expect(getNamedInput(queryInputName).value).toBe(query);
jest.runAllTimers();

View File

@ -1,9 +1,9 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Form from '~/jobs/components/manual_variables_form.vue';
import ManualVariablesForm from '~/jobs/components/manual_variables_form.vue';
const localVue = createLocalVue();
@ -21,7 +21,7 @@ describe('Manual Variables Form', () => {
},
};
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
const createComponent = (props = {}) => {
store = new Vuex.Store({
actions: {
triggerManualJob: jest.fn(),
@ -29,7 +29,7 @@ describe('Manual Variables Form', () => {
});
wrapper = extendedWrapper(
mountFn(localVue.extend(Form), {
mount(localVue.extend(ManualVariablesForm), {
propsData: { ...requiredProps, ...props },
localVue,
store,
@ -40,88 +40,120 @@ describe('Manual Variables Form', () => {
);
};
const findInputKey = () => wrapper.findComponent({ ref: 'inputKey' });
const findInputValue = () => wrapper.findComponent({ ref: 'inputSecretValue' });
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
const setCiVariableKey = () => {
findCiVariableKey().setValue('new key');
findCiVariableKey().vm.$emit('change');
nextTick();
};
const setCiVariableKeyByPosition = (position, value) => {
findAllCiVariableKeys().at(position).setValue(value);
findAllCiVariableKeys().at(position).vm.$emit('change');
nextTick();
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('shallowMount', () => {
beforeEach(() => {
createComponent();
});
it('creates a new variable when user enters a new key value', async () => {
expect(findAllVariables()).toHaveLength(1);
it('renders empty form with correct placeholders', () => {
expect(findInputKey().attributes('placeholder')).toBe('Input variable key');
expect(findInputValue().attributes('placeholder')).toBe('Input variable value');
});
await setCiVariableKey();
it('renders help text with provided link', () => {
expect(findHelpText().exists()).toBe(true);
expect(findHelpLink().attributes('href')).toBe(
'/help/ci/variables/index#add-a-cicd-variable-to-a-project',
);
});
describe('when adding a new variable', () => {
it('creates a new variable when user types a new key and resets the form', async () => {
await findInputKey().setValue('new key');
expect(findAllVariables()).toHaveLength(1);
expect(findCiVariableKey().element.value).toBe('new key');
expect(findInputKey().attributes('value')).toBe(undefined);
});
it('creates a new variable when user types a new value and resets the form', async () => {
await findInputValue().setValue('new value');
expect(findAllVariables()).toHaveLength(1);
expect(findCiVariableValue().element.value).toBe('new value');
expect(findInputValue().attributes('value')).toBe(undefined);
});
});
expect(findAllVariables()).toHaveLength(2);
});
describe('mount', () => {
beforeEach(() => {
createComponent({ mountFn: mount });
});
it('does not create extra empty variables', async () => {
expect(findAllVariables()).toHaveLength(1);
describe('when deleting a variable', () => {
it('removes the variable row', async () => {
await wrapper.setData({
variables: [
{
key: 'new key',
secret_value: 'value',
id: '1',
},
],
});
await setCiVariableKey();
findDeleteVarBtn().trigger('click');
expect(findAllVariables()).toHaveLength(2);
await wrapper.vm.$nextTick();
await setCiVariableKey();
expect(findAllVariables()).toHaveLength(0);
});
});
expect(findAllVariables()).toHaveLength(2);
});
it('trigger button is disabled after trigger action', async () => {
expect(findTriggerBtn().props('disabled')).toBe(false);
it('removes the correct variable row', async () => {
const variableKeyNameOne = 'key-one';
const variableKeyNameThree = 'key-three';
await findTriggerBtn().trigger('click');
await setCiVariableKeyByPosition(0, variableKeyNameOne);
expect(findTriggerBtn().props('disabled')).toBe(true);
});
await setCiVariableKeyByPosition(1, 'key-two');
await setCiVariableKeyByPosition(2, variableKeyNameThree);
expect(findAllVariables()).toHaveLength(4);
await findAllDeleteVarBtns().at(1).trigger('click');
expect(findAllVariables()).toHaveLength(3);
expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
expect(findAllCiVariableKeys().at(2).element.value).toBe('');
});
it('trigger button is disabled after trigger action', async () => {
expect(findTriggerBtn().props('disabled')).toBe(false);
await findTriggerBtn().trigger('click');
expect(findTriggerBtn().props('disabled')).toBe(true);
});
it('delete variable button should only show when there is more than one variable', async () => {
expect(findDeleteVarBtn().exists()).toBe(false);
await setCiVariableKey();
expect(findDeleteVarBtn().exists()).toBe(true);
});
it('delete variable button placeholder should only exist when a user cannot remove', async () => {
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
it('renders help text with provided link', () => {
expect(findHelpText().exists()).toBe(true);
expect(findHelpLink().attributes('href')).toBe(
'/help/ci/variables/index#add-a-cicd-variable-to-a-project',
);
});
it('passes variables in correct format', async () => {
jest.spyOn(store, 'dispatch');
await setCiVariableKey();
await findCiVariableValue().setValue('new value');
await findTriggerBtn().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [
{
key: 'new key',
secret_value: 'new value',
},
]);
});
});

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline do
let_it_be(:project) { create(:project) }
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:entity) { create(:bulk_import_entity, :project_entity, project: project, bulk_import: bulk_import) }
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:attributes) { {} }
let(:external_pr) { project.external_pull_requests.last }
let(:external_pull_request) do
{
'pull_request_iid' => 4,
'source_branch' => 'feature',
'target_branch' => 'main',
'source_repository' => 'repository',
'target_repository' => 'repository',
'source_sha' => 'abc',
'target_sha' => 'xyz',
'status' => 'open',
'created_at' => '2019-12-24T14:04:50.053Z',
'updated_at' => '2019-12-24T14:05:18.138Z'
}.merge(attributes)
end
subject(:pipeline) { described_class.new(context) }
describe '#run' do
before do
allow_next_instance_of(BulkImports::Common::Extractors::NdjsonExtractor) do |extractor|
allow(extractor).to receive(:remove_tmp_dir)
allow(extractor).to receive(:extract).and_return(BulkImports::Pipeline::ExtractedData.new(data: [[external_pull_request, 0]]))
end
pipeline.run
end
it 'imports external pull request', :aggregate_failures do
expect(external_pr.pull_request_iid).to eq(external_pull_request['pull_request_iid'])
expect(external_pr.source_branch).to eq(external_pull_request['source_branch'])
expect(external_pr.target_branch).to eq(external_pull_request['target_branch'])
expect(external_pr.status).to eq(external_pull_request['status'])
expect(external_pr.created_at).to eq(external_pull_request['created_at'])
expect(external_pr.updated_at).to eq(external_pull_request['updated_at'])
end
context 'when status is closed' do
let(:attributes) { { 'status' => 'closed' } }
it 'imports closed external pull request' do
expect(external_pr.status).to eq(attributes['status'])
end
end
context 'when from fork' do
let(:attributes) { { 'source_repository' => 'source' } }
it 'does not create external pull request' do
expect(external_pr).to be_nil
end
end
end
end

View File

@ -11,6 +11,7 @@ RSpec.describe BulkImports::Projects::Stage do
[3, BulkImports::Projects::Pipelines::IssuesPipeline],
[4, BulkImports::Common::Pipelines::BoardsPipeline],
[4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
[4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline],
[5, BulkImports::Common::Pipelines::UploadsPipeline],
[6, BulkImports::Common::Pipelines::EntityFinisher]
]

View File

@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Config do
end
let(:config) do
described_class.new(yml, project: nil, sha: nil, user: nil)
described_class.new(yml, project: nil, pipeline: nil, sha: nil, user: nil)
end
context 'when config is valid' do
@ -286,9 +286,12 @@ RSpec.describe Gitlab::Ci::Config do
end
context "when using 'include' directive" do
let(:group) { create(:group) }
let_it_be(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) }
let(:main_project) { create(:project, :repository, :public, group: group) }
let(:pipeline) { build(:ci_pipeline, project: project) }
let(:remote_location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:local_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' }
@ -327,7 +330,7 @@ RSpec.describe Gitlab::Ci::Config do
end
let(:config) do
described_class.new(gitlab_ci_yml, project: project, sha: '12345', user: user)
described_class.new(gitlab_ci_yml, project: project, pipeline: pipeline, sha: '12345', user: user)
end
before do
@ -724,7 +727,7 @@ RSpec.describe Gitlab::Ci::Config do
end
end
context "when an 'include' has rules" do
context "when an 'include' has rules with a project variable" do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
@ -751,5 +754,30 @@ RSpec.describe Gitlab::Ci::Config do
end
end
end
context "when an 'include' has rules with a pipeline variable" do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
- local: #{local_location}
rules:
- if: $CI_COMMIT_SHA == "#{project.commit.sha}"
HEREDOC
end
context 'when a pipeline is passed' do
it 'includes the file' do
expect(config.to_hash).to include(local_location_hash)
end
end
context 'when a pipeline is not passed' do
let(:pipeline) { nil }
it 'does not include the file' do
expect(config.to_hash).not_to include(local_location_hash)
end
end
end
end
end

View File

@ -0,0 +1,88 @@
# frozen_string_literal: true
# This spec is a lightweight version of:
# * project/tree_restorer_spec.rb
#
# In depth testing is being done in the above specs.
# This spec tests that restore project works
# but does not have 100% relation coverage.
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
let_it_be(:group) { create(:group) }
let_it_be(:importable) { create(:group, parent: group) }
include_context 'relation tree restorer shared context' do
let(:importable_name) { nil }
end
let(:path) { 'spec/fixtures/lib/gitlab/import_export/group_exports/no_children/group.json' }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.group_relation_names)
end
let(:reader) do
Gitlab::ImportExport::Reader.new(
shared: shared,
config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
)
end
let(:relation_tree_restorer) do
described_class.new(
user: user,
shared: shared,
relation_reader: relation_reader,
object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
members_mapper: members_mapper,
relation_factory: Gitlab::ImportExport::Group::RelationFactory,
reader: reader,
importable: importable,
importable_path: nil,
importable_attributes: attributes
)
end
subject { relation_tree_restorer.restore }
shared_examples 'logging of relations creation' do
context 'when log_import_export_relation_creation feature flag is enabled' do
before do
stub_feature_flags(log_import_export_relation_creation: group)
end
it 'logs top-level relation creation' do
expect(shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.at_least(:once)
subject
end
end
context 'when log_import_export_relation_creation feature flag is disabled' do
before do
stub_feature_flags(log_import_export_relation_creation: false)
end
it 'does not log top-level relation creation' do
expect(shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.never
subject
end
end
end
it 'restores group tree' do
expect(subject).to eq(true)
end
include_examples 'logging of relations creation'
end

View File

@ -0,0 +1,150 @@
# frozen_string_literal: true
# This spec is a lightweight version of:
# * project/tree_restorer_spec.rb
#
# In depth testing is being done in the above specs.
# This spec tests that restore project works
# but does not have 100% relation coverage.
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do
let_it_be(:importable, reload: true) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
end
include_context 'relation tree restorer shared context' do
let(:importable_name) { 'project' }
end
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:relation_tree_restorer) do
described_class.new(
user: user,
shared: shared,
relation_reader: relation_reader,
object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper,
relation_factory: Gitlab::ImportExport::Project::RelationFactory,
reader: reader,
importable: importable,
importable_path: 'project',
importable_attributes: attributes
)
end
subject { relation_tree_restorer.restore }
shared_examples 'import project successfully' do
describe 'imported project' do
it 'has the project attributes and relations', :aggregate_failures do
expect(subject).to eq(true)
project = Project.find_by_path('project')
expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.')
expect(project.labels.count).to eq(3)
expect(project.boards.count).to eq(1)
expect(project.project_feature).not_to be_nil
expect(project.custom_attributes.count).to eq(2)
expect(project.project_badges.count).to eq(2)
expect(project.snippets.count).to eq(1)
end
end
end
shared_examples 'logging of relations creation' do
context 'when log_import_export_relation_creation feature flag is enabled' do
before do
stub_feature_flags(log_import_export_relation_creation: group)
end
it 'logs top-level relation creation' do
expect(shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.at_least(:once)
subject
end
end
context 'when log_import_export_relation_creation feature flag is disabled' do
before do
stub_feature_flags(log_import_export_relation_creation: false)
end
it 'does not log top-level relation creation' do
expect(shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.never
subject
end
end
end
context 'with legacy reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.project_relation_names,
allowed_path: 'project'
)
end
let(:attributes) { relation_reader.consume_attributes('project') }
it_behaves_like 'import project successfully'
context 'with logging of relations creation' do
let_it_be(:group) { create(:group) }
let_it_be(:importable) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
end
include_examples 'logging of relations creation'
end
end
context 'with ndjson reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it_behaves_like 'import project successfully'
context 'when inside a group' do
let_it_be(:group) do
create(:group, :disabled_and_unoverridable)
end
before do
importable.update!(shared_runners_enabled: false, group: group)
end
it_behaves_like 'import project successfully'
end
end
context 'with invalid relations' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'logs the invalid relation and its errors' do
expect(shared.logger)
.to receive(:warn)
.with(
error_messages: "Title can't be blank. Title is invalid",
message: '[Project/Group Import] Invalid object relation built',
relation_class: 'ProjectLabel',
relation_index: 0,
relation_key: 'labels'
).once
relation_tree_restorer.restore
end
end
end

View File

@ -10,19 +10,26 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
include_context 'relation tree restorer shared context'
let_it_be(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
include_context 'relation tree restorer shared context' do
let(:importable_name) { 'project' }
end
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
let(:sample_data_relation_tree_restorer) do
described_class.new(
user: user,
shared: shared,
relation_reader: relation_reader,
object_builder: object_builder,
object_builder: Gitlab::ImportExport::Project::ObjectBuilder,
members_mapper: members_mapper,
relation_factory: relation_factory,
relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory,
reader: reader,
importable: importable,
importable_path: importable_path,
importable_path: 'project',
importable_attributes: attributes
)
end
@ -69,32 +76,21 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
end
end
context 'when restoring a project' do
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
let(:importable_name) { 'project' }
let(:importable_path) { 'project' }
let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Project::Sample::RelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'initializes relation_factory with date_calculator as parameter' do
expect(Gitlab::ImportExport::Project::Sample::RelationFactory).to receive(:create).with(hash_including(:date_calculator)).at_least(:once).times
it 'initializes relation_factory with date_calculator as parameter' do
expect(Gitlab::ImportExport::Project::Sample::RelationFactory).to receive(:create).with(hash_including(:date_calculator)).at_least(:once).times
subject
end
subject
end
context 'when relation tree restorer is initialized' do
it 'initializes date calculator with due dates' do
expect(Gitlab::ImportExport::Project::Sample::DateCalculator).to receive(:new).with(Array)
context 'when relation tree restorer is initialized' do
it 'initializes date calculator with due dates' do
expect(Gitlab::ImportExport::Project::Sample::DateCalculator).to receive(:new).with(Array)
sample_data_relation_tree_restorer
end
end
context 'using ndjson reader' do
it_behaves_like 'import project successfully'
sample_data_relation_tree_restorer
end
end
context 'using ndjson reader' do
it_behaves_like 'import project successfully'
end
end

View File

@ -1,184 +0,0 @@
# frozen_string_literal: true
# This spec is a lightweight version of:
# * project/tree_restorer_spec.rb
#
# In depth testing is being done in the above specs.
# This spec tests that restore project works
# but does not have 100% relation coverage.
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
include_context 'relation tree restorer shared context'
let(:relation_tree_restorer) do
described_class.new(
user: user,
shared: shared,
relation_reader: relation_reader,
object_builder: object_builder,
members_mapper: members_mapper,
relation_factory: relation_factory,
reader: reader,
importable: importable,
importable_path: importable_path,
importable_attributes: attributes
)
end
subject { relation_tree_restorer.restore }
shared_examples 'import project successfully' do
describe 'imported project' do
it 'has the project attributes and relations', :aggregate_failures do
expect(subject).to eq(true)
project = Project.find_by_path('project')
expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.')
expect(project.labels.count).to eq(3)
expect(project.boards.count).to eq(1)
expect(project.project_feature).not_to be_nil
expect(project.custom_attributes.count).to eq(2)
expect(project.project_badges.count).to eq(2)
expect(project.snippets.count).to eq(1)
end
end
end
shared_examples 'logging of relations creation' do
context 'when log_import_export_relation_creation feature flag is enabled' do
before do
stub_feature_flags(log_import_export_relation_creation: group)
end
it 'logs top-level relation creation' do
expect(relation_tree_restorer.shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.at_least(:once)
subject
end
end
context 'when log_import_export_relation_creation feature flag is disabled' do
before do
stub_feature_flags(log_import_export_relation_creation: false)
end
it 'does not log top-level relation creation' do
expect(relation_tree_restorer.shared.logger)
.to receive(:info)
.with(hash_including(message: '[Project/Group Import] Created new object relation'))
.never
subject
end
end
end
context 'when restoring a project' do
let_it_be(:importable, reload: true) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
end
let(:importable_name) { 'project' }
let(:importable_path) { 'project' }
let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
context 'using legacy reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.project_relation_names,
allowed_path: 'project'
)
end
let(:attributes) { relation_reader.consume_attributes('project') }
it_behaves_like 'import project successfully'
context 'logging of relations creation' do
let_it_be(:group) { create(:group) }
let_it_be(:importable) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
end
include_examples 'logging of relations creation'
end
end
context 'using ndjson reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it_behaves_like 'import project successfully'
context 'when inside a group' do
let_it_be(:group) do
create(:group, :disabled_and_unoverridable)
end
before do
importable.update!(shared_runners_enabled: false, group: group)
end
it_behaves_like 'import project successfully'
end
end
context 'with invalid relations' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'logs the invalid relation and its errors' do
expect(relation_tree_restorer.shared.logger)
.to receive(:warn)
.with(
error_messages: "Title can't be blank. Title is invalid",
message: '[Project/Group Import] Invalid object relation built',
relation_class: 'ProjectLabel',
relation_index: 0,
relation_key: 'labels'
).once
relation_tree_restorer.restore
end
end
end
context 'when restoring a group' do
let_it_be(:group) { create(:group) }
let_it_be(:importable) { create(:group, parent: group) }
let(:path) { 'spec/fixtures/lib/gitlab/import_export/group_exports/no_children/group.json' }
let(:importable_name) { nil }
let(:importable_path) { nil }
let(:object_builder) { Gitlab::ImportExport::Group::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Group::RelationFactory }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.group_relation_names)
end
let(:reader) do
Gitlab::ImportExport::Reader.new(
shared: shared,
config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
)
end
it 'restores group tree' do
expect(subject).to eq(true)
end
include_examples 'logging of relations creation'
end
end

View File

@ -7,9 +7,11 @@ RSpec.describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:ref) { 'refs/heads/master' }
let(:variables_attributes) { [{ key: 'MYVAR', secret_value: 'hello' }] }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref, variables_attributes: variables_attributes }) }
let(:pipeline) { service.execute(source).payload }
let(:file_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' }
@ -24,6 +26,20 @@ RSpec.describe Ci::CreatePipelineService do
.and_return(File.read(Rails.root.join(file_location)))
end
shared_examples 'not including the file' do
it 'does not include the job in the file' do
expect(pipeline).to be_created_successfully
expect(pipeline.processables.pluck(:name)).to contain_exactly('job')
end
end
shared_examples 'including the file' do
it 'includes the job in the file' do
expect(pipeline).to be_created_successfully
expect(pipeline.processables.pluck(:name)).to contain_exactly('job', 'rspec')
end
end
context 'with a local file' do
let(:config) do
<<~EOY
@ -33,13 +49,10 @@ RSpec.describe Ci::CreatePipelineService do
EOY
end
it 'includes the job in the file' do
expect(pipeline).to be_created_successfully
expect(pipeline.processables.pluck(:name)).to contain_exactly('job', 'rspec')
end
it_behaves_like 'including the file'
end
context 'with a local file with rules' do
context 'with a local file with rules with a project variable' do
let(:config) do
<<~EOY
include:
@ -54,19 +67,63 @@ RSpec.describe Ci::CreatePipelineService do
context 'when the rules matches' do
let(:project_id) { project.id }
it 'includes the job in the file' do
expect(pipeline).to be_created_successfully
expect(pipeline.processables.pluck(:name)).to contain_exactly('job', 'rspec')
end
it_behaves_like 'including the file'
end
context 'when the rules does not match' do
let(:project_id) { non_existing_record_id }
it 'does not include the job in the file' do
expect(pipeline).to be_created_successfully
expect(pipeline.processables.pluck(:name)).to contain_exactly('job')
end
it_behaves_like 'not including the file'
end
end
context 'with a local file with rules with a predefined pipeline variable' do
let(:config) do
<<~EOY
include:
- local: #{file_location}
rules:
- if: $CI_PIPELINE_SOURCE == "#{pipeline_source}"
job:
script: exit 0
EOY
end
context 'when the rules matches' do
let(:pipeline_source) { 'push' }
it_behaves_like 'including the file'
end
context 'when the rules does not match' do
let(:pipeline_source) { 'web' }
it_behaves_like 'not including the file'
end
end
context 'with a local file with rules with a run pipeline variable' do
let(:config) do
<<~EOY
include:
- local: #{file_location}
rules:
- if: $MYVAR == "#{my_var}"
job:
script: exit 0
EOY
end
context 'when the rules matches' do
let(:my_var) { 'hello' }
it_behaves_like 'including the file'
end
context 'when the rules does not match' do
let(:my_var) { 'mello' }
it_behaves_like 'not including the file'
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples IntegrationsActions do
RSpec.shared_examples Integrations::Actions do
let(:integration) do
create(:datadog_integration,
integration_attributes.merge(

View File

@ -2529,12 +2529,12 @@ axios-mock-adapter@^1.15.0:
dependencies:
deep-equal "^1.0.1"
axios@^0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd"
integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==
axios@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6"
integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==
dependencies:
follow-redirects "^1.10.0"
follow-redirects "^1.14.4"
babel-eslint@^10.0.3:
version "10.0.3"
@ -4982,10 +4982,10 @@ eslint-import-resolver-node@^0.3.4:
debug "^2.6.9"
resolve "^1.13.1"
eslint-import-resolver-webpack@0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.1.tgz#6d2fb928091daf2da46efa1e568055555b2de902"
integrity sha512-O/8mG6AHmaKYSMb4lWxiXPpaARxOJ4rMQEHJ8vTgjS1MXooJA3KPgBPPAdOPoV17v5ML5120qod5FBLM+DtgEw==
eslint-import-resolver-webpack@0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.2.tgz#fc813df0d08b9265cc7072d22393bda5198bdc1e"
integrity sha512-XodIPyg1OgE2h5BDErz3WJoK7lawxKTJNhgPNafRST6csC/MZC+L5P6kKqsZGRInpbgc02s/WZMrb4uGJzcuRg==
dependencies:
array-find "^1.0.0"
debug "^3.2.7"
@ -4993,8 +4993,8 @@ eslint-import-resolver-webpack@0.13.1:
find-root "^1.1.0"
has "^1.0.3"
interpret "^1.4.0"
is-core-module "^2.4.0"
is-regex "^1.1.3"
is-core-module "^2.7.0"
is-regex "^1.1.4"
lodash "^4.17.21"
resolve "^1.20.0"
semver "^5.7.1"
@ -5050,10 +5050,10 @@ eslint-plugin-jest@^23.8.2:
dependencies:
"@typescript-eslint/experimental-utils" "^2.5.0"
eslint-plugin-no-jquery@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.6.0.tgz#7892cb7c086f7813156bca6bc48429825428e9eb"
integrity sha512-xC7pbNHJMdyxqhzcNMRrmC5/tbt1T4KCKXjOqUpKm/CaRryGKS5iWztzWPrL0KwyI3R3ub6goHFmIQS19f+mZA==
eslint-plugin-no-jquery@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz#855f5631cf5b8e25b930cf6f06e02dd81f132e72"
integrity sha512-Aeg7dA6GTH1AcWLlBtWNzOU9efK5KpNi7b0EhBO0o0M+awyzguUUo8gF6hXGjQ9n5h8/uRtYv9zOqQkeC5CG0w==
eslint-plugin-promise@^4.2.1:
version "4.2.1"
@ -5605,10 +5605,10 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
follow-redirects@^1.0.0, follow-redirects@^1.14.4:
version "1.14.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==
for-in@^1.0.2:
version "1.0.2"
@ -5999,6 +5999,13 @@ has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has-tostringtag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
dependencies:
has-symbols "^1.0.2"
has-value@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@ -6511,10 +6518,10 @@ is-ci@^2.0.0:
dependencies:
ci-info "^2.0.0"
is-core-module@^2.2.0, is-core-module@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1"
integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==
is-core-module@^2.2.0, is-core-module@^2.7.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
dependencies:
has "^1.0.3"
@ -6690,13 +6697,13 @@ is-potential-custom-element-name@^1.0.0:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
is-regex@^1.1.1, is-regex@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
is-regex@^1.1.1, is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
dependencies:
call-bind "^1.0.2"
has-symbols "^1.0.2"
has-tostringtag "^1.0.0"
is-regexp@^2.0.0:
version "2.1.0"