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
|
||||
variant="confirm"
|
||||
class="gl-mt-3"
|
||||
data-testid="create_new_ci_button"
|
||||
data-qa-selector="create_new_ci_button"
|
||||
@click="createEmptyConfigFile"
|
||||
>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
|
||||
import { TREE_TYPE } from '../constants';
|
||||
|
||||
export const getLowestSingleFolder = (folder) => {
|
||||
|
@ -28,7 +27,7 @@ export const getLowestSingleFolder = (folder) => {
|
|||
const { path, tree } = getFolder(folder, [folder.name]);
|
||||
|
||||
return {
|
||||
path: truncatePathMiddleToLength(path.join('/'), 40),
|
||||
path: path.join('/'),
|
||||
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);
|
||||
|
||||
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
|
||||
*
|
||||
|
|
|
@ -16,8 +16,6 @@ import DynamicContent from './dynamic_content.vue';
|
|||
import StatusIcon from './status_icon.vue';
|
||||
import ActionButtons from './action_buttons.vue';
|
||||
|
||||
const FETCH_TYPE_COLLAPSED = 'collapsed';
|
||||
const FETCH_TYPE_EXPANDED = 'expanded';
|
||||
const WIDGET_PREFIX = 'Widget';
|
||||
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.';
|
||||
|
@ -49,15 +47,6 @@ export default {
|
|||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* @param {value.collapsed} Object
|
||||
* @param {value.expanded} Object
|
||||
*/
|
||||
value: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -238,7 +227,7 @@ export default {
|
|||
|
||||
try {
|
||||
if (this.fetchCollapsedData) {
|
||||
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
|
||||
await this.fetch(this.fetchCollapsedData);
|
||||
}
|
||||
} catch {
|
||||
this.summaryError = this.errorText;
|
||||
|
@ -271,7 +260,7 @@ export default {
|
|||
this.contentError = null;
|
||||
|
||||
try {
|
||||
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
|
||||
await this.fetch(this.fetchExpandedData);
|
||||
} catch {
|
||||
this.contentError = this.errorText;
|
||||
|
||||
|
@ -282,7 +271,7 @@ export default {
|
|||
|
||||
this.isLoadingExpandedContent = false;
|
||||
},
|
||||
fetch(handler, dataType) {
|
||||
fetch(handler) {
|
||||
const requests = this.multiPolling ? handler() : [handler];
|
||||
|
||||
const promises = requests.map((request) => {
|
||||
|
@ -319,9 +308,7 @@ export default {
|
|||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then((data) => {
|
||||
this.$emit('input', { ...this.value, [dataType]: this.multiPolling ? data : data[0] });
|
||||
});
|
||||
return Promise.all(promises);
|
||||
},
|
||||
},
|
||||
failedStatusIcon: EXTENSION_ICONS.failed,
|
||||
|
|
|
@ -110,7 +110,7 @@ module Projects
|
|||
|
||||
update_pending_builds
|
||||
|
||||
post_update_hooks(project)
|
||||
post_update_hooks(project, @old_group)
|
||||
rescue Exception # rubocop:disable Lint/RescueException
|
||||
rollback_side_effects
|
||||
raise
|
||||
|
@ -119,7 +119,7 @@ module Projects
|
|||
end
|
||||
|
||||
# Overridden in EE
|
||||
def post_update_hooks(project)
|
||||
def post_update_hooks(project, _old_group)
|
||||
ensure_personal_project_owner_membership(project)
|
||||
invalidate_personal_projects_counts
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.form-group
|
||||
= 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/,
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
{
|
||||
test: /gridstack\/.*\.js$/,
|
||||
include: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
{
|
||||
test: /_worker\.js$/,
|
||||
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
|
|
@ -28,7 +28,7 @@ is recommended when [FIPS mode](../../development/fips_compliance.md) is enabled
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225545) in GitLab 13.12.
|
||||
|
||||
Download a PyPI package file. The [simple API](#group-level-simple-api-entry-point)
|
||||
Download a PyPI package file. The [simple API](#group-level-simple-api-entry-point)
|
||||
usually supplies this URL.
|
||||
|
||||
```plaintext
|
||||
|
|
|
@ -36,7 +36,7 @@ gRPC would carry messages between modules.
|
|||
## Use Packwerk to enforce module boundaries
|
||||
|
||||
Packwerk is a static analyzer that helps defining and enforcing module boundaries
|
||||
in Ruby.
|
||||
in Ruby.
|
||||
|
||||
[In this PoC merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98801)
|
||||
we demonstrate a possible directory structure of the monolith broken down into separate
|
||||
|
|
|
@ -367,7 +367,7 @@ This action is not reversible.
|
|||
|
||||
Any existing CI template, that you share with other projects via `include:` syntax, can be converted to a CI component.
|
||||
|
||||
1. Decide whether you want the component to be part of an existing [components repository](#components-repository),
|
||||
1. Decide whether you want the component to be part of an existing [components repository](#components-repository),
|
||||
if you want to logically group components together. Create and setup a [components repository](#components-repository) otherwise.
|
||||
1. Create a YAML file in the components repository according to the expected [directory structure](#directory-structure).
|
||||
1. Copy the content of the template YAML file into the new component YAML file.
|
||||
|
|
|
@ -154,7 +154,7 @@ to interact with your application, so we need to install and run them.
|
|||
Furthermore, WebdriverIO uses Selenium as a common interface to control different browsers,
|
||||
so we need to install and run Selenium as well. Luckily, the Selenium project provides the Docker images for Firefox
|
||||
[standalone-firefox](https://hub.docker.com/r/selenium/standalone-firefox/) and
|
||||
and for Chrome [standalone-chrome](https://hub.docker.com/r/selenium/standalone-chrome/).
|
||||
and for Chrome [standalone-chrome](https://hub.docker.com/r/selenium/standalone-chrome/).
|
||||
(Since Safari and Internet Explorer/Edge are not open source and
|
||||
not available for Linux, we are unfortunately unable to use those in GitLab CI/CD).
|
||||
|
||||
|
|
|
@ -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.
|
||||
- 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
|
||||
|
||||
|
@ -72,7 +72,7 @@ Please see the `sha_tokenizer` explanation later below for an example.
|
|||
|
||||
#### `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.
|
||||
|
||||
|
@ -81,10 +81,6 @@ Please see the `code` filter for an explanation on how tokens are split.
|
|||
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).
|
||||
|
||||
#### `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
|
||||
|
||||
#### `sha_tokenizer`
|
||||
|
@ -115,27 +111,10 @@ Example:
|
|||
- `'path/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
|
||||
|
||||
- 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
|
||||
- Searches can have their own analyzers. Remember to check when editing analyzers.
|
||||
- `Character` filters (as opposed to token filters) always replace the original character. These filters can hinder exact searches.
|
||||
|
||||
## Zero downtime reindexing with multiple indices
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ For versioning and upgrade details, see our [Release and Maintenance policy](../
|
|||
|
||||
GitLab self-managed packages are semantically versioned and follow our [maintenance policy](../../policy/maintenance.md). This process applies to features and APIs that are generally available, not beta or experimental.
|
||||
|
||||
This maintenance policy is in place to allow our customers to prepare for disruptive changes by establishing a clear and predictable pattern that is widely used in the software industry. For many of our customers, GitLab is a business-critical application and surprising changes can cause damages and erode trust.
|
||||
This maintenance policy is in place to allow our customers to prepare for disruptive changes by establishing a clear and predictable pattern that is widely used in the software industry. For many of our customers, GitLab is a business-critical application and surprising changes can cause damages and erode trust.
|
||||
|
||||
Introducing breaking changes in minor releases is against policy because it can disrupt our customers and introduces an element of randomness that requires customers to check for breaking changes every minor release to ensure that their business is not impacted. This does not align with our goal [to make it as easy as possible for customers to do business with GitLab](https://about.gitlab.com/company/yearlies/#fy24-yearlies) and is strongly discouraged.
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ In general, for topic titles:
|
|||
- Follow [capitalization](../styleguide/index.md#topic-titles) guidelines.
|
||||
- Do not repeat text from earlier topic titles. For example, if the page is about merge requests,
|
||||
instead of `Troubleshooting merge requests`, use only `Troubleshooting`.
|
||||
- Avoid using hyphens to separate information.
|
||||
- Avoid using hyphens to separate information.
|
||||
For example, instead of `Internal analytics - Architecture`, use `Internal analytics architecture` or `Architecture of internal analytics`.
|
||||
|
||||
See also [guidelines for heading levels in Markdown](../styleguide/index.md#heading-levels-in-markdown).
|
||||
|
|
|
@ -13,7 +13,7 @@ We extract libraries from our codebase when their functionality
|
|||
is highly isolated and we want to use them in other applications
|
||||
ourselves or we think it would benefit the wider community.
|
||||
|
||||
Extracting code to a gem also ensures that the gem does not contain any hidden
|
||||
Extracting code to a gem also ensures that the gem does not contain any hidden
|
||||
dependencies on our application code.
|
||||
|
||||
Gems should always be used when implementing functionality that can be considered isolated,
|
||||
|
@ -36,7 +36,7 @@ You can always start by creating a new Gem [in the same repo](#in-the-same-repo)
|
|||
to be used by a wider community.
|
||||
|
||||
WARNING:
|
||||
To prevent malicious actors from name-squatting the extracted Gems, follow the instructions
|
||||
To prevent malicious actors from name-squatting the extracted Gems, follow the instructions
|
||||
to [reserve a gem name](#reserve-a-gem-name).
|
||||
|
||||
## Advantages of using Gems
|
||||
|
@ -81,7 +81,7 @@ and prevents complexity (coordinating changes across repos, new permissions, mul
|
|||
Gems stored in the same repo should be referenced in `Gemfile` with the `path:` syntax.
|
||||
|
||||
WARNING:
|
||||
To prevent malicious actors from name-squatting the extracted Gems, follow the instructions
|
||||
To prevent malicious actors from name-squatting the extracted Gems, follow the instructions
|
||||
to [reserve a gem name](#reserve-a-gem-name).
|
||||
|
||||
### Create and use a new Gem
|
||||
|
|
|
@ -680,7 +680,7 @@ on, check out our [self-service framework](geo/framework.md).
|
|||
|
||||
After triggering a successful [e2e:package-and-test-ee](testing_guide/end_to_end/index.md#using-the-package-and-test-job) pipeline, you can manually trigger a job named `GET:Geo`:
|
||||
|
||||
1. In the [GitLab project](https://gitlab.com/gitlab-org/gitlab), select the **Pipelines** tab of a merge request.
|
||||
1. In the [GitLab project](https://gitlab.com/gitlab-org/gitlab), select the **Pipelines** tab of a merge request.
|
||||
1. Select the `Stage: qa` stage on the latest pipeline to expand and list all the related jobs.
|
||||
1. Select `trigger-omnibus` to view the [Omnibus GitLab Mirror](https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror) pipeline corresponding to the merge request.
|
||||
1. The `GET:Geo` job can be found and triggered under the `trigger-qa` stage.
|
||||
|
|
|
@ -577,7 +577,7 @@ All other attributes are optional.
|
|||
|
||||
##### SAST
|
||||
|
||||
The `location` of a SAST vulnerability must have a `file` that gives the path of the affected file and
|
||||
The `location` of a SAST vulnerability must have a `file` that gives the path of the affected file and
|
||||
a `start_line` field with the affected line number.
|
||||
It may also have an `end_line`, a `class`, and a `method`.
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ or even a result of a public holiday in some regions of the world with a larger
|
|||
1. Check (via [Grafana explore tab](https://dashboards.gitlab.net/explore) ) following Prometheus counters `gitlab_snowplow_events_total`, `gitlab_snowplow_failed_events_total` and `gitlab_snowplow_successful_events_total` to see how many events were fired correctly from GitLab.com. Example query to use `sum(rate(gitlab_snowplow_successful_events_total{env="gprd"}[5m])) / sum(rate(gitlab_snowplow_events_total{env="gprd"}[5m]))` would chart rate at which number of good events rose in comparison to events sent in total. If number drops from 1 it means that problem might be in communication between GitLab and AWS collectors fleet.
|
||||
1. Check [logs in Kibana](https://log.gprd.gitlab.net/app/discover#) and filter with `{ "query": { "match_phrase": { "json.message": "failed to be reported to collector at" } } }` if there are some failed events logged
|
||||
|
||||
We conducted an investigation into an unexpected drop in snowplow events volume.
|
||||
We conducted an investigation into an unexpected drop in snowplow events volume.
|
||||
|
||||
GitLab team members can view more information in this confidential issue: `https://gitlab.com/gitlab-org/gitlab/-/issues/335206`
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ A new subscription must be purchased and applied as needed.
|
|||
## Choose a GitLab tier
|
||||
|
||||
Pricing is [tier-based](https://about.gitlab.com/pricing/), allowing you to choose
|
||||
the features that fit your budget.
|
||||
the features that fit your budget.
|
||||
|
||||
For more details, see
|
||||
[a comparison of self-managed features available in each tier](https://about.gitlab.com/pricing/feature-comparison/).
|
||||
|
|
|
@ -501,7 +501,7 @@ To run an existing on-demand scan:
|
|||
1. In the scan's row, select **Run scan**.
|
||||
|
||||
If the branch saved in the scan no longer exists, you must:
|
||||
|
||||
|
||||
1. [Edit the scan](#edit-an-on-demand-scan).
|
||||
1. Select a new branch.
|
||||
1. Save the edited scan.
|
||||
|
|
|
@ -22,7 +22,7 @@ GitLab analyzers make an effort to fit the severity descriptions below, but they
|
|||
|
||||
## Critical severity
|
||||
|
||||
Vulnerabilities identified at the Critical Severity level should be investigated immediately. Vulnerabilities at this level assume exploitation of the flaw could lead to full system or data compromise. Examples of critical severity flaws are Command/Code Injection and SQL Injection. Typically these flaws are rated with CVSS 3.1 between 9.0-10.0.
|
||||
Vulnerabilities identified at the Critical Severity level should be investigated immediately. Vulnerabilities at this level assume exploitation of the flaw could lead to full system or data compromise. Examples of critical severity flaws are Command/Code Injection and SQL Injection. Typically these flaws are rated with CVSS 3.1 between 9.0-10.0.
|
||||
|
||||
## High severity
|
||||
|
||||
|
|
|
@ -63,6 +63,24 @@ This page shows groups that you are a member of:
|
|||
- Through membership of a subgroup's parent group.
|
||||
- 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
|
||||
|
||||
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. Select **Your work**.
|
||||
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
|
||||
|
||||
|
|
|
@ -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. 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
|
||||
|
||||
|
|
|
@ -203,6 +203,7 @@ module.exports = (path, options = {}) => {
|
|||
'@gitlab/favicon-overlay',
|
||||
'@gitlab/cluster-client',
|
||||
'bootstrap-vue',
|
||||
'gridstack',
|
||||
'three',
|
||||
'monaco-editor',
|
||||
'monaco-yaml',
|
||||
|
|
|
@ -73,7 +73,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|URL:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Unmodified Response"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@
|
|||
"gettext-parser": "^6.0.0",
|
||||
"graphql": "^15.7.2",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"gridstack": "^7.3.0",
|
||||
"gridstack": "^8.3.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"immer": "^9.0.15",
|
||||
"ipaddr.js": "^1.9.1",
|
||||
|
|
|
@ -27,13 +27,6 @@ module QA
|
|||
Page::Project::PipelineEditor::New.perform(&:create_new_ci)
|
||||
|
||||
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
|
||||
show.set_source_branch(SecureRandom.hex(10))
|
||||
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(:other_branch) { 'test' }
|
||||
let(:branch_with_invalid_ci) { 'despair' }
|
||||
let(:branch_without_ci) { 'empty' }
|
||||
|
||||
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, 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, 'index.js', "file", message: 'New js file', branch_name: branch_without_ci)
|
||||
|
||||
visit project_ci_pipeline_editor_path(project)
|
||||
wait_for_requests
|
||||
|
@ -62,6 +64,31 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
|||
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
|
||||
before do
|
||||
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
|
||||
|
||||
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
|
||||
page.within('[data-testid="branch-selector"]') do
|
||||
expect(page).to have_content(default_branch)
|
||||
|
@ -195,12 +213,20 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
|||
end
|
||||
|
||||
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
|
||||
visit project_ci_pipeline_editor_path(project)
|
||||
wait_for_requests
|
||||
|
||||
# close button for the popover
|
||||
find('[data-testid="close-button"]').click
|
||||
end
|
||||
|
||||
it_behaves_like 'default branch switcher behavior'
|
||||
|
@ -262,6 +288,24 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
|
|||
end
|
||||
|
||||
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
|
||||
find('#commit-message').set('message', clear: :backspace)
|
||||
find('#source-branch-field').set('new_branch', clear: :backspace)
|
||||
|
|
|
@ -382,7 +382,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
|
|||
},
|
||||
{
|
||||
type: 'tree',
|
||||
name: 'ee/lib/…/…/…/longtreenametomakepath',
|
||||
name: 'ee/lib/ee/gitlab/checks/longtreenametomakepath',
|
||||
tree: [
|
||||
{
|
||||
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', () => {
|
||||
it('should replaces whitespaces with underscore and convert to lower case', () => {
|
||||
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', () => {
|
||||
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: [] } };
|
||||
createComponent({ propsData: { fetchCollapsedData: () => Promise.resolve(mockData) } });
|
||||
const fetchCollapsedData = jest.fn().mockResolvedValue(mockData);
|
||||
createComponent({ propsData: { fetchCollapsedData } });
|
||||
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 = {
|
||||
headers: {},
|
||||
status: HTTP_STATUS_OK,
|
||||
|
@ -140,22 +141,22 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
|||
data: { vulnerabilities: [{ vuln: 2 }] },
|
||||
};
|
||||
|
||||
const fetchCollapsedData = [
|
||||
jest.fn().mockResolvedValue(mockData1),
|
||||
jest.fn().mockResolvedValue(mockData2),
|
||||
];
|
||||
|
||||
createComponent({
|
||||
propsData: {
|
||||
multiPolling: true,
|
||||
fetchCollapsedData: () => [
|
||||
() => Promise.resolve(mockData1),
|
||||
() => Promise.resolve(mockData2),
|
||||
],
|
||||
fetchCollapsedData: () => fetchCollapsedData,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
||||
collapsed: [mockData1.data, mockData2.data],
|
||||
expanded: null,
|
||||
});
|
||||
expect(fetchCollapsedData[0]).toHaveBeenCalledTimes(1);
|
||||
expect(fetchCollapsedData[1]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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 fetchCollapsedData = jest.fn().mockResolvedValue(mockDataCollapsed);
|
||||
|
||||
await createComponent({
|
||||
propsData: {
|
||||
isCollapsible: true,
|
||||
fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
|
||||
fetchCollapsedData,
|
||||
fetchExpandedData,
|
||||
},
|
||||
});
|
||||
|
@ -340,17 +342,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
|||
findToggleButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
// First fetches the collapsed data
|
||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
||||
collapsed: mockDataCollapsed.data,
|
||||
expanded: null,
|
||||
});
|
||||
|
||||
// Then fetches the expanded data
|
||||
expect(wrapper.emitted('input')[1][0]).toEqual({
|
||||
collapsed: null,
|
||||
expanded: mockDataExpanded.data,
|
||||
});
|
||||
expect(fetchCollapsedData).toHaveBeenCalledTimes(1);
|
||||
expect(fetchExpandedData).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Triggering a click does not call the expanded data again
|
||||
findToggleButton().vm.$emit('click');
|
||||
|
@ -371,14 +364,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
|||
findToggleButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
// First fetches the collapsed data
|
||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
||||
collapsed: undefined,
|
||||
expanded: null,
|
||||
});
|
||||
|
||||
expect(fetchExpandedData).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.emitted('input')).toHaveLength(1); // Should not an emit an input call because request failed
|
||||
|
||||
findToggleButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
|
|
@ -9,151 +9,9 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
|||
|
||||
subject { described_class.new(result, arguments) }
|
||||
|
||||
context 'when input data is valid' 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).to be_valid
|
||||
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config has a syntax error' do
|
||||
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') }
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'surfaces an error about invalid config' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.error_message).to eq subject.errors.first
|
||||
expect(subject.errors).to include 'Invalid configuration format'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when spec header is invalid' do
|
||||
let(:header) do
|
||||
{ spec: { arguments: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.website ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'surfaces an error about invalid header' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.error_message).to eq subject.errors.first
|
||||
expect(subject.errors).to include('header:spec config contains unknown keys: arguments')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation block is invalid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.abc ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'correctly interpolates the config' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors).to include 'unknown interpolation key: `abc`'
|
||||
expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`'
|
||||
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
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
|
||||
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 'interpolation interrupted by errors, unknown interpolation key: `something`'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_hash' do
|
||||
context 'when interpolation is not used' do
|
||||
let(:result) do
|
||||
::Gitlab::Ci::Config::Yaml::Result.new(config: content)
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy production' }
|
||||
end
|
||||
|
||||
let(:arguments) { nil }
|
||||
|
||||
it 'returns original content' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).not_to be_interpolated
|
||||
expect(subject.to_hash).to eq(content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation is available' do
|
||||
# Remove shared examples when ci_interpolation_inputs_refactor is removed.
|
||||
shared_examples 'interpolator' do
|
||||
context 'when input data is valid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
@ -166,12 +24,189 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli
|
|||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'correctly interpolates content' do
|
||||
it 'correctly interpolates the config' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).to be_interpolated
|
||||
expect(subject).to be_valid
|
||||
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config has a syntax error' do
|
||||
let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') }
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'surfaces an error about invalid config' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.error_message).to eq subject.errors.first
|
||||
expect(subject.errors).to include 'Invalid configuration format'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when spec header is invalid' do
|
||||
let(:header) do
|
||||
{ spec: { arguments: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.website ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'surfaces an error about invalid header' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.error_message).to eq subject.errors.first
|
||||
expect(subject.errors).to include('header:spec config contains unknown keys: arguments')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation block is invalid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.abc ]]' }
|
||||
end
|
||||
|
||||
let(:arguments) do
|
||||
{ website: 'gitlab.com' }
|
||||
end
|
||||
|
||||
it 'correctly interpolates the config' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors).to include 'unknown interpolation key: `abc`'
|
||||
expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multiple interpolation blocks are invalid' do
|
||||
let(:header) do
|
||||
{ spec: { inputs: { website: nil } } }
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' }
|
||||
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 'interpolation interrupted by errors, unknown interpolation key: `something`'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_hash' do
|
||||
context 'when interpolation is not used' do
|
||||
let(:result) do
|
||||
::Gitlab::Ci::Config::Yaml::Result.new(config: content)
|
||||
end
|
||||
|
||||
let(:content) do
|
||||
{ test: 'deploy production' }
|
||||
end
|
||||
|
||||
let(:arguments) { nil }
|
||||
|
||||
it 'returns original content' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject.to_hash).to eq(content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when interpolation is available' 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 content' do
|
||||
subject.interpolate!
|
||||
|
||||
expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' })
|
||||
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
|
||||
|
|
|
@ -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(
|
||||
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.
|
||||
config.module.rules = [
|
||||
|
|
|
@ -6746,10 +6746,10 @@ graphql@^15.7.2:
|
|||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
|
||||
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
|
||||
|
||||
gridstack@^7.3.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-7.3.0.tgz#7b32395edcd885bc39b84068ac86f2831f7a2451"
|
||||
integrity sha512-JKZgsHzm1ljkn1NnBZpf8j4NDOBCXTuw0m1ZC0sr6NKUh0BFWzXAONIxtX1hWGUVeKLj5l1VcmnTwCXw5ypDNw==
|
||||
gridstack@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-8.3.0.tgz#4c79f8b8c4cffeb3664266108e38ed91b3d0f7b4"
|
||||
integrity sha512-RcL2xskAYKOpakvpSwHdKheG7C7YgNY7777C5m+T1JMjSgcmEc3qPBM573l0NuyjMz4Errx1/3p+rMgUfF4+mw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
|
|
Loading…
Reference in New Issue