Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c1cea595b6
commit
6c44b67631
|
@ -61,6 +61,7 @@ export default {
|
||||||
<gl-button
|
<gl-button
|
||||||
variant="confirm"
|
variant="confirm"
|
||||||
class="gl-mt-3"
|
class="gl-mt-3"
|
||||||
|
data-testid="create_new_ci_button"
|
||||||
data-qa-selector="create_new_ci_button"
|
data-qa-selector="create_new_ci_button"
|
||||||
@click="createEmptyConfigFile"
|
@click="createEmptyConfigFile"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
|
|
||||||
import { TREE_TYPE } from '../constants';
|
import { TREE_TYPE } from '../constants';
|
||||||
|
|
||||||
export const getLowestSingleFolder = (folder) => {
|
export const getLowestSingleFolder = (folder) => {
|
||||||
|
@ -28,7 +27,7 @@ export const getLowestSingleFolder = (folder) => {
|
||||||
const { path, tree } = getFolder(folder, [folder.name]);
|
const { path, tree } = getFolder(folder, [folder.name]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: truncatePathMiddleToLength(path.join('/'), 40),
|
path: path.join('/'),
|
||||||
treeAcc: tree.length ? tree[tree.length - 1].tree : null,
|
treeAcc: tree.length ? tree[tree.length - 1].tree : null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -167,36 +167,6 @@ export const truncateWidth = (string, options = {}) => {
|
||||||
*/
|
*/
|
||||||
export const truncateSha = (sha) => sha.substring(0, 8);
|
export const truncateSha = (sha) => sha.substring(0, 8);
|
||||||
|
|
||||||
const ELLIPSIS_CHAR = '…';
|
|
||||||
export const truncatePathMiddleToLength = (text, maxWidth) => {
|
|
||||||
let returnText = text;
|
|
||||||
let ellipsisCount = 0;
|
|
||||||
|
|
||||||
while (returnText.length >= maxWidth) {
|
|
||||||
const textSplit = returnText.split('/').filter((s) => s !== ELLIPSIS_CHAR);
|
|
||||||
|
|
||||||
if (textSplit.length === 0) {
|
|
||||||
// There are n - 1 path separators for n segments, so 2n - 1 <= maxWidth
|
|
||||||
const maxSegments = Math.floor((maxWidth + 1) / 2);
|
|
||||||
return new Array(maxSegments).fill(ELLIPSIS_CHAR).join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const middleIndex = Math.floor(textSplit.length / 2);
|
|
||||||
|
|
||||||
returnText = textSplit
|
|
||||||
.slice(0, middleIndex)
|
|
||||||
.concat(
|
|
||||||
new Array(ellipsisCount + 1).fill().map(() => ELLIPSIS_CHAR),
|
|
||||||
textSplit.slice(middleIndex + 1),
|
|
||||||
)
|
|
||||||
.join('/');
|
|
||||||
|
|
||||||
ellipsisCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnText;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capitalizes first character
|
* Capitalizes first character
|
||||||
*
|
*
|
||||||
|
|
|
@ -16,8 +16,6 @@ import DynamicContent from './dynamic_content.vue';
|
||||||
import StatusIcon from './status_icon.vue';
|
import StatusIcon from './status_icon.vue';
|
||||||
import ActionButtons from './action_buttons.vue';
|
import ActionButtons from './action_buttons.vue';
|
||||||
|
|
||||||
const FETCH_TYPE_COLLAPSED = 'collapsed';
|
|
||||||
const FETCH_TYPE_EXPANDED = 'expanded';
|
|
||||||
const WIDGET_PREFIX = 'Widget';
|
const WIDGET_PREFIX = 'Widget';
|
||||||
const MISSING_RESPONSE_HEADERS =
|
const MISSING_RESPONSE_HEADERS =
|
||||||
'MR Widget: raesponse object should contain status and headers object. Make sure to include that in your `fetchCollapsedData` and `fetchExpandedData` functions.';
|
'MR Widget: raesponse object should contain status and headers object. Make sure to include that in your `fetchCollapsedData` and `fetchExpandedData` functions.';
|
||||||
|
@ -49,15 +47,6 @@ export default {
|
||||||
SafeHtml,
|
SafeHtml,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
/**
|
|
||||||
* @param {value.collapsed} Object
|
|
||||||
* @param {value.expanded} Object
|
|
||||||
*/
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
loadingText: {
|
loadingText: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -238,7 +227,7 @@ export default {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.fetchCollapsedData) {
|
if (this.fetchCollapsedData) {
|
||||||
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
|
await this.fetch(this.fetchCollapsedData);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
this.summaryError = this.errorText;
|
this.summaryError = this.errorText;
|
||||||
|
@ -271,7 +260,7 @@ export default {
|
||||||
this.contentError = null;
|
this.contentError = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
|
await this.fetch(this.fetchExpandedData);
|
||||||
} catch {
|
} catch {
|
||||||
this.contentError = this.errorText;
|
this.contentError = this.errorText;
|
||||||
|
|
||||||
|
@ -282,7 +271,7 @@ export default {
|
||||||
|
|
||||||
this.isLoadingExpandedContent = false;
|
this.isLoadingExpandedContent = false;
|
||||||
},
|
},
|
||||||
fetch(handler, dataType) {
|
fetch(handler) {
|
||||||
const requests = this.multiPolling ? handler() : [handler];
|
const requests = this.multiPolling ? handler() : [handler];
|
||||||
|
|
||||||
const promises = requests.map((request) => {
|
const promises = requests.map((request) => {
|
||||||
|
@ -319,9 +308,7 @@ export default {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(promises).then((data) => {
|
return Promise.all(promises);
|
||||||
this.$emit('input', { ...this.value, [dataType]: this.multiPolling ? data : data[0] });
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
failedStatusIcon: EXTENSION_ICONS.failed,
|
failedStatusIcon: EXTENSION_ICONS.failed,
|
||||||
|
|
|
@ -110,7 +110,7 @@ module Projects
|
||||||
|
|
||||||
update_pending_builds
|
update_pending_builds
|
||||||
|
|
||||||
post_update_hooks(project)
|
post_update_hooks(project, @old_group)
|
||||||
rescue Exception # rubocop:disable Lint/RescueException
|
rescue Exception # rubocop:disable Lint/RescueException
|
||||||
rollback_side_effects
|
rollback_side_effects
|
||||||
raise
|
raise
|
||||||
|
@ -119,7 +119,7 @@ module Projects
|
||||||
end
|
end
|
||||||
|
|
||||||
# Overridden in EE
|
# Overridden in EE
|
||||||
def post_update_hooks(project)
|
def post_update_hooks(project, _old_group)
|
||||||
ensure_personal_project_owner_membership(project)
|
ensure_personal_project_owner_membership(project)
|
||||||
invalidate_personal_projects_counts
|
invalidate_personal_projects_counts
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
.form-group
|
.form-group
|
||||||
= f.label s_('SubgroupCreationLevel|Roles allowed to create subgroups'), class: 'label-bold'
|
= f.label s_('SubgroupCreationLevel|Roles allowed to create subgroups'), class: 'label-bold'
|
||||||
= f.select :subgroup_creation_level, options_for_select(::Gitlab::Access.subgroup_creation_options, group.subgroup_creation_level), {}, class: 'form-control'
|
- ::Gitlab::Access.subgroup_creation_options.each do |label, action|
|
||||||
|
= f.gitlab_ui_radio_component :subgroup_creation_level, action, label
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: ci_interpolation_inputs_refactor
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125632
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/418198
|
||||||
|
milestone: '16.3'
|
||||||
|
type: development
|
||||||
|
group: group::pipeline authoring
|
||||||
|
default_enabled: false
|
|
@ -371,6 +371,11 @@ module.exports = {
|
||||||
include: /node_modules/,
|
include: /node_modules/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /gridstack\/.*\.js$/,
|
||||||
|
include: /node_modules/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /_worker\.js$/,
|
test: /_worker\.js$/,
|
||||||
resourceQuery: /worker/,
|
resourceQuery: /worker/,
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EnsureBackfillForCiPipelineVariablesPipelineIdIsFinished < Gitlab::Database::Migration[2.1]
|
||||||
|
include Gitlab::Database::MigrationHelpers::ConvertToBigint
|
||||||
|
|
||||||
|
restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
TABLE_NAME = :ci_pipeline_variables
|
||||||
|
|
||||||
|
def up
|
||||||
|
ensure_batched_background_migration_is_finished(
|
||||||
|
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
|
||||||
|
table_name: TABLE_NAME,
|
||||||
|
column_name: 'pipeline_id',
|
||||||
|
job_arguments: [['pipeline_id'], ['id_convert_to_bigint']]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# no-op
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateAsyncIndexForCiPiplineVariablesPipelineId < Gitlab::Database::Migration[2.1]
|
||||||
|
TABLE_NAME = :ci_pipeline_variables
|
||||||
|
INDEX_NAME = "index_ci_pipeline_variables_on_pipeline_id_bigint_and_key"
|
||||||
|
|
||||||
|
def up
|
||||||
|
prepare_async_index TABLE_NAME, [:pipeline_id_convert_to_bigint, :key], unique: true, name: INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
unprepare_async_index TABLE_NAME, [:pipeline_id_convert_to_bigint, :key], unique: true, name: INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
e2a7487d4da68819a1bdde6626b32ef8399a917a75d4cbc8ecf2fa140ba1ff16
|
|
@ -0,0 +1 @@
|
||||||
|
54ac5a22e121379b1ffcefc7b4c1f26cadd15a9b0cabfd0d9e3cba3886777d46
|
|
@ -52,9 +52,9 @@ during indexing and searching operations. Some of the benefits and tradeoffs to
|
||||||
- Routing is not used if too many shards would be hit for global and group scoped searches.
|
- Routing is not used if too many shards would be hit for global and group scoped searches.
|
||||||
- Shard size imbalance might occur.
|
- Shard size imbalance might occur.
|
||||||
|
|
||||||
## Existing Analyzers/Tokenizers/Filters
|
## Existing analyzers and tokenizers
|
||||||
|
|
||||||
These are all defined in [`ee/lib/elastic/latest/config.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/elastic/latest/config.rb)
|
The following analyzers and tokenizers are defined in [`ee/lib/elastic/latest/config.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/elastic/latest/config.rb).
|
||||||
|
|
||||||
### Analyzers
|
### Analyzers
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ Please see the `sha_tokenizer` explanation later below for an example.
|
||||||
|
|
||||||
#### `code_analyzer`
|
#### `code_analyzer`
|
||||||
|
|
||||||
Used when indexing a blob's filename and content. Uses the `whitespace` tokenizer and the filters: [`code`](#code), `lowercase`, and `asciifolding`
|
Used when indexing a blob's filename and content. Uses the `whitespace` tokenizer and the [`word_delimiter_graph`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-word-delimiter-graph-tokenfilter.html), `lowercase`, and `asciifolding` filters.
|
||||||
|
|
||||||
The `whitespace` tokenizer was selected to have more control over how tokens are split. For example the string `Foo::bar(4)` needs to generate tokens like `Foo` and `bar(4)` to be properly searched.
|
The `whitespace` tokenizer was selected to have more control over how tokens are split. For example the string `Foo::bar(4)` needs to generate tokens like `Foo` and `bar(4)` to be properly searched.
|
||||||
|
|
||||||
|
@ -81,10 +81,6 @@ Please see the `code` filter for an explanation on how tokens are split.
|
||||||
NOTE:
|
NOTE:
|
||||||
The [Elasticsearch `code_analyzer` doesn't account for all code cases](../integration/advanced_search/elasticsearch_troubleshooting.md#elasticsearch-code_analyzer-doesnt-account-for-all-code-cases).
|
The [Elasticsearch `code_analyzer` doesn't account for all code cases](../integration/advanced_search/elasticsearch_troubleshooting.md#elasticsearch-code_analyzer-doesnt-account-for-all-code-cases).
|
||||||
|
|
||||||
#### `code_search_analyzer`
|
|
||||||
|
|
||||||
Not directly used for indexing, but rather used to transform a search input. Uses the `whitespace` tokenizer and the `lowercase` and `asciifolding` filters.
|
|
||||||
|
|
||||||
### Tokenizers
|
### Tokenizers
|
||||||
|
|
||||||
#### `sha_tokenizer`
|
#### `sha_tokenizer`
|
||||||
|
@ -115,27 +111,10 @@ Example:
|
||||||
- `'path/application.js'`
|
- `'path/application.js'`
|
||||||
- `'application.js'`
|
- `'application.js'`
|
||||||
|
|
||||||
### Filters
|
|
||||||
|
|
||||||
#### `code`
|
|
||||||
|
|
||||||
Uses a [Pattern Capture token filter](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-pattern-capture-tokenfilter.html) to split tokens into more easily searched versions of themselves.
|
|
||||||
|
|
||||||
Patterns:
|
|
||||||
|
|
||||||
- `"(\\p{Ll}+|\\p{Lu}\\p{Ll}+|\\p{Lu}+)"`: captures CamelCase and lowerCamelCase strings as separate tokens
|
|
||||||
- `"(\\d+)"`: extracts digits
|
|
||||||
- `"(?=([\\p{Lu}]+[\\p{L}]+))"`: captures CamelCase strings recursively. For example: `ThisIsATest` => `[ThisIsATest, IsATest, ATest, Test]`
|
|
||||||
- `'"((?:\\"|[^"]|\\")*)"'`: captures terms inside quotes, removing the quotes
|
|
||||||
- `"'((?:\\'|[^']|\\')*)'"`: same as above, for single-quotes
|
|
||||||
- `'\.([^.]+)(?=\.|\s|\Z)'`: separate terms with periods in-between
|
|
||||||
- `'([\p{L}_.-]+)'`: some common chars in file names to keep the whole filename intact (for example `my_file-ñame.txt`)
|
|
||||||
- `'([\p{L}\d_]+)'`: letters, numbers and underscores are the most common tokens in programming. Always capture them greedily regardless of context.
|
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- Searches can have their own analyzers. Remember to check when editing analyzers
|
- Searches can have their own analyzers. Remember to check when editing analyzers.
|
||||||
- `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches
|
- `Character` filters (as opposed to token filters) always replace the original character. These filters can hinder exact searches.
|
||||||
|
|
||||||
## Zero downtime reindexing with multiple indices
|
## Zero downtime reindexing with multiple indices
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,24 @@ This page shows groups that you are a member of:
|
||||||
- Through membership of a subgroup's parent group.
|
- Through membership of a subgroup's parent group.
|
||||||
- Through direct or inherited membership of a project in the group or subgroup.
|
- Through direct or inherited membership of a project in the group or subgroup.
|
||||||
|
|
||||||
|
## View group activity
|
||||||
|
|
||||||
|
To view the activity of a project:
|
||||||
|
|
||||||
|
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
|
||||||
|
1. Select **Manage > Activity**.
|
||||||
|
1. Optional. To filter activity by contribution type, select a tab:
|
||||||
|
|
||||||
|
- **All**: All contributions by group members in the group and group's projects.
|
||||||
|
- **Push events**: Push events in the group's projects.
|
||||||
|
- **Merge events**: Accepted merge requests in the group's projects.
|
||||||
|
- **Issue events**: Issues opened and closed in the group's projects.
|
||||||
|
- **Epic events**: Epics opened and closed in the group.
|
||||||
|
- **Comments**: Comments posted by group members in the group's projects.
|
||||||
|
- **Wiki**: Updates to wiki pages in the group.
|
||||||
|
- **Designs**: Designs added, updated, and removed in the group's projects.
|
||||||
|
- **Team**: Group members who joined and left the group's projects.
|
||||||
|
|
||||||
## Create a group
|
## Create a group
|
||||||
|
|
||||||
To create a group:
|
To create a group:
|
||||||
|
|
|
@ -354,7 +354,16 @@ To view your activity:
|
||||||
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
|
||||||
1. Select **Your work**.
|
1. Select **Your work**.
|
||||||
1. Select **Activity**.
|
1. Select **Activity**.
|
||||||
1. Optional. To filter your activity by contribution type, in the **Your Activity** tab, select a tab.
|
1. Optional. To filter your activity by contribution type, in the **Your Activity** tab, select a tab:
|
||||||
|
|
||||||
|
- **All**: All contributions you made in your groups and projects.
|
||||||
|
- **Push events**: Push events you made in your projects.
|
||||||
|
- **Merge events**: Merge requests you accepted in your projects.
|
||||||
|
- **Issue events**: Issues you opened and closed in your projects.
|
||||||
|
- **Comments**: Comments you posted in your projects.
|
||||||
|
- **Wiki**: Wiki pages you created and updated in your projects.
|
||||||
|
- **Designs**: Designs you added, updated, and removed in your projects.
|
||||||
|
- **Team**: Projects you joined and left.
|
||||||
|
|
||||||
## Session duration
|
## Session duration
|
||||||
|
|
||||||
|
|
|
@ -178,7 +178,15 @@ To view the activity of a project:
|
||||||
|
|
||||||
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
|
||||||
1. Select **Manage > Activity**.
|
1. Select **Manage > Activity**.
|
||||||
1. Select a tab to view the type of project activity.
|
1. Optional. To filter activity by contribution type, select a tab:
|
||||||
|
|
||||||
|
- **All**: All contributions by project members.
|
||||||
|
- **Push events**: Push events in the project.
|
||||||
|
- **Merge events**: Accepted merge requests in the project.
|
||||||
|
- **Issue events**: Issues opened and closed in the project.
|
||||||
|
- **Comments**: Comments posted by project members.
|
||||||
|
- **Designs**: Designs added, updated, and removed in the project.
|
||||||
|
- **Team**: Members who joined and left the project.
|
||||||
|
|
||||||
## Search in projects
|
## Search in projects
|
||||||
|
|
||||||
|
|
|
@ -203,6 +203,7 @@ module.exports = (path, options = {}) => {
|
||||||
'@gitlab/favicon-overlay',
|
'@gitlab/favicon-overlay',
|
||||||
'@gitlab/cluster-client',
|
'@gitlab/cluster-client',
|
||||||
'bootstrap-vue',
|
'bootstrap-vue',
|
||||||
|
'gridstack',
|
||||||
'three',
|
'three',
|
||||||
'monaco-editor',
|
'monaco-editor',
|
||||||
'monaco-yaml',
|
'monaco-yaml',
|
||||||
|
|
|
@ -73,7 +73,11 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def inputs
|
def inputs
|
||||||
@inputs ||= Ci::Input::Inputs.new(spec, args)
|
@inputs ||= if Feature.enabled?(:ci_interpolation_inputs_refactor)
|
||||||
|
Ci::Interpolation::Inputs.new(spec, args)
|
||||||
|
else
|
||||||
|
Ci::Input::Inputs.new(spec, args)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def context
|
def context
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Ci
|
||||||
|
module Interpolation
|
||||||
|
# Interpolation inputs provided by the user.
|
||||||
|
class Inputs
|
||||||
|
UnknownInputTypeError = Class.new(StandardError)
|
||||||
|
|
||||||
|
TYPES = [
|
||||||
|
StringInput
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def initialize(specs, args)
|
||||||
|
@specs = specs.to_h
|
||||||
|
@args = args.to_h
|
||||||
|
@inputs = []
|
||||||
|
@errors = []
|
||||||
|
|
||||||
|
validate!
|
||||||
|
fabricate!
|
||||||
|
end
|
||||||
|
|
||||||
|
def errors
|
||||||
|
@errors + @inputs.flat_map(&:errors)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
errors.none?
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_hash
|
||||||
|
@inputs.inject({}) do |hash, input|
|
||||||
|
hash.merge(input.to_hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate!
|
||||||
|
unknown_inputs = @args.keys - @specs.keys
|
||||||
|
return if unknown_inputs.empty?
|
||||||
|
|
||||||
|
@errors.push("unknown input arguments: #{unknown_inputs.join(', ')}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def fabricate!
|
||||||
|
@specs.each do |input_name, spec|
|
||||||
|
input_type = TYPES.find { |klass| klass.matches?(spec) }
|
||||||
|
|
||||||
|
unless input_type
|
||||||
|
@errors.push(
|
||||||
|
"unknown input specification for `#{input_name}` (valid types: #{valid_type_names.join(', ')})")
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
@inputs.push(input_type.new(
|
||||||
|
name: input_name,
|
||||||
|
spec: spec,
|
||||||
|
value: @args[input_name]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_type_names
|
||||||
|
TYPES.map(&:type_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,92 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Ci
|
||||||
|
module Interpolation
|
||||||
|
class Inputs
|
||||||
|
##
|
||||||
|
# This is a common abstraction for all input types
|
||||||
|
class BaseInput
|
||||||
|
ArgumentNotValidError = Class.new(StandardError)
|
||||||
|
|
||||||
|
# Checks whether the class matches the type in the specification
|
||||||
|
def self.matches?(spec)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Human readable type used in error messages
|
||||||
|
def self.type_name
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks whether the provided value is of the given type
|
||||||
|
def valid_value?(value)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :errors, :name, :spec, :value
|
||||||
|
|
||||||
|
def initialize(name:, spec:, value:)
|
||||||
|
@name = name
|
||||||
|
@errors = []
|
||||||
|
|
||||||
|
# Treat minimal spec definition (nil) as a valid hash:
|
||||||
|
# spec:
|
||||||
|
# inputs:
|
||||||
|
# website:
|
||||||
|
@spec = spec || {} # specification from input definition
|
||||||
|
@value = value # actual value provided by the user
|
||||||
|
|
||||||
|
validate!
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_hash
|
||||||
|
raise ArgumentNotValidError unless valid?
|
||||||
|
|
||||||
|
{ name => actual_value }
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
@errors.none?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate!
|
||||||
|
return error('required value has not been provided') if required_input? && value.nil?
|
||||||
|
|
||||||
|
# validate default value
|
||||||
|
return error("default value is not a #{self.class.type_name}") if !required_input? && !valid_value?(default)
|
||||||
|
|
||||||
|
# validate provided value
|
||||||
|
error("provided value is not a #{self.class.type_name}") unless valid_value?(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(message)
|
||||||
|
@errors.push("`#{name}` input: #{message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def actual_value
|
||||||
|
# nil check is to support boolean values.
|
||||||
|
value.nil? ? default : value
|
||||||
|
end
|
||||||
|
|
||||||
|
# An input specification without a default value is required.
|
||||||
|
# For example:
|
||||||
|
# ```yaml
|
||||||
|
# spec:
|
||||||
|
# inputs:
|
||||||
|
# website:
|
||||||
|
# ```
|
||||||
|
def required_input?
|
||||||
|
!spec.key?(:default)
|
||||||
|
end
|
||||||
|
|
||||||
|
def default
|
||||||
|
spec[:default]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Ci
|
||||||
|
module Interpolation
|
||||||
|
class Inputs
|
||||||
|
class StringInput < BaseInput
|
||||||
|
def self.matches?(spec)
|
||||||
|
# The input spec can be `nil` when using a minimal specification
|
||||||
|
# and also when `type` is not specified.
|
||||||
|
#
|
||||||
|
# ```yaml
|
||||||
|
# spec:
|
||||||
|
# inputs:
|
||||||
|
# foo:
|
||||||
|
# ```
|
||||||
|
spec.nil? || (spec.is_a?(Hash) && [nil, type_name].include?(spec[:type]))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.type_name
|
||||||
|
'string'
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_value?(value)
|
||||||
|
value.nil? || value.is_a?(String)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -51441,6 +51441,9 @@ msgstr ""
|
||||||
msgid "Vulnerability|Try it out"
|
msgid "Vulnerability|Try it out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vulnerability|URL:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Vulnerability|Unmodified Response"
|
msgid "Vulnerability|Unmodified Response"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@
|
||||||
"gettext-parser": "^6.0.0",
|
"gettext-parser": "^6.0.0",
|
||||||
"graphql": "^15.7.2",
|
"graphql": "^15.7.2",
|
||||||
"graphql-tag": "^2.11.0",
|
"graphql-tag": "^2.11.0",
|
||||||
"gridstack": "^7.3.0",
|
"gridstack": "^8.3.0",
|
||||||
"highlight.js": "^11.8.0",
|
"highlight.js": "^11.8.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"ipaddr.js": "^1.9.1",
|
"ipaddr.js": "^1.9.1",
|
||||||
|
|
|
@ -27,13 +27,6 @@ module QA
|
||||||
Page::Project::PipelineEditor::New.perform(&:create_new_ci)
|
Page::Project::PipelineEditor::New.perform(&:create_new_ci)
|
||||||
|
|
||||||
Page::Project::PipelineEditor::Show.perform do |show|
|
Page::Project::PipelineEditor::Show.perform do |show|
|
||||||
# Editor should display default content when project does not have CI file yet
|
|
||||||
# New MR checkbox should not be rendered when a new target branch is yet to be provided
|
|
||||||
aggregate_failures 'check editor default conditions' do
|
|
||||||
expect(show.editing_content).not_to be_empty
|
|
||||||
expect(show).to have_no_new_mr_checkbox
|
|
||||||
end
|
|
||||||
|
|
||||||
# The new MR checkbox is visible after a new branch name is set
|
# The new MR checkbox is visible after a new branch name is set
|
||||||
show.set_source_branch(SecureRandom.hex(10))
|
show.set_source_branch(SecureRandom.hex(10))
|
||||||
expect(show).to have_new_mr_checkbox
|
expect(show).to have_new_mr_checkbox
|
||||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
||||||
let(:default_branch) { 'main' }
|
let(:default_branch) { 'main' }
|
||||||
let(:other_branch) { 'test' }
|
let(:other_branch) { 'test' }
|
||||||
let(:branch_with_invalid_ci) { 'despair' }
|
let(:branch_with_invalid_ci) { 'despair' }
|
||||||
|
let(:branch_without_ci) { 'empty' }
|
||||||
|
|
||||||
let(:default_content) { 'Default' }
|
let(:default_content) { 'Default' }
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
||||||
project.repository.create_file(user, project.ci_config_path_or_default, default_content, message: 'Create CI file for main', branch_name: default_branch)
|
project.repository.create_file(user, project.ci_config_path_or_default, default_content, message: 'Create CI file for main', branch_name: default_branch)
|
||||||
project.repository.create_file(user, project.ci_config_path_or_default, valid_content, message: 'Create CI file for test', branch_name: other_branch)
|
project.repository.create_file(user, project.ci_config_path_or_default, valid_content, message: 'Create CI file for test', branch_name: other_branch)
|
||||||
project.repository.create_file(user, project.ci_config_path_or_default, invalid_content, message: 'Create CI file for test', branch_name: branch_with_invalid_ci)
|
project.repository.create_file(user, project.ci_config_path_or_default, invalid_content, message: 'Create CI file for test', branch_name: branch_with_invalid_ci)
|
||||||
|
project.repository.create_file(user, 'index.js', "file", message: 'New js file', branch_name: branch_without_ci)
|
||||||
|
|
||||||
visit project_ci_pipeline_editor_path(project)
|
visit project_ci_pipeline_editor_path(project)
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
@ -62,6 +64,31 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'when there are no CI config file' do
|
||||||
|
before do
|
||||||
|
visit project_ci_pipeline_editor_path(project, branch_name: branch_without_ci)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the empty page', :aggregate_failures do
|
||||||
|
expect(page).to have_content 'Optimize your workflow with CI/CD Pipelines'
|
||||||
|
expect(page).to have_selector '[data-testid="create_new_ci_button"]'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when clicking on the create new CI button' do
|
||||||
|
before do
|
||||||
|
click_button 'Configure pipeline'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the source editor with default content', :aggregate_failures do
|
||||||
|
expect(page).to have_selector('#source-editor-')
|
||||||
|
|
||||||
|
page.within('#source-editor-') do
|
||||||
|
expect(page).to have_content('This file is a template, and might need editing before it works on your project.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'When CI yml has valid syntax' do
|
describe 'When CI yml has valid syntax' do
|
||||||
before do
|
before do
|
||||||
visit project_ci_pipeline_editor_path(project, branch_name: other_branch)
|
visit project_ci_pipeline_editor_path(project, branch_name: other_branch)
|
||||||
|
@ -149,15 +176,6 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'default branch switcher behavior' do
|
shared_examples 'default branch switcher behavior' do
|
||||||
def switch_to_branch(branch)
|
|
||||||
find('[data-testid="branch-selector"]').click
|
|
||||||
|
|
||||||
page.within '[data-testid="branch-selector"]' do
|
|
||||||
click_button branch
|
|
||||||
wait_for_requests
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'displays current branch' do
|
it 'displays current branch' do
|
||||||
page.within('[data-testid="branch-selector"]') do
|
page.within('[data-testid="branch-selector"]') do
|
||||||
expect(page).to have_content(default_branch)
|
expect(page).to have_content(default_branch)
|
||||||
|
@ -195,12 +213,20 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Branch Switcher' do
|
describe 'Branch Switcher' do
|
||||||
|
def switch_to_branch(branch)
|
||||||
|
# close button for the popover
|
||||||
|
find('[data-testid="close-button"]').click
|
||||||
|
find('[data-testid="branch-selector"]').click
|
||||||
|
|
||||||
|
page.within '[data-testid="branch-selector"]' do
|
||||||
|
click_button branch
|
||||||
|
wait_for_requests
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
visit project_ci_pipeline_editor_path(project)
|
visit project_ci_pipeline_editor_path(project)
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
# close button for the popover
|
|
||||||
find('[data-testid="close-button"]').click
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'default branch switcher behavior'
|
it_behaves_like 'default branch switcher behavior'
|
||||||
|
@ -262,6 +288,24 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Commit Form' do
|
describe 'Commit Form' do
|
||||||
|
context 'when targetting the main branch' do
|
||||||
|
it 'does not show the option to create a Merge request', :aggregate_failures do
|
||||||
|
expect(page).not_to have_selector('[data-testid="new-mr-checkbox"]')
|
||||||
|
expect(page).not_to have_content('Start a new merge request with these changes')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when targetting any non-main branch' do
|
||||||
|
before do
|
||||||
|
find('#source-branch-field').set('new_branch', clear: :backspace)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows the option to create a Merge request', :aggregate_failures do
|
||||||
|
expect(page).to have_selector('[data-testid="new-mr-checkbox"]')
|
||||||
|
expect(page).to have_content('Start a new merge request with these changes')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'is preserved when changing tabs' do
|
it 'is preserved when changing tabs' do
|
||||||
find('#commit-message').set('message', clear: :backspace)
|
find('#commit-message').set('message', clear: :backspace)
|
||||||
find('#source-branch-field').set('new_branch', clear: :backspace)
|
find('#source-branch-field').set('new_branch', clear: :backspace)
|
||||||
|
|
|
@ -382,7 +382,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'tree',
|
type: 'tree',
|
||||||
name: 'ee/lib/…/…/…/longtreenametomakepath',
|
name: 'ee/lib/ee/gitlab/checks/longtreenametomakepath',
|
||||||
tree: [
|
tree: [
|
||||||
{
|
{
|
||||||
name: 'diff_check.rb',
|
name: 'diff_check.rb',
|
||||||
|
|
|
@ -238,36 +238,6 @@ describe('text_utility', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('truncatePathMiddleToLength', () => {
|
|
||||||
it('does not truncate text', () => {
|
|
||||||
expect(textUtils.truncatePathMiddleToLength('app/test', 50)).toEqual('app/test');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('truncates middle of the path', () => {
|
|
||||||
expect(textUtils.truncatePathMiddleToLength('app/test/diff', 13)).toEqual('app/…/diff');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('truncates multiple times in the middle of the path', () => {
|
|
||||||
expect(textUtils.truncatePathMiddleToLength('app/test/merge_request/diff', 13)).toEqual(
|
|
||||||
'app/…/…/diff',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('given a path too long for the maxWidth', () => {
|
|
||||||
it.each`
|
|
||||||
path | maxWidth | result
|
|
||||||
${'aa/bb/cc'} | ${1} | ${'…'}
|
|
||||||
${'aa/bb/cc'} | ${2} | ${'…'}
|
|
||||||
${'aa/bb/cc'} | ${3} | ${'…/…'}
|
|
||||||
${'aa/bb/cc'} | ${4} | ${'…/…'}
|
|
||||||
${'aa/bb/cc'} | ${5} | ${'…/…/…'}
|
|
||||||
`('truncates ($path, $maxWidth) to $result', ({ path, maxWidth, result }) => {
|
|
||||||
expect(result.length).toBeLessThanOrEqual(maxWidth);
|
|
||||||
expect(textUtils.truncatePathMiddleToLength(path, maxWidth)).toEqual(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('slugifyWithUnderscore', () => {
|
describe('slugifyWithUnderscore', () => {
|
||||||
it('should replaces whitespaces with underscore and convert to lower case', () => {
|
it('should replaces whitespaces with underscore and convert to lower case', () => {
|
||||||
expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string');
|
expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string');
|
||||||
|
|
|
@ -121,14 +121,15 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetch', () => {
|
describe('fetch', () => {
|
||||||
it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => {
|
it('calls fetchCollapsedData properly when multiPolling is false', async () => {
|
||||||
const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } };
|
const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } };
|
||||||
createComponent({ propsData: { fetchCollapsedData: () => Promise.resolve(mockData) } });
|
const fetchCollapsedData = jest.fn().mockResolvedValue(mockData);
|
||||||
|
createComponent({ propsData: { fetchCollapsedData } });
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null });
|
expect(fetchCollapsedData).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the data.collapsed property after a successfull call - multiPolling: true', async () => {
|
it('calls fetchCollapsedData properly when multiPolling is true', async () => {
|
||||||
const mockData1 = {
|
const mockData1 = {
|
||||||
headers: {},
|
headers: {},
|
||||||
status: HTTP_STATUS_OK,
|
status: HTTP_STATUS_OK,
|
||||||
|
@ -140,22 +141,22 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
||||||
data: { vulnerabilities: [{ vuln: 2 }] },
|
data: { vulnerabilities: [{ vuln: 2 }] },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchCollapsedData = [
|
||||||
|
jest.fn().mockResolvedValue(mockData1),
|
||||||
|
jest.fn().mockResolvedValue(mockData2),
|
||||||
|
];
|
||||||
|
|
||||||
createComponent({
|
createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
multiPolling: true,
|
multiPolling: true,
|
||||||
fetchCollapsedData: () => [
|
fetchCollapsedData: () => fetchCollapsedData,
|
||||||
() => Promise.resolve(mockData1),
|
|
||||||
() => Promise.resolve(mockData2),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
expect(fetchCollapsedData[0]).toHaveBeenCalledTimes(1);
|
||||||
collapsed: [mockData1.data, mockData2.data],
|
expect(fetchCollapsedData[1]).toHaveBeenCalledTimes(1);
|
||||||
expanded: null,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error when the handler does not include headers or status objects', async () => {
|
it('throws an error when the handler does not include headers or status objects', async () => {
|
||||||
|
@ -328,11 +329,12 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded);
|
const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded);
|
||||||
|
const fetchCollapsedData = jest.fn().mockResolvedValue(mockDataCollapsed);
|
||||||
|
|
||||||
await createComponent({
|
await createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
isCollapsible: true,
|
isCollapsible: true,
|
||||||
fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
|
fetchCollapsedData,
|
||||||
fetchExpandedData,
|
fetchExpandedData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -340,17 +342,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
||||||
findToggleButton().vm.$emit('click');
|
findToggleButton().vm.$emit('click');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
// First fetches the collapsed data
|
expect(fetchCollapsedData).toHaveBeenCalledTimes(1);
|
||||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
expect(fetchExpandedData).toHaveBeenCalledTimes(1);
|
||||||
collapsed: mockDataCollapsed.data,
|
|
||||||
expanded: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then fetches the expanded data
|
|
||||||
expect(wrapper.emitted('input')[1][0]).toEqual({
|
|
||||||
collapsed: null,
|
|
||||||
expanded: mockDataExpanded.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Triggering a click does not call the expanded data again
|
// Triggering a click does not call the expanded data again
|
||||||
findToggleButton().vm.$emit('click');
|
findToggleButton().vm.$emit('click');
|
||||||
|
@ -371,14 +364,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
||||||
findToggleButton().vm.$emit('click');
|
findToggleButton().vm.$emit('click');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
// First fetches the collapsed data
|
|
||||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
|
||||||
collapsed: undefined,
|
|
||||||
expanded: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(fetchExpandedData).toHaveBeenCalledTimes(1);
|
expect(fetchExpandedData).toHaveBeenCalledTimes(1);
|
||||||
expect(wrapper.emitted('input')).toHaveLength(1); // Should not an emit an input call because request failed
|
|
||||||
|
|
||||||
findToggleButton().vm.$emit('click');
|
findToggleButton().vm.$emit('click');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
|
@ -9,6 +9,8 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
||||||
|
|
||||||
subject { described_class.new(result, arguments) }
|
subject { described_class.new(result, arguments) }
|
||||||
|
|
||||||
|
# Remove shared examples when ci_interpolation_inputs_refactor is removed.
|
||||||
|
shared_examples 'interpolator' do
|
||||||
context 'when input data is valid' do
|
context 'when input data is valid' do
|
||||||
let(:header) do
|
let(:header) do
|
||||||
{ spec: { inputs: { website: nil } } }
|
{ spec: { inputs: { website: nil } } }
|
||||||
|
@ -25,6 +27,7 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
||||||
it 'correctly interpolates the config' do
|
it 'correctly interpolates the config' do
|
||||||
subject.interpolate!
|
subject.interpolate!
|
||||||
|
|
||||||
|
expect(subject).to be_interpolated
|
||||||
expect(subject).to be_valid
|
expect(subject).to be_valid
|
||||||
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
|
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
|
||||||
end
|
end
|
||||||
|
@ -90,28 +93,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when provided interpolation argument is invalid' do
|
|
||||||
let(:header) do
|
|
||||||
{ spec: { inputs: { website: nil } } }
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:content) do
|
|
||||||
{ test: 'deploy $[[ inputs.website ]]' }
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:arguments) do
|
|
||||||
{ website: ['gitlab.com'] }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'correctly interpolates the config' do
|
|
||||||
subject.interpolate!
|
|
||||||
|
|
||||||
expect(subject).not_to be_valid
|
|
||||||
expect(subject.error_message).to eq subject.errors.first
|
|
||||||
expect(subject.errors).to include 'unsupported value in input argument `website`'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when multiple interpolation blocks are invalid' do
|
context 'when multiple interpolation blocks are invalid' do
|
||||||
let(:header) do
|
let(:header) do
|
||||||
{ spec: { inputs: { website: nil } } }
|
{ spec: { inputs: { website: nil } } }
|
||||||
|
@ -129,7 +110,8 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
||||||
subject.interpolate!
|
subject.interpolate!
|
||||||
|
|
||||||
expect(subject).not_to be_valid
|
expect(subject).not_to be_valid
|
||||||
expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `something`'
|
expect(subject.error_message)
|
||||||
|
.to eq 'interpolation interrupted by errors, unknown interpolation key: `something`'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -148,7 +130,6 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
||||||
it 'returns original content' do
|
it 'returns original content' do
|
||||||
subject.interpolate!
|
subject.interpolate!
|
||||||
|
|
||||||
expect(subject).not_to be_interpolated
|
|
||||||
expect(subject.to_hash).to eq(content)
|
expect(subject.to_hash).to eq(content)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -169,9 +150,63 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
||||||
it 'correctly interpolates content' do
|
it 'correctly interpolates content' do
|
||||||
subject.interpolate!
|
subject.interpolate!
|
||||||
|
|
||||||
expect(subject).to be_interpolated
|
|
||||||
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
|
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'interpolator' do
|
||||||
|
context 'when provided interpolation argument is invalid' do
|
||||||
|
let(:header) do
|
||||||
|
{ spec: { inputs: { website: nil } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:content) do
|
||||||
|
{ test: 'deploy $[[ inputs.website ]]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:arguments) do
|
||||||
|
{ website: ['gitlab.com'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error' do
|
||||||
|
subject.interpolate!
|
||||||
|
|
||||||
|
expect(subject).not_to be_valid
|
||||||
|
expect(subject.error_message).to eq subject.errors.first
|
||||||
|
expect(subject.errors).to include '`website` input: provided value is not a string'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag ci_interpolation_inputs_refactor is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_interpolation_inputs_refactor: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'interpolator'
|
||||||
|
|
||||||
|
context 'when provided interpolation argument is invalid' do
|
||||||
|
let(:header) do
|
||||||
|
{ spec: { inputs: { website: nil } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:content) do
|
||||||
|
{ test: 'deploy $[[ inputs.website ]]' }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:arguments) do
|
||||||
|
{ website: ['gitlab.com'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an error' do
|
||||||
|
subject.interpolate!
|
||||||
|
|
||||||
|
expect(subject).not_to be_valid
|
||||||
|
expect(subject.error_message).to eq subject.errors.first
|
||||||
|
expect(subject.errors).to include 'unsupported value in input argument `website`'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Ci::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do
|
||||||
|
describe '.matches?' do
|
||||||
|
it 'is not implemented' do
|
||||||
|
expect { described_class.matches?(double) }.to raise_error(NotImplementedError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.type_name' do
|
||||||
|
it 'is not implemented' do
|
||||||
|
expect { described_class.type_name }.to raise_error(NotImplementedError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#valid_value?' do
|
||||||
|
it 'is not implemented' do
|
||||||
|
expect do
|
||||||
|
described_class.new(
|
||||||
|
name: 'website', spec: { website: nil }, value: { website: 'example.com' }
|
||||||
|
).valid_value?('test')
|
||||||
|
end.to raise_error(NotImplementedError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,97 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Ci::Interpolation::Inputs, feature_category: :pipeline_composition do
|
||||||
|
let(:inputs) { described_class.new(specs, args) }
|
||||||
|
let(:specs) { { foo: { default: 'bar' } } }
|
||||||
|
let(:args) { {} }
|
||||||
|
|
||||||
|
context 'when inputs are valid' do
|
||||||
|
where(:specs, :args, :merged) do
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ foo: { default: 'bar' } }, {},
|
||||||
|
{ foo: 'bar' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: { default: 'bar' } }, { foo: 'test' },
|
||||||
|
{ foo: 'test' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: nil }, { foo: 'bar' },
|
||||||
|
{ foo: 'bar' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: { type: 'string' } }, { foo: 'bar' },
|
||||||
|
{ foo: 'bar' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: { type: 'string', default: 'bar' } }, { foo: 'test' },
|
||||||
|
{ foo: 'test' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: { type: 'string', default: 'bar' } }, {},
|
||||||
|
{ foo: 'bar' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: { default: 'bar' }, baz: nil }, { baz: 'test' },
|
||||||
|
{ foo: 'bar', baz: 'test' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it 'contains the merged inputs' do
|
||||||
|
expect(inputs).to be_valid
|
||||||
|
expect(inputs.to_hash).to eq(merged)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when inputs are invalid' do
|
||||||
|
where(:specs, :args, :errors) do
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ foo: nil }, { foo: 'bar', test: 'bar' },
|
||||||
|
['unknown input arguments: test']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: nil }, { test: 'bar', gitlab: '1' },
|
||||||
|
['unknown input arguments: test, gitlab', '`foo` input: required value has not been provided']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: 123 }, {},
|
||||||
|
['unknown input specification for `foo` (valid types: string)']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ a: nil, foo: 123 }, { a: '123' },
|
||||||
|
['unknown input specification for `foo` (valid types: string)']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: nil }, {},
|
||||||
|
['`foo` input: required value has not been provided']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: { default: 123 } }, { foo: 'test' },
|
||||||
|
['`foo` input: default value is not a string']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: { default: 'test' } }, { foo: 123 },
|
||||||
|
['`foo` input: provided value is not a string']
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ foo: nil }, { foo: 123 },
|
||||||
|
['`foo` input: provided value is not a string']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it 'contains the merged inputs', :aggregate_failures do
|
||||||
|
expect(inputs).not_to be_valid
|
||||||
|
expect(inputs.errors).to contain_exactly(*errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -117,6 +117,10 @@ module.exports = function storybookWebpackConfig({ config }) {
|
||||||
config.resolve.extensions = Array.from(
|
config.resolve.extensions = Array.from(
|
||||||
new Set([...config.resolve.extensions, ...gitlabWebpackConfig.resolve.extensions]),
|
new Set([...config.resolve.extensions, ...gitlabWebpackConfig.resolve.extensions]),
|
||||||
);
|
);
|
||||||
|
config.resolve.alias = {
|
||||||
|
...config.resolve.alias,
|
||||||
|
gridstack: require.resolve('gridstack/dist/es5/gridstack.js'),
|
||||||
|
};
|
||||||
|
|
||||||
// Replace any Storybook-defined CSS loaders with our custom one.
|
// Replace any Storybook-defined CSS loaders with our custom one.
|
||||||
config.module.rules = [
|
config.module.rules = [
|
||||||
|
|
|
@ -6746,10 +6746,10 @@ graphql@^15.7.2:
|
||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
|
||||||
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
|
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
|
||||||
|
|
||||||
gridstack@^7.3.0:
|
gridstack@^8.3.0:
|
||||||
version "7.3.0"
|
version "8.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-7.3.0.tgz#7b32395edcd885bc39b84068ac86f2831f7a2451"
|
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-8.3.0.tgz#4c79f8b8c4cffeb3664266108e38ed91b3d0f7b4"
|
||||||
integrity sha512-JKZgsHzm1ljkn1NnBZpf8j4NDOBCXTuw0m1ZC0sr6NKUh0BFWzXAONIxtX1hWGUVeKLj5l1VcmnTwCXw5ypDNw==
|
integrity sha512-RcL2xskAYKOpakvpSwHdKheG7C7YgNY7777C5m+T1JMjSgcmEc3qPBM573l0NuyjMz4Errx1/3p+rMgUfF4+mw==
|
||||||
|
|
||||||
gzip-size@^6.0.0:
|
gzip-size@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
|
|
Loading…
Reference in New Issue