Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-02-25 12:07:21 +00:00
parent c82f9ecc2a
commit 814b3eca14
65 changed files with 1175 additions and 176 deletions

View File

@ -40,6 +40,7 @@ RSpec/SpecFilePathFormat:
- 'spec/services/ci/create_pipeline_service/environment_spec.rb'
- 'spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb'
- 'spec/services/ci/create_pipeline_service/include_spec.rb'
- 'spec/services/ci/create_pipeline_service/inputs_spec.rb'
- 'spec/services/ci/create_pipeline_service/limit_active_jobs_spec.rb'
- 'spec/services/ci/create_pipeline_service/logger_spec.rb'
- 'spec/services/ci/create_pipeline_service/merge_requests_spec.rb'

View File

@ -349,7 +349,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'ee/app/workers/search/elastic/metrics_update_cron_worker.rb'
- 'ee/app/workers/search/index_curation_worker.rb'
- 'ee/app/workers/search/zoekt/namespace_indexer_worker.rb'
- 'ee/app/workers/search/zoekt/namespace_initial_indexing_worker.rb'
- 'ee/app/workers/search/zoekt/scheduling_worker.rb'
- 'ee/app/workers/security/create_orchestration_policy_worker.rb'
- 'ee/app/workers/security/orchestration_policy_rule_schedule_worker.rb'

View File

@ -153,11 +153,13 @@ export default {
</gl-alert>
<gl-form-group
:label="__('Name')"
label-for="comment-template-name-input"
:state="isNameValid"
:invalid-feedback="__('Please enter a name for the comment template.')"
data-testid="comment-template-name-form-group"
>
<gl-form-input
id="comment-template-name-input"
v-model="updateCommentTemplate.name"
:placeholder="__('Enter a name for your comment template')"
data-testid="comment-template-name-input"

View File

@ -20,12 +20,23 @@ export default {
<template>
<multi-step-form-template :title="option.title" :current-step="2" :steps-total="2">
<template #next>
<gl-button category="primary" variant="confirm" :disabled="true">
<gl-button
category="primary"
variant="confirm"
:disabled="true"
data-testid="create-project-button"
@click="$emit('create-project')"
>
{{ __('Create project') }}
</gl-button>
</template>
<template #back>
<gl-button category="primary" variant="default" @click="$emit('back')">
<gl-button
category="primary"
data-testid="create-project-back-button"
variant="default"
@click="$emit('back')"
>
{{ __('Go back') }}
</gl-button>
</template>

View File

@ -20,12 +20,22 @@ export default {
<template>
<multi-step-form-template :title="option.title" :current-step="2" :steps-total="2">
<template #next>
<gl-button category="primary" variant="confirm" :disabled="true">
<gl-button
category="primary"
variant="confirm"
:disabled="true"
data-testid="create-cicd-project-button"
>
{{ __('Create project') }}
</gl-button>
</template>
<template #back>
<gl-button category="primary" variant="default" @click="$emit('back')">
<gl-button
category="primary"
variant="default"
data-testid="create-cicd-project-back-button"
@click="$emit('back')"
>
{{ __('Go back') }}
</gl-button>
</template>

View File

@ -18,14 +18,24 @@ export default {
</script>
<template>
<multi-step-form-template :title="option.title" :current-step="2">
<multi-step-form-template :title="option.title" :current-step="2" :steps-total="2">
<template #next>
<gl-button category="primary" variant="confirm" :disabled="true">
<gl-button
category="primary"
variant="confirm"
:disabled="true"
data-testid="import-project-next-button"
>
{{ __('Next step') }}
</gl-button>
</template>
<template #back>
<gl-button category="primary" variant="default" @click="$emit('back')">
<gl-button
category="primary"
variant="default"
data-testid="import-project-back-button"
@click="$emit('back')"
>
{{ __('Go back') }}
</gl-button>
</template>

View File

@ -20,12 +20,22 @@ export default {
<template>
<multi-step-form-template :title="option.title" :current-step="2" :steps-total="3">
<template #next>
<gl-button category="primary" variant="confirm" :disabled="true">
<gl-button
category="primary"
variant="confirm"
:disabled="true"
data-testid="template-project-next-button"
>
{{ __('Next step') }}
</gl-button>
</template>
<template #back>
<gl-button category="primary" variant="default" @click="$emit('back')">
<gl-button
category="primary"
variant="default"
data-testid="template-project-back-button"
@click="$emit('back')"
>
{{ __('Go back') }}
</gl-button>
</template>

View File

@ -485,10 +485,7 @@ module Ci
end
def archived?
return true if degenerated?
archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
archive_builds_older_than.present? && created_at < archive_builds_older_than
degenerated? || super
end
def playable?

View File

@ -714,9 +714,16 @@ module Ci
end
def retryable?
return false if archived?
retryable_builds.any?
end
def archived?
archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
archive_builds_older_than.present? && created_at < archive_builds_older_than
end
def cancelable?
cancelable_statuses.any? && internal_pipeline?
end

View File

@ -290,7 +290,7 @@ class CommitStatus < Ci::ApplicationRecord
end
def archived?
false
pipeline.archived?
end
def stuck?

View File

@ -26,6 +26,10 @@ module Ci
can?(:read_build, @subject.project)
end
condition(:archived, scope: :subject) do
@subject.archived?
end
# Allow reading builds for external pipelines regardless of whether CI/CD is disabled
overrides :read_build
rule { project_allows_read_build | (external_pipeline & can?(:reporter_access)) }.policy do
@ -37,7 +41,7 @@ module Ci
prevent :read_pipeline
end
rule { protected_ref }.policy do
rule { archived | protected_ref }.policy do
prevent :update_pipeline
prevent :cancel_pipeline
end

View File

@ -63,7 +63,12 @@ module Ci
#
# @return [Ci::Pipeline] The created Ci::Pipeline object.
# rubocop: disable Metrics/ParameterLists, Metrics/AbcSize
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
def execute(
source,
ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil,
external_pull_request: nil, bridge: nil, inputs: {},
**options, &block
)
@logger = build_logger
@command_logger = Gitlab::Ci::Pipeline::CommandLogger.new
@pipeline = Ci::Pipeline.new
@ -93,6 +98,7 @@ module Ci
bridge: bridge,
logger: @logger,
partition_id: params[:partition_id],
inputs: ::Feature.enabled?(:ci_inputs_for_pipelines, project) ? inputs : {},
**extra_options(**options))
@pipeline.readonly! if command.readonly?

View File

@ -11,6 +11,8 @@ module Ci
end
def execute(&transition)
return forbidden unless allowed?
transition ||= ->(job) { job.enqueue! }
Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job') do |job|
@ -24,5 +26,15 @@ module Ci
job
end
private
def allowed?
::Ability.allowed?(current_user, :update_pipeline, job.pipeline)
end
def forbidden
ServiceResponse.error(message: 'Forbidden', reason: :forbidden)
end
end
end

View File

@ -3,16 +3,17 @@
module Ci
module Pipelines
class UpdateMetadataService
def initialize(pipeline, params)
def initialize(pipeline, current_user:, params: {})
@pipeline = pipeline
@current_user = current_user
@params = params
end
def execute
return forbidden unless allowed?
metadata = pipeline.pipeline_metadata
metadata = pipeline.build_pipeline_metadata(project: pipeline.project) if metadata.nil?
params[:name] = params[:name].strip if params.key?(:name)
if metadata.update(params)
@ -25,7 +26,15 @@ module Ci
private
attr_reader :pipeline, :params
attr_reader :pipeline, :current_user, :params
def allowed?
::Ability.allowed?(current_user, :update_pipeline, pipeline)
end
def forbidden
ServiceResponse.error(message: 'Forbidden', reason: :forbidden)
end
end
end
end

View File

@ -10,14 +10,14 @@ module Ci
pipeline.ensure_scheduling_type!
builds_relation(pipeline).find_each do |build|
next unless can_be_retried?(build)
builds_relation(pipeline).find_each do |job|
next unless can_be_retried?(job)
Ci::RetryJobService.new(project, current_user).clone!(build)
Ci::RetryJobService.new(project, current_user).clone!(job)
end
pipeline.processables.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped, name: 'ci_retry_pipeline') { |build| build.process(current_user) }
retry_optimistic_lock(skipped, name: 'ci_retry_pipeline') { |job| job.process(current_user) }
end
pipeline.reset_source_bridge!(current_user)
@ -47,8 +47,8 @@ module Ci
pipeline.retryable_builds.preload_needs
end
def can_be_retried?(build)
can?(current_user, :update_build, build)
def can_be_retried?(job)
can?(current_user, :update_build, job) && job.retryable?
end
def start_pipeline(pipeline)

View File

@ -3,13 +3,12 @@
%fieldset
.form-group
= f.label :notes_create_limit, _('Maximum requests per minute'), class: 'label-bold'
= f.label :notes_create_limit, _('Maximum requests per minute')
= f.number_field :notes_create_limit, class: 'form-control gl-form-input'
.form-group
= f.label :notes_create_limit_allowlist, _('Users to exclude from the rate limit'), class: 'label-bold'
= f.text_area :notes_create_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'note-create-limits-allowlist-field-description' }
= f.label :notes_create_limit_allowlist_raw, _('Users to exclude from the rate limit')
= f.text_area :notes_create_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5
.form-text.gl-text-subtle{ id: 'note-create-limits-allowlist-field-description' }
= _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
= f.submit _('Save changes'), pajamas_button: true

View File

@ -7,17 +7,17 @@
_('Enable rate limiting for requests to the specified paths'),
help_text: _('Helps reduce request volume for protected paths.')
.form-group
= f.label :throttle_protected_paths_requests_per_period, _('Maximum requests per period per user'), class: 'label-bold'
= f.label :throttle_protected_paths_requests_per_period, _('Maximum requests per period per user')
= f.number_field :throttle_protected_paths_requests_per_period, class: 'form-control gl-form-input'
.form-group
= f.label :throttle_protected_paths_period_in_seconds, _('Rate limit period (in seconds)'), class: 'label-bold'
= f.label :throttle_protected_paths_period_in_seconds, _('Rate limit period (in seconds)')
= f.number_field :throttle_protected_paths_period_in_seconds, class: 'form-control gl-form-input'
.form-group
= f.label :protected_paths, class: 'label-bold' do
= f.label :protected_paths do
= _('Paths with rate limiting for POST requests')
= f.text_area :protected_paths_raw, placeholder: '/users/sign_in,/users/password', class: 'form-control gl-form-input', rows: 10
.form-group
= f.label :protected_paths_for_get_request, class: 'label-bold' do
= f.label :protected_paths_for_get_request_raw do
= _('Paths with rate limiting for GET requests')
= f.text_area :protected_paths_for_get_request_raw, class: 'form-control gl-form-input', rows: 10
%span.form-text.gl-text-subtle

View File

@ -3,20 +3,19 @@
%fieldset
.form-group
= f.label :search_rate_limit, _('Maximum number of requests per minute for an authenticated user'), class: 'label-bold'
= f.label :search_rate_limit, _('Maximum number of requests per minute for an authenticated user')
.form-text.gl-text-subtle
= _("Set this number to 0 to disable the limit.")
= f.number_field :search_rate_limit, class: 'form-control gl-form-input'
.form-group
= f.label :search_rate_limit_unauthenticated, _('Maximum number of requests per minute for an unauthenticated IP address'), class: 'label-bold'
= f.label :search_rate_limit_unauthenticated, _('Maximum number of requests per minute for an unauthenticated IP address')
= f.number_field :search_rate_limit_unauthenticated, class: 'form-control gl-form-input'
.form-group
= f.label :search_rate_limit_allowlist, _('Users to exclude from the rate limit'), class: 'label-bold'
= f.text_area :search_rate_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'search-rate-limit-allowlist-field-description' }
= f.label :search_rate_limit_allowlist_raw, _('Users to exclude from the rate limit')
= f.text_area :search_rate_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5
.form-text.gl-text-subtle{ id: 'search-rate-limit-allowlist-field-description' }
= _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
= f.submit _('Save changes'), pajamas_button: true

View File

@ -4,7 +4,7 @@
= render ::Layouts::PageHeadingComponent.new(_('Projects')) do |c|
- c.with_actions do
= render Pajamas::ButtonComponent.new(href: starred_explore_projects_path, variant: :link, button_options: { class: 'gl-m-2' }) do
= render Pajamas::ButtonComponent.new(href: starred_explore_projects_path, variant: :confirm, category: :tertiary) do
= _("Explore projects")
- if current_user.can_create_project?

View File

@ -35,7 +35,7 @@
.gl-mb-5
= render Pajamas::CardComponent.new do |c|
- c.with_body do
%p.gl-mt-0.gl-mb-3.gl-font-bold
%h3.gl-mt-0.gl-mt-0.gl-mb-3.gl-font-bold.gl-leading-reset.gl-text-base.gl-text-default
= _("Can't scan the code?")
%p.gl-mt-0.gl-mb-3
= _("To add the entry manually, provide the following details to the application on your phone.")

View File

@ -0,0 +1,9 @@
---
name: ci_inputs_for_pipelines
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/16321
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182125
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/520290
milestone: '17.10'
group: group::pipeline authoring
type: wip
default_enabled: false

View File

@ -825,8 +825,6 @@
- 1
- - search_zoekt_namespace_indexer
- 1
- - search_zoekt_namespace_initial_indexing
- 1
- - search_zoekt_node_with_negative_unclaimed_storage_event
- 1
- - search_zoekt_orphaned_index_event

View File

@ -4278,20 +4278,6 @@ CREATE TABLE loose_foreign_keys_deleted_records (
)
PARTITION BY LIST (partition);
CREATE TABLE merge_request_diff_commits_b5377a7a34 (
authored_date timestamp without time zone,
committed_date timestamp without time zone,
sha bytea NOT NULL,
message text,
trailers jsonb DEFAULT '{}'::jsonb NOT NULL,
commit_author_id bigint,
committer_id bigint,
merge_request_diff_id bigint NOT NULL,
relative_order integer NOT NULL,
project_id bigint
)
PARTITION BY RANGE (merge_request_diff_id);
CREATE TABLE p_batched_git_ref_updates_deletions (
id bigint NOT NULL,
project_id bigint NOT NULL,
@ -16026,6 +16012,20 @@ CREATE TABLE merge_request_diff_commits (
committer_id bigint
);
CREATE TABLE merge_request_diff_commits_b5377a7a34 (
authored_date timestamp without time zone,
committed_date timestamp without time zone,
sha bytea NOT NULL,
message text,
trailers jsonb DEFAULT '{}'::jsonb NOT NULL,
commit_author_id bigint,
committer_id bigint,
merge_request_diff_id bigint NOT NULL,
relative_order integer NOT NULL,
project_id bigint
)
PARTITION BY RANGE (merge_request_diff_id);
CREATE TABLE merge_request_diff_details (
merge_request_diff_id bigint NOT NULL,
verification_retry_at timestamp with time zone,

View File

@ -156,7 +156,7 @@ After developer tools have been enabled, obtain a session cookie as follows:
1. Select the `results?request_id=<some-request-id>` request on the left hand side.
1. The session cookie is displayed under the `Request Headers` section of the `Headers` panel. Right-click on the cookie value and select `Copy value`.
![Obtaining a session cookie for request](img/obtaining-a-session-cookie-for-request_v14_3.png)
![Viewing a session cookie in the Developer Tools panel of a browser](img/obtaining-a-session-cookie-for-request_v14_3.png)
You have the value of the session cookie copied to your clipboard, for example:
@ -191,11 +191,11 @@ You can then view the database details for this request:
1. Paste the `x-request-id` value into the `request details` field of the [performance bar](../monitoring/performance/performance_bar.md) and press <kbd>Enter/Return</kbd>. This example uses the `x-request-id` value `01FGN8P881GF2E5J91JYA338Y3`, returned by the above response:
![Paste request ID into progress bar](img/paste-request-id-into-progress-bar_v14_3.png)
![The request details field of the performance bar containing an example value](img/paste-request-id-into-progress-bar_v14_3.png)
1. A new request is inserted into the `Request Selector` dropdown list on the right-hand side of the Performance Bar. Select the new request to view the metrics of the API request:
![Select request ID from request selector drop down menu](img/select-request-id-from-request-selector-drop-down-menu_v14_3.png)
![A highlighted example request in an open Request Selector dropdown list](img/select-request-id-from-request-selector-drop-down-menu_v14_3.png)
<!-- vale gitlab_base.Substitutions = NO -->
1. Select the `pg` link in the Progress Bar to view the database queries executed by the API request:

View File

@ -316,7 +316,7 @@ administrators can allow top-level group Owners to create service accounts.
{{< details >}}
- Tier: Ultimate
- Offering: GitLab Self-Managed
- Offering: GitLab Self-Managed, GitLab Dedicated
{{< /details >}}
@ -330,6 +330,7 @@ administrators can allow top-level group Owners to create service accounts.
The availability of the extended maximum allowable lifetime limit is controlled by a feature flag.
For more information, see the history.
The feature flag is not available on GitLab Dedicated.
{{< /alert >}}
@ -359,6 +360,7 @@ After a lifetime for SSH keys is set, GitLab:
- Requires users to set an expiration date that is no later than the allowed lifetime on new SSH keys. The maximum allowed lifetime is:
- 365 days by default.
- 400 days, if you enable the `buffered_token_expiration_limit` feature flag.
This extended limit is not available on GitLab Dedicated.
- Applies the lifetime restriction to existing SSH keys. Keys with no expiry or a lifetime
greater than the maximum immediately become invalid.
@ -373,7 +375,7 @@ When a user's SSH key becomes invalid they can delete and re-add the same key ag
{{< details >}}
- Tier: Ultimate
- Offering: GitLab Self-Managed
- Offering: GitLab Self-Managed, GitLab Dedicated
{{< /details >}}
@ -387,6 +389,7 @@ When a user's SSH key becomes invalid they can delete and re-add the same key ag
The availability of the extended maximum allowable lifetime limit is controlled by a feature flag.
For more information, see the history.
The feature flag is not available on GitLab Dedicated.
{{< /alert >}}
@ -397,11 +400,13 @@ This lifetime is not a requirement, and can be set to any value greater than 0 a
- 365 days by default.
- 400 days, if you enable the `buffered_token_expiration_limit` feature flag.
This extended limit is not available on GitLab Dedicated.
If this setting is left blank, the default allowable lifetime of access tokens is:
- 365 days by default.
- 400 days, if you enable the `buffered_token_expiration_limit` feature flag.
This extended limit is not available on GitLab Dedicated.
Access tokens are the only tokens needed for programmatic access to GitLab.
However, organizations with security requirements may want to enforce more protection by

View File

@ -315,3 +315,12 @@ You can configure this in either of the following ways:
```
This configuration ensures the AI Gateway can properly cache HuggingFace models while respecting OpenShift's security constraints. The exact directory you choose may depend on your specific OpenShift configuration and security policies.
### Self-signed certificate error
A `[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain` error is logged by the AI gateway
when the gateway tries to connect to a GitLab instance using either a certificate signed by a custom certificate authority (CA), or a self-signed certificate:
- The use of custom CA certificates in the Helm chart configuration when deploying the AI gateway is not supported. For more information, see [issue 3](https://gitlab.com/gitlab-org/charts/ai-gateway-helm-chart/-/issues/3). Use the [workaround](https://gitlab.com/gitlab-org/charts/ai-gateway-helm-chart/-/issues/3#workaround) detailed in this issue.
- The use of a self-signed certificate by the GitLab instance is not supported. For more information, see [issue 799](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/issues/799).

View File

@ -235,7 +235,7 @@ To create a Jira Cloud API token:
To copy the API token, select **Copy**.
## Migrate from Jira Server to Jira Cloud
## Migrate from one Jira site to another
{{< history >}}
@ -243,17 +243,10 @@ To copy the API token, select **Copy**.
{{< /history >}}
To migrate from Jira Server to Jira Cloud in GitLab and maintain your Jira issues integration:
To migrate from one Jira site to another in GitLab and maintain your Jira issues integration:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Settings > Integrations**.
1. Select **Jira issues**.
1. In **Web URL**, enter the new Jira site URL (for example, `https://myjirasite.atlassian.net`).
1. In **Email or username**, enter the email registered on your Jira profile.
1. [Create a Jira Cloud API token](#create-a-jira-cloud-api-token), and copy the token value.
1. In **API token or password**, paste the API token value.
1. Optional. Select **Test settings**.
1. Select **Save changes**.
1. Follow the steps in [configure the integration](#configure-the-integration).
1. Enter the new Jira site URL (for example, `https://myjirasite.atlassian.net`).
To update existing Jira issue references in GitLab to use the new Jira site URL, you must
[invalidate the Markdown cache](../../administration/invalidate_markdown_cache.md#invalidate-the-cache).

View File

@ -710,7 +710,7 @@ group(fullPath: "your-group-path") {
Similarly, to request metrics for a project, run:
```graphl
project(fullPath: "your-group-path") {
project(fullPath: "your-project-path") {
valueStreams {
nodes {
id

View File

@ -105,7 +105,7 @@ Prerequisites:
The `CODEOWNERS` file defines who is responsible for code in a GitLab project.
Its purpose is to:
- Automatically assign reviewers to merge requests based on the files changed.
- Define Code Owners for specific files and directories.
- Enforce approval requirements for protected branches.
- Communicate code ownership in a project.

View File

@ -291,7 +291,9 @@ module API
put ':id/pipelines/:pipeline_id/metadata', urgency: :low, feature_category: :continuous_integration do
authorize! :update_pipeline, pipeline
response = ::Ci::Pipelines::UpdateMetadataService.new(pipeline, params.slice(:name)).execute
response = ::Ci::Pipelines::UpdateMetadataService
.new(pipeline, current_user: current_user, params: params.slice(:name))
.execute
if response.success?
present response.payload, with: Entities::Ci::PipelineWithMetadata

View File

@ -22,7 +22,11 @@ module Gitlab
attr_reader :root, :context, :source_ref_path, :source, :logger, :inject_edge_stages, :pipeline_policy_context
# rubocop: disable Metrics/ParameterLists
def initialize(config, project: nil, pipeline: nil, sha: nil, ref: nil, user: nil, parent_pipeline: nil, source: nil, pipeline_config: nil, logger: nil, inject_edge_stages: true, pipeline_policy_context: nil)
def initialize(
config,
project: nil, pipeline: nil, sha: nil, ref: nil, user: nil, parent_pipeline: nil, source: nil,
pipeline_config: nil, logger: nil, inject_edge_stages: true, pipeline_policy_context: nil, inputs: {}
)
@logger = logger || ::Gitlab::Ci::Pipeline::Logger.new(project: project)
@source_ref_path = pipeline&.source_ref_path
@project = project
@ -41,7 +45,7 @@ module Gitlab
Gitlab::Ci::Config::FeatureFlags.with_actor(project) do
@config = self.logger.instrument(:config_expand, once: true) do
expand_config(config)
expand_config(config, inputs)
end
@root = self.logger.instrument(:config_root, once: true) do
@ -148,8 +152,8 @@ module Gitlab
private
def expand_config(config)
build_config(config)
def expand_config(config, inputs)
build_config(config, inputs)
rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
track_and_raise_for_dev_exception(e)
@ -158,11 +162,18 @@ module Gitlab
rescue Gitlab::Ci::Config::External::Context::TimeoutError => e
track_and_raise_for_dev_exception(e)
raise Config::ConfigError, TIMEOUT_MESSAGE
rescue Gitlab::Ci::Config::Yaml::LoadError => e
raise Config::ConfigError, e.message
end
def build_config(config)
def build_config(config, inputs)
initial_config = logger.instrument(:config_yaml_load, once: true) do
Config::Yaml.load!(config)
if inputs.present?
Config::Yaml.load_with_inputs!(config, inputs, @context.variables)
else
Config::Yaml.load!(config)
end
end
initial_config = logger.instrument(:config_external_process, once: true) do

View File

@ -15,6 +15,15 @@ module Gitlab
result.content
end
end
def load_with_inputs!(content, inputs, variables)
Loader.new(content, inputs: inputs, variables: variables).load.then do |result|
raise result.error_class, result.error if !result.valid? && result.error_class.present?
raise LoadError, result.error unless result.valid?
result.content
end
end
end
end
end

View File

@ -13,7 +13,7 @@ module Gitlab
:chat_data, :allow_mirror_update, :bridge, :content, :dry_run, :linting, :logger, :pipeline_policy_context,
# These attributes are set by Chains during processing:
:config_content, :yaml_processor_result, :workflow_rules_result, :pipeline_seed,
:pipeline_config, :partition_id,
:pipeline_config, :partition_id, :inputs,
keyword_init: true
) do
include Gitlab::Utils::StrongMemoize

View File

@ -34,7 +34,8 @@ module Gitlab
pipeline_source: @command.source, pipeline_source_bridge: @command.bridge,
triggered_for_branch: @pipeline.branch?,
ref: @pipeline.ref,
pipeline_policy_context: @command.pipeline_policy_context
pipeline_policy_context: @command.pipeline_policy_context,
inputs: @command.inputs
)
end
end

View File

@ -51,7 +51,8 @@ module Gitlab
user: current_user,
parent_pipeline: parent_pipeline,
pipeline_config: @command.pipeline_config,
logger: logger
logger: logger,
inputs: @command.pipeline_config.inputs_for_pipeline_creation
}
end

View File

@ -24,9 +24,9 @@ module Gitlab
FALLBACK_POLICY_SOURCE = ProjectConfig::SecurityPolicyDefault
def initialize(
def initialize( # rubocop:disable Metrics/ParameterLists -- we need all these parameters
project:, sha:, custom_content: nil, pipeline_source: nil, pipeline_source_bridge: nil,
triggered_for_branch: nil, ref: nil, pipeline_policy_context: nil)
triggered_for_branch: nil, ref: nil, pipeline_policy_context: nil, inputs: nil)
unless pipeline_policy_context&.applying_config_override?
@config = find_source(project: project,
@ -35,7 +35,8 @@ module Gitlab
pipeline_source: pipeline_source,
pipeline_source_bridge: pipeline_source_bridge,
triggered_for_branch: triggered_for_branch,
ref: ref
ref: ref,
inputs: inputs
)
return if @config
@ -52,7 +53,7 @@ module Gitlab
@config = fallback_config if fallback_config.exists?
end
delegate :content, :source, :url, to: :@config, allow_nil: true
delegate :content, :source, :url, :inputs_for_pipeline_creation, to: :@config, allow_nil: true
delegate :internal_include_prepended?, to: :@config
def exists?
@ -62,7 +63,8 @@ module Gitlab
private
def find_source(
project:, sha:, custom_content:, pipeline_source:, pipeline_source_bridge:, triggered_for_branch:, ref:)
project:, sha:, custom_content:, pipeline_source:, pipeline_source_bridge:, triggered_for_branch:, ref:,
inputs:)
STANDARD_SOURCES.each do |source|
source_config = source.new(project: project,
sha: sha,
@ -70,7 +72,8 @@ module Gitlab
pipeline_source: pipeline_source,
pipeline_source_bridge: pipeline_source_bridge,
triggered_for_branch: triggered_for_branch,
ref: ref
ref: ref,
inputs: inputs
)
return source_config if source_config.exists?

View File

@ -9,7 +9,7 @@ module Gitlab
next unless project&.auto_devops_enabled?
template = Gitlab::Template::GitlabCiYmlTemplate.find(template_name)
YAML.dump('include' => [{ 'template' => template.full_name }])
ci_yaml_include({ 'template' => template.full_name })
end
end

View File

@ -76,10 +76,6 @@ module Gitlab
def remote_config_path?
URI::DEFAULT_PARSER.make_regexp(%w[http https]).match?(ci_config_path)
end
def ci_yaml_include(config)
YAML.dump('include' => [config])
end
end
end
end

View File

@ -8,7 +8,7 @@ module Gitlab
def initialize(
project:, sha:, custom_content: nil, pipeline_source: nil, pipeline_source_bridge: nil,
triggered_for_branch: false, ref: nil)
triggered_for_branch: false, ref: nil, inputs: {})
@project = project
@sha = sha
@custom_content = custom_content
@ -16,6 +16,7 @@ module Gitlab
@pipeline_source_bridge = pipeline_source_bridge
@triggered_for_branch = triggered_for_branch
@ref = ref
@inputs = inputs
end
def exists?
@ -41,14 +42,27 @@ module Gitlab
nil
end
def inputs_for_pipeline_creation
# For internal_include_prepended?, we already pass the inputs to the `include` statement.
internal_include_prepended? ? {} : inputs
end
private
attr_reader :project, :sha, :custom_content, :pipeline_source, :pipeline_source_bridge, :triggered_for_branch,
:ref
:ref, :inputs
def ci_config_path
@ci_config_path ||= project.ci_config_path_or_default
end
def ci_yaml_include(config)
YAML.dump('include' => [config.merge(include_inputs)])
end
def include_inputs
{ 'inputs' => inputs }.compact_blank
end
end
end
end

View File

@ -0,0 +1,48 @@
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BlankProjectForm from '~/projects/new_v2/components/blank_project_form.vue';
describe('Blank Project Form', () => {
let wrapper;
const defaultProps = {
option: {
title: 'Import project',
},
};
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(BlankProjectForm, {
propsData: {
...defaultProps,
...props,
},
});
};
beforeEach(() => {
createComponent();
});
const findMultiStepFormTemplate = () => wrapper.findComponent(MultiStepFormTemplate);
const findCreateButton = () => wrapper.findByTestId('create-project-button');
const findBackButton = () => wrapper.findByTestId('create-project-back-button');
it('passes the correct props to MultiStepFormTemplate', () => {
expect(findMultiStepFormTemplate().props()).toMatchObject({
title: defaultProps.option.title,
currentStep: 2,
stepsTotal: 2,
});
});
it('renders the option to Create Project as disabled', () => {
expect(findCreateButton().text()).toBe('Create project');
expect(findCreateButton().props('disabled')).toBe(true);
});
it(`emits the "back" event when the back button is clicked`, () => {
findBackButton().vm.$emit('click');
expect(wrapper.emitted('back')).toHaveLength(1);
});
});

View File

@ -0,0 +1,47 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CICDProjectForm from '~/projects/new_v2/components/ci_cd_project_form.vue';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
describe('CI/CD Project Form', () => {
let wrapper;
const defaultProps = {
option: {
title: 'Import project',
},
};
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(CICDProjectForm, {
propsData: {
...defaultProps,
...props,
},
});
};
beforeEach(() => {
createComponent();
});
const findMultiStepFormTemplate = () => wrapper.findComponent(MultiStepFormTemplate);
const findCreateButton = () => wrapper.findByTestId('create-cicd-project-button');
const findBackButton = () => wrapper.findByTestId('create-cicd-project-back-button');
it('passes the correct props to MultiStepFormTemplate', () => {
expect(findMultiStepFormTemplate().props()).toMatchObject({
title: defaultProps.option.title,
currentStep: 2,
stepsTotal: 2,
});
});
it('renders the option to Create Project', () => {
expect(findCreateButton().text()).toBe('Create project');
});
it(`emits the "back" event when the back button is clicked`, () => {
findBackButton().vm.$emit('click');
expect(wrapper.emitted('back')).toHaveLength(1);
});
});

View File

@ -0,0 +1,47 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ImportProjectForm from '~/projects/new_v2/components/import_project_form.vue';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
describe('Import Project Form', () => {
let wrapper;
const defaultProps = {
option: {
title: 'Import project',
},
};
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(ImportProjectForm, {
propsData: {
...defaultProps,
...props,
},
});
};
beforeEach(() => {
createComponent();
});
const findMultiStepFormTemplate = () => wrapper.findComponent(MultiStepFormTemplate);
const findNextButton = () => wrapper.findByTestId('import-project-next-button');
const findBackButton = () => wrapper.findByTestId('import-project-back-button');
it('passes the correct props to MultiStepFormTemplate', () => {
expect(findMultiStepFormTemplate().props()).toMatchObject({
title: defaultProps.option.title,
currentStep: 2,
stepsTotal: 2,
});
});
it('renders the option to move to Next Step', () => {
expect(findNextButton().text()).toBe('Next step');
});
it(`emits the "back" event when the back button is clicked`, () => {
findBackButton().vm.$emit('click');
expect(wrapper.emitted('back')).toHaveLength(1);
});
});

View File

@ -0,0 +1,47 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TemplateProjectForm from '~/projects/new_v2/components/template_project_form.vue';
import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
describe('Template Project Form', () => {
let wrapper;
const defaultProps = {
option: {
title: 'Template project',
},
};
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(TemplateProjectForm, {
propsData: {
...defaultProps,
...props,
},
});
};
const findMultiStepFormTemplate = () => wrapper.findComponent(MultiStepFormTemplate);
const findNextButton = () => wrapper.findByTestId('template-project-next-button');
const findBackButton = () => wrapper.findByTestId('template-project-back-button');
beforeEach(() => {
createComponent();
});
it('passes the correct props to MultiStepFormTemplate', () => {
expect(findMultiStepFormTemplate().props()).toMatchObject({
title: defaultProps.option.title,
currentStep: 2,
stepsTotal: 3,
});
});
it('renders the option to move to Next Step', () => {
expect(findNextButton().text()).toBe('Next step');
});
it(`emits the "back" event when the back button is clicked`, () => {
findBackButton().vm.$emit('click');
expect(wrapper.emitted('back')).toHaveLength(1);
});
});

View File

@ -5,18 +5,13 @@ import SingleChoiceSelector from '~/vue_shared/components/single_choice_selector
describe('SingleChoice', () => {
let wrapper;
const defaultPropsData = {
checked: 'option',
};
function createComponent({ propsData = {} } = {}) {
const createComponent = (props = {}) => {
wrapper = shallowMount(SingleChoiceSelector, {
propsData: {
...defaultPropsData,
...propsData,
...props,
},
});
}
};
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
@ -25,4 +20,10 @@ describe('SingleChoice', () => {
expect(findRadioGroup().exists()).toBe(true);
});
it('passes the checked prop to GlFormRadioGroup', () => {
const checkedValue = 'test-option';
createComponent({ checked: checkedValue });
expect(findRadioGroup().attributes('checked')).toBe(checkedValue);
});
});

View File

@ -3,17 +3,17 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition do
let(:yaml) do
<<~YAML
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
end
describe '.load!' do
let(:yaml) do
<<~YAML
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
end
subject(:config) { described_class.load!(yaml) }
it 'loads a YAML file' do
@ -37,4 +37,67 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition
end
end
end
describe '.load_with_inputs!' do
let(:yaml) do
<<~YAML
spec:
inputs:
compiler:
default: gcc
optimization_level:
type: number
---
test:
script:
- echo "with compiler $[[ inputs.compiler | expand_vars ]] and level $[[ inputs.optimization_level ]]"
YAML
end
let(:inputs) do
{ compiler: 'g++', optimization_level: 1 }
end
let(:variables) do
[{ key: 'COMPILER', value: 'c++' }]
end
subject(:config) { described_class.load_with_inputs!(yaml, inputs, variables) }
it 'loads a YAML file' do
expect(config).to eq(
test: {
script: ['echo "with compiler g++ and level 1"']
}
)
end
context 'when using a variable in the input value' do
let(:inputs) do
{ compiler: '$COMPILER', optimization_level: 2 }
end
it 'loads the YAML config file, expands the variable and interpolates the input(s)' do
expect(config).to eq(
test: {
script: ['echo "with compiler c++ and level 2"']
}
)
end
end
context 'when given invalid input values' do
let(:inputs) do
{ compiler: 5, optimization_level: 'a string' }
end
it 'raises error' do
expect { config }.to raise_error(
::Gitlab::Ci::Config::Yaml::LoadError,
'`compiler` input: provided value is not a string, `optimization_level` input: provided value is not a number'
)
end
end
end
end

View File

@ -970,6 +970,66 @@ RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_composition do
end
end
context 'when config accepts inputs' do
let_it_be(:complex_inputs_example_path) { 'spec/lib/gitlab/ci/config/yaml/fixtures/complex-included-ci.yml' }
let_it_be(:complex_inputs_example_yaml) { File.read(Rails.root.join(complex_inputs_example_path)) }
let(:inputs) { nil }
subject(:config) do
described_class.new(complex_inputs_example_yaml, inputs: inputs)
end
context 'when not passing required inputs' do
it 'raises Config::ConfigError' do
expect { config }.to raise_error(
described_class::ConfigError,
/required value has not been provided/
)
end
end
context 'when passing required inputs' do
let(:inputs) do
{
deploy_strategy: 'blue-green',
job_stage: 'deploy',
test_script: ['echo "test"'],
test_rules: [
{ if: '$CI_PIPELINE_SOURCE == "push"' }
],
test_framework: '$TEST_FRAMEWORK'
}
end
let(:result) do
{
"my-job-build": a_hash_including(stage: 'deploy'),
"my-job-test": a_hash_including(
stage: 'deploy',
script: [
'echo "Testing with $TEST_FRAMEWORK"',
'if [ false == true ]; then echo "Coverage is enabled"; fi'
]
),
"my-job-test-2": a_hash_including(
stage: 'deploy',
script: ['echo "test"']
),
"my-job-deploy": a_hash_including(
stage: 'deploy',
script: ['echo "Deploying to staging using blue-green strategy"']
)
}
end
it 'returns valid result' do
expect(config.valid?).to be_truthy
expect(config.to_hash).to match(a_hash_including(result))
end
end
end
describe '#workflow_rules' do
subject(:workflow_rules) { config.workflow_rules }

View File

@ -7,7 +7,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: :
let(:pipeline) { build(:ci_pipeline, project: project) }
let(:content) { nil }
let(:source) { :push }
let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, content: content, source: source) }
let(:inputs) { {} }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, content: content, source: source, inputs: inputs)
end
subject { described_class.new(pipeline, command) }
@ -145,6 +148,28 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: :
expect(command.config_content).to eq(config_content_result)
expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
context 'when passing inputs' do
let(:inputs) { { 'foo' => 'bar' } }
let(:config_content_result) do
<<~CICONFIG
---
include:
- local: ".gitlab-ci.yml"
inputs:
foo: bar
CICONFIG
end
it 'builds the config with the inputs' do
subject.perform!
expect(pipeline.config_source).to eq 'repository_source'
expect(pipeline.pipeline_config.content).to eq(config_content_result)
expect(command.config_content).to eq(config_content_result)
expect(command.pipeline_config.internal_include_prepended?).to eq(true)
end
end
end
context 'when config is the Auto-Devops template' do
@ -190,6 +215,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: :
expect(command.config_content).to eq(content)
expect(command.pipeline_config.internal_include_prepended?).to eq(false)
end
context 'when passing inputs' do
let(:inputs) { { 'foo' => 'bar' } }
it 'uses the parameter content with inputs' do
subject.perform!
expect(pipeline.config_source).to eq 'parameter_source'
expect(pipeline.pipeline_config.content).to eq(content)
expect(command.config_content).to eq(content)
expect(command.pipeline_config.internal_include_prepended?).to eq(false)
end
end
end
context 'when config is not defined anywhere' do

View File

@ -7,13 +7,14 @@ RSpec.describe Gitlab::Ci::ProjectConfig::ProjectSetting, feature_category: :pip
let(:sha) { project.repository.head_commit.sha }
let(:files) { { 'README.md' => 'hello' } }
let(:config_path) { nil }
let(:inputs) { nil }
before do
project.ci_config_path = config_path
end
subject(:config) do
described_class.new(project: project, sha: sha)
described_class.new(project: project, sha: sha, inputs: inputs)
end
describe '#content' do
@ -31,6 +32,21 @@ RSpec.describe Gitlab::Ci::ProjectConfig::ProjectSetting, feature_category: :pip
let(:files) { { '.gitlab-ci.yml' => 'content' } }
it { is_expected.to eq(config_content_result) }
context 'when passing inputs' do
let(:inputs) { { 'foo' => 'bar' } }
let(:config_content_result) do
<<~CICONFIG
---
include:
- local: ".gitlab-ci.yml"
inputs:
foo: bar
CICONFIG
end
it { is_expected.to eq(config_content_result) }
end
end
context 'with external config' do
@ -46,6 +62,22 @@ RSpec.describe Gitlab::Ci::ProjectConfig::ProjectSetting, feature_category: :pip
end
it { is_expected.to eq(config_content_result) }
context 'when passing inputs' do
let(:inputs) { { 'foo' => 'bar' } }
let(:config_content_result) do
<<~CICONFIG
---
include:
- project: another-group/another-project
file: path/to/.gitlab-ci.yml
inputs:
foo: bar
CICONFIG
end
it { is_expected.to eq(config_content_result) }
end
end
context 'with remote config' do
@ -59,6 +91,21 @@ RSpec.describe Gitlab::Ci::ProjectConfig::ProjectSetting, feature_category: :pip
end
it { is_expected.to eq(config_content_result) }
context 'when passing inputs' do
let(:inputs) { { 'foo' => 'bar' } }
let(:config_content_result) do
<<~CICONFIG
---
include:
- remote: #{config_path}
inputs:
foo: bar
CICONFIG
end
it { is_expected.to eq(config_content_result) }
end
end
context 'when file is not in repository' do
@ -103,4 +150,12 @@ RSpec.describe Gitlab::Ci::ProjectConfig::ProjectSetting, feature_category: :pip
it { is_expected.to eq(true) }
end
describe '#inputs_for_pipeline_creation' do
let(:inputs) { { 'foo' => 'bar' } }
subject(:inputs_for_pipeline_creation) { config.inputs_for_pipeline_creation }
it { is_expected.to eq({}) }
end
end

View File

@ -2,12 +2,13 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::ProjectConfig::Source, feature_category: :continuous_integration do
RSpec.describe Gitlab::Ci::ProjectConfig::Source, feature_category: :pipeline_composition do
let_it_be(:custom_config_class) { Class.new(described_class) }
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:sha) { '123456' }
let_it_be(:inputs) { {} }
subject(:custom_config) { custom_config_class.new(project: project, sha: sha) }
subject(:custom_config) { custom_config_class.new(project: project, sha: sha, inputs: inputs) }
describe '#content' do
subject(:content) { custom_config.content }
@ -26,4 +27,12 @@ RSpec.describe Gitlab::Ci::ProjectConfig::Source, feature_category: :continuous_
it { expect(internal_include_prepended).to eq(false) }
end
describe '#inputs_for_pipeline_creation' do
let(:inputs) { { 'foo' => 'bar' } }
subject(:inputs_for_pipeline_creation) { custom_config.inputs_for_pipeline_creation }
it { expect(inputs_for_pipeline_creation).to eq(inputs) }
end
end

View File

@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_compositio
let(:bridge) { nil }
let(:triggered_for_branch) { true }
let(:ref) { 'master' }
let(:inputs) { nil }
subject(:config) do
described_class.new(
@ -19,7 +20,8 @@ RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_compositio
pipeline_source: source,
pipeline_source_bridge: bridge,
triggered_for_branch: triggered_for_branch,
ref: ref
ref: ref,
inputs: inputs
)
end
@ -141,6 +143,25 @@ RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_compositio
expect(config.content).to eq(config_content_result)
expect(config.url).to eq("localhost/#{project.full_path}//.gitlab-ci.yml")
end
context 'when passing inputs' do
let(:inputs) { { 'foo' => 'bar' } }
let(:config_content_result) do
<<~CICONFIG
---
include:
- local: ".gitlab-ci.yml"
inputs:
foo: bar
CICONFIG
end
it 'returns root config with inputs' do
expect(config.source).to eq(:repository_source)
expect(config.content).to eq(config_content_result)
expect(config.url).to eq("localhost/#{project.full_path}//.gitlab-ci.yml")
end
end
end
context 'when config is the Auto-Devops template' do

View File

@ -4991,14 +4991,18 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
describe '#archived?' do
before do
pipeline.update!(created_at: 1.day.ago)
end
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated, pipeline: pipeline) }
subject { build_stubbed(:ci_build, :degenerated, pipeline: pipeline) }
it { is_expected.to be_archived }
end
context 'for old build' do
subject { create(:ci_build, created_at: 1.day.ago, pipeline: pipeline) }
context 'for old pipelines' do
subject { build_stubbed(:ci_build, pipeline: pipeline) }
context 'when archive_builds_in is set' do
before do

View File

@ -1426,7 +1426,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '#retryable?' do
subject { pipeline.retryable? }
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) }
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project, created_at: 1.day.ago) }
context 'no failed builds' do
before do
@ -1445,6 +1445,14 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
it 'is retryable' do
is_expected.to be_truthy
end
context 'with archived pipelines' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it { is_expected.to be_falsey }
end
end
end
@ -6114,4 +6122,24 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
end
describe '#archived?' do
subject { build_stubbed(:ci_pipeline, created_at: 1.day.ago, project: project) }
context 'when archive_builds_in is set' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it { is_expected.to be_archived }
end
context 'when archive_builds_in is not set' do
before do
stub_application_setting(archive_builds_in_seconds: nil)
end
it { is_expected.not_to be_archived }
end
end
end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe CommitStatus, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) do
let_it_be_with_reload(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit.id)
end
@ -1046,4 +1046,28 @@ RSpec.describe CommitStatus, feature_category: :continuous_integration do
it_behaves_like 'having enum with nil value'
end
describe '#archived?' do
before do
pipeline.update!(created_at: 1.day.ago)
end
subject { build_stubbed(:commit_status, pipeline: pipeline) }
context 'when archive_builds_in is set' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it { is_expected.to be_archived }
end
context 'when archive_builds_in is not set' do
before do
stub_application_setting(archive_builds_in_seconds: nil)
end
it { is_expected.not_to be_archived }
end
end
end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::PipelinePolicy, :models, :request_store, :use_clean_rails_redis_caching, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, developers: user) }
let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline, project: project, created_at: 1.day.ago) }
subject(:policy) do
described_class.new(user, pipeline)
@ -53,6 +53,26 @@ RSpec.describe Ci::PipelinePolicy, :models, :request_store, :use_clean_rails_red
end
end
describe 'archived rules' do
context 'when archive_builds_in is set' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it { is_expected.not_to be_allowed(:update_pipeline) }
it { is_expected.not_to be_allowed(:cancel_pipeline) }
end
context 'when archive_builds_in is not set' do
before do
stub_application_setting(archive_builds_in_seconds: nil)
end
it { is_expected.to be_allowed(:update_pipeline) }
it { is_expected.to be_allowed(:cancel_pipeline) }
end
end
context 'when maintainer is allowed to push to pipeline branch' do
before_all do
project.add_maintainer(user)
@ -62,6 +82,15 @@ RSpec.describe Ci::PipelinePolicy, :models, :request_store, :use_clean_rails_red
it { is_expected.to be_allowed(:update_pipeline) }
it { is_expected.to be_allowed(:cancel_pipeline) }
context 'and the pipeline is archived' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it { is_expected.not_to be_allowed(:update_pipeline) }
it { is_expected.not_to be_allowed(:cancel_pipeline) }
end
end
context 'when user does not have access to internal CI' do

View File

@ -67,6 +67,10 @@ RSpec.describe 'Update of an existing issue', feature_category: :team_planning d
end
context 'setting labels' do
before do
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(102)
end
let(:mutation) do
graphql_mutation(:update_issue, input_params) do
<<~QL

View File

@ -70,7 +70,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur
context 'when the current user does not have permission to add assignees' do
let(:current_user) { create(:user) }
let(:db_query_limit) { 29 }
let(:db_query_limit) { 31 }
it 'does not change the assignees' do
project.add_guest(current_user)

View File

@ -64,7 +64,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
it 'executes a limited number of queries', :use_clean_rails_redis_caching do
control = ActiveRecord::QueryRecorder.new { perform_archive_upload }
expect(control.count).to be <= 126
expect(control.count).to be <= 127
end
it 'schedules an import using a namespace' do

View File

@ -2,10 +2,10 @@
require 'spec_helper'
RSpec.describe Ci::BuildUnscheduleService, feature_category: :continuous_integration do
RSpec.describe Ci::BuildUnscheduleService, :aggregate_failures, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, created_at: 1.day.ago) }
describe '#execute' do
subject(:execute) { described_class.new(build, user).execute }
@ -24,12 +24,25 @@ RSpec.describe Ci::BuildUnscheduleService, feature_category: :continuous_integra
expect(response).to be_success
expect(response.payload.reload).to be_manual
end
context 'when the pipeline is archived' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it 'responds with forbidden' do
response = execute
expect(response).to be_error
expect(response.http_status).to eq(:forbidden)
end
end
end
context 'when build is not scheduled' do
let!(:build) { create(:ci_build, pipeline: pipeline) }
it 'responds with unprocessable entity', :aggregate_failures do
it 'responds with unprocessable entity' do
response = execute
expect(response).to be_error
@ -41,7 +54,7 @@ RSpec.describe Ci::BuildUnscheduleService, feature_category: :continuous_integra
context 'when user is not authorized to unschedule the build' do
let!(:build) { create(:ci_build, :scheduled, pipeline: pipeline) }
it 'responds with forbidden', :aggregate_failures do
it 'responds with forbidden' do
response = execute
expect(response).to be_error

View File

@ -18,11 +18,11 @@ RSpec.describe Ci::CreateCommitStatusService, :clean_gitlab_redis_cache, feature
let(:params) { { state: 'pending' } }
let(:job) { response.payload[:job] }
%w[pending running success failed canceled skipped].each do |status|
context "for #{status}" do
let(:params) { { state: status } }
context 'when pipeline for sha does not exists' do
%w[pending running success failed canceled skipped].each do |status|
context "for #{status}" do
let(:params) { { state: status } }
context 'when pipeline for sha does not exists' do
it 'creates commit status and sets pipeline iid' do
expect(response).to be_success
expect(job.sha).to eq(commit.id)
@ -41,6 +41,37 @@ RSpec.describe Ci::CreateCommitStatusService, :clean_gitlab_redis_cache, feature
end
end
context 'when pipeline for sha already exists' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: commit.id, created_at: 1.day.ago) }
%w[pending running success failed canceled skipped].each do |status|
context "for #{status}" do
let(:params) { { state: status } }
it 'creates commit status on the pipeline' do
expect(response).to be_success
expect(job.sha).to eq(commit.id)
expect(job.status).to eq(status)
expect(job.name).to eq('default')
expect(job.ref).not_to be_empty
expect(job.pipeline_id).to eq(pipeline.id)
end
context 'when the pipeline is archived' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it 'returns an error' do
expect(response).to be_error
expect(response.http_status).to eq(:forbidden)
expect(response.message).to eq('403 Forbidden')
end
end
end
end
end
context 'when status transitions from pending' do
before do
execute_service(state: 'pending')

View File

@ -0,0 +1,220 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_composition do
include RepoHelpers
context 'for inputs' do
let_it_be(:project) { create(:project, :small_repo) }
let_it_be(:user) { project.first_owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:content) { nil }
let(:inputs) { {} }
let_it_be(:complex_inputs_example_path) { 'spec/lib/gitlab/ci/config/yaml/fixtures/complex-included-ci.yml' }
let_it_be(:complex_inputs_example_yaml) { File.read(Rails.root.join(complex_inputs_example_path)) }
let(:project_ci_config_path) { nil } # default: .gitlab-ci.yml
let(:service) { described_class.new(project, user, { ref: ref }) }
subject(:execute) { service.execute(source, content: content, inputs: inputs) }
before do
project.ci_config_path = project_ci_config_path
end
shared_examples 'returning errors' do
it 'returns errors' do
response = execute
pipeline = response.payload
expect(response).to be_error
errors = pipeline.error_messages.map(&:content)
expect(errors.size).to eq(1)
expected_errors.each do |error|
expect(errors.first).to include(error)
end
end
end
shared_examples 'testing invalid and valid cases' do
context 'when passing no inputs' do
let(:expected_errors) do
[
'`deploy_strategy` input: required value has not been provided',
'`job_stage` input: required value has not been provided',
'`test_script` input: required value has not been provided'
]
end
it_behaves_like 'returning errors'
end
context 'when passing invalid inputs' do
let(:inputs) do
{
deploy_strategy: 'manual',
job_stage: 'deploy',
test_script: 'echo "test"'
}
end
let(:expected_errors) do
[
'`deploy_strategy` input: `manual` cannot be used because it is not in the list of allowed options',
'`test_script` input: provided value is not an array'
]
end
it_behaves_like 'returning errors'
end
context 'when passing valid inputs' do
let(:inputs) do
{
deploy_strategy: 'blue-green',
job_stage: 'deploy',
test_script: ['echo "test"'],
test_rules: [
{ if: '$CI_PIPELINE_SOURCE == "push"' }
],
test_framework: '$TEST_FRAMEWORK'
}
end
before_all do
create(:ci_variable, project: project, key: 'TEST_FRAMEWORK', value: 'rspec')
end
it 'creates a pipeline with correct jobs' do
response = execute
pipeline = response.payload
expect(response).to be_success
expect(pipeline).to be_created_successfully
expect(pipeline.builds.map(&:name)).to contain_exactly(
'my-job-build 1/2', 'my-job-build 2/2', 'my-job-test', 'my-job-test-2', 'my-job-deploy'
)
expect(pipeline.builds.map(&:stage)).to contain_exactly('deploy', 'deploy', 'deploy', 'deploy', 'deploy')
my_job_test = pipeline.builds.find { |build| build.name == 'my-job-test' }
expect(my_job_test.options[:script]).to eq([
'echo "Testing with rspec"',
'if [ false == true ]; then echo "Coverage is enabled"; fi'
])
my_job_test_2 = pipeline.builds.find { |build| build.name == 'my-job-test-2' }
expect(my_job_test_2.options[:script]).to eq(['echo "test"'])
my_job_deploy = pipeline.builds.find { |build| build.name == 'my-job-deploy' }
expect(my_job_deploy.options[:script]).to eq(['echo "Deploying to staging using blue-green strategy"'])
end
context 'when the FF ci_inputs_for_pipelines is disabled' do
before do
stub_feature_flags(ci_inputs_for_pipelines: false)
end
let(:expected_errors) do
[
'`deploy_strategy` input: required value has not been provided',
'`job_stage` input: required value has not been provided',
'`test_script` input: required value has not been provided'
]
end
it_behaves_like 'returning errors'
end
end
end
context 'when the project CI config is in the current repository file' do
let(:project_files) do
{ '.gitlab-ci.yml' => complex_inputs_example_yaml }
end
around do |example|
create_and_delete_files(project, project_files) do
example.run
end
end
it_behaves_like 'testing invalid and valid cases'
end
context 'when the project CI config is in another project repository file' do
let_it_be(:project_2) do
create(:project, :custom_repo, files: { 'a_config_file.yml' => complex_inputs_example_yaml })
end
let(:project_ci_config_path) { "a_config_file.yml@#{project_2.full_path}:#{project_2.default_branch}" }
before_all do
project_2.add_developer(user)
end
it_behaves_like 'testing invalid and valid cases'
end
context 'when the project CI config is a remote file' do
let(:project_ci_config_path) { 'https://gitlab.example.com/something/.gitlab-ci.yml' }
before do
stub_request(:get, project_ci_config_path)
.to_return(status: 200, body: complex_inputs_example_yaml)
end
it_behaves_like 'testing invalid and valid cases'
end
context 'when the CI config is passed as content' do
let(:content) { complex_inputs_example_yaml }
it_behaves_like 'testing invalid and valid cases'
end
context 'when the CI config does not have spec:inputs' do
let(:content) do
<<~YAML
job:
script: echo "hello"
YAML
end
context 'when passing inputs' do
let(:inputs) do
{ deploy_strategy: 'blue-green' }
end
let(:expected_errors) do
['Given inputs not defined in the `spec` section of the included configuration file']
end
it_behaves_like 'returning errors'
context 'when the FF ci_inputs_for_pipelines is disabled' do
before do
stub_feature_flags(ci_inputs_for_pipelines: false)
end
it 'creates a pipeline' do
response = execute
pipeline = response.payload
expect(response).to be_success
expect(pipeline).to be_created_successfully
expect(pipeline.builds.map(&:name)).to contain_exactly('job')
end
end
end
end
end
end

View File

@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let(:user) { create(:user, developer_of: project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
let_it_be(:user) { create(:user, maintainer_of: project) }
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project, created_at: 1.day.ago) }
let_it_be_with_reload(:build) { create(:ci_build, :manual, pipeline: pipeline) }
let(:service) do
described_class.new(build, current_user: user)
@ -107,4 +107,28 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_
expect(build.job_variables.map(&:key)).to contain_exactly('third', 'fourth')
end
end
context 'when the pipeline is archived' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it 'responds with forbidden' do
response = execute
expect(response).to be_error
expect(response.reason).to eq(:forbidden)
end
end
context 'when user is not authorized' do
let_it_be(:user) { create(:user, developer_of: project) }
it 'responds with forbidden' do
response = execute
expect(response).to be_error
expect(response.reason).to eq(:forbidden)
end
end
end

View File

@ -3,32 +3,64 @@
require 'spec_helper'
RSpec.describe Ci::Pipelines::UpdateMetadataService, feature_category: :continuous_integration do
subject(:execute) { described_class.new(pipeline, { name: name }).execute }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project, created_at: 1.day.ago) }
let(:params) { { name: name } }
let(:name) { 'Some random pipeline name' }
context 'when pipeline has no name' do
let(:pipeline) { create(:ci_pipeline) }
subject(:execute) { described_class.new(pipeline, current_user: user, params: params).execute }
it 'updates the name' do
expect { execute }.to change { pipeline.reload.name }.to(name)
describe '#execute' do
context 'when user is authorized' do
before_all do
project.add_maintainer(user)
end
context 'when pipeline has no name' do
it 'updates the name' do
expect { execute }.to change { pipeline.reload.name }.to(name)
end
end
context 'when pipeline has a name' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project, name: 'Some other name') }
it 'updates the name' do
expect { execute }.to change { pipeline.reload.name }.to(name)
end
end
context 'when new name is too long' do
let(:name) { 'a' * 256 }
it 'does not update the name' do
expect { execute }.not_to change { pipeline.reload.name }
end
end
context 'when the pipeline is archived' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it 'responds with forbidden' do
response = execute
expect(response).to be_error
expect(response.reason).to eq(:forbidden)
end
end
end
end
context 'when pipeline has a name' do
let(:pipeline) { create(:ci_pipeline, name: 'Some other name') }
context 'when user is not authorized' do
it 'responds with forbidden' do
response = execute
it 'updates the name' do
expect { execute }.to change { pipeline.reload.name }.to(name)
end
end
context 'when new name is too long' do
let(:pipeline) { create(:ci_pipeline) }
let(:name) { 'a' * 256 }
it 'does not update the name' do
expect { execute }.not_to change { pipeline.reload.name }
expect(response).to be_error
expect(response.reason).to eq(:forbidden)
end
end
end
end

View File

@ -3,20 +3,17 @@
require 'spec_helper'
RSpec.describe Ci::PlayBridgeService, '#execute', feature_category: :continuous_integration do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:downstream_project) { create(:project) }
let_it_be(:project) { create(:project) }
let_it_be(:downstream_project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_of: [project, downstream_project]) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let(:bridge) { create(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) }
let(:instance) { described_class.new(project, user) }
subject(:execute_service) { instance.execute(bridge) }
context 'when user can run the bridge' do
before do
allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(true)
end
it 'marks the bridge pending' do
execute_service
@ -57,9 +54,7 @@ RSpec.describe Ci::PlayBridgeService, '#execute', feature_category: :continuous_
end
context 'when user can not run the bridge' do
before do
allow(instance).to receive(:can?).with(user, :play_job, bridge).and_return(false)
end
let_it_be(:user) { create(:user, developer_of: project) }
it 'allows user with developer role to play a bridge' do
expect { execute_service }.to raise_error Gitlab::Access::AccessDeniedError

View File

@ -392,6 +392,22 @@ RSpec.describe Ci::RetryPipelineService, '#execute', feature_category: :continuo
expect(pipeline.reload).not_to be_running
end
end
context 'when the pipeline is archived' do
let(:pipeline) { create(:ci_pipeline, project: project, created_at: 1.day.ago) }
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it 'returns an error' do
response = service.execute(pipeline)
expect(response.http_status).to eq(:forbidden)
expect(response.errors).to include('403 Forbidden')
expect(pipeline.reload).not_to be_running
end
end
end
context 'when user is not allowed to retry pipeline' do

View File

@ -2,17 +2,19 @@
require 'spec_helper'
RSpec.describe Ci::RunScheduledBuildService, feature_category: :continuous_integration do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
RSpec.describe Ci::RunScheduledBuildService, :aggregate_failures, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, created_at: 1.day.ago) }
subject(:execute_service) { described_class.new(build).execute }
context 'when user can update build' do
before do
before_all do
project.add_developer(user)
end
before do
create(:protected_branch, :developers_can_merge, name: pipeline.ref, project: project)
end
@ -59,6 +61,20 @@ RSpec.describe Ci::RunScheduledBuildService, feature_category: :continuous_integ
expect(build).to be_created
end
end
context 'when the pipeline is archived' do
let(:build) { create(:ci_build, :scheduled, user: user, project: project, pipeline: pipeline) }
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it 'can not run the build' do
expect { execute_service }.to raise_error(Gitlab::Access::AccessDeniedError)
expect(build).to be_scheduled
end
end
end
context 'when user can not update build' do