Commit 4eb8816d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 87d95ab0 0c902636
40fae4205d3ad62ca9341620146486bee8d31b28
d924490032231edb9452acdaca7d8e4747cf6ab4
......@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
},
methods: {
onChangePage(page) {
......@@ -42,7 +38,7 @@ export default {
<slot name="empty-state"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder">
<environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
<environment-table :environments="environments" />
<table-pagination
v-if="pagination && pagination.totalPages > 1"
......
......@@ -48,12 +48,6 @@ export default {
mixins: [timeagoMixin],
props: {
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
model: {
type: Object,
required: true,
......@@ -790,14 +784,14 @@ export default {
/>
<external-url-component
v-if="externalURL && canReadEnvironment"
v-if="externalURL"
:external-url="externalURL"
data-track-action="click_button"
data-track-label="environment_url"
/>
<monitoring-button-component
v-if="monitoringUrl && canReadEnvironment"
v-if="monitoringUrl"
:monitoring-url="monitoringUrl"
data-track-action="click_button"
data-track-label="environment_monitoring"
......
......@@ -52,10 +52,6 @@ export default {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
newEnvironmentPath: {
type: String,
required: true,
......@@ -210,7 +206,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
>
<template v-if="!isLoading && state.environments.length === 0" #empty-state>
......
......@@ -27,10 +27,6 @@ export default {
type: Object,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
canAdminEnvironment: {
type: Boolean,
required: true,
......@@ -84,7 +80,7 @@ export default {
return this.environment.isAvailable && Boolean(this.environment.autoStopAt);
},
shouldShowExternalUrlButton() {
return this.canReadEnvironment && Boolean(this.environment.externalUrl);
return Boolean(this.environment.externalUrl);
},
shouldShowStopButton() {
return this.canStopEnvironment && this.environment.isAvailable;
......@@ -138,7 +134,7 @@ export default {
>{{ $options.i18n.externalButtonText }}</gl-button
>
<gl-button
v-if="canReadEnvironment"
v-if="shouldShowExternalUrlButton"
data-testid="metrics-button"
:href="metricsPath"
:title="$options.i18n.metricsButtonTitle"
......
......@@ -23,11 +23,6 @@ export default {
required: true,
default: () => [],
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -155,7 +150,6 @@ export default {
<environment-item
:key="`environment-item-${i}`"
:model="model"
:can-read-environment="canReadEnvironment"
:table-data="tableData"
data-qa-selector="environment_item"
/>
......@@ -191,7 +185,6 @@ export default {
<environment-item
:key="`environment-row-${i}-${index}`"
:model="child"
:can-read-environment="canReadEnvironment"
:table-data="tableData"
data-qa-selector="environment_item"
/>
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
import environmentsFolderApp from './environments_folder_view.vue';
......@@ -31,7 +30,6 @@ export default () => {
endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName,
cssContainerClass: environmentsData.cssClass,
canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment),
};
},
render(createElement) {
......@@ -40,7 +38,6 @@ export default () => {
endpoint: this.endpoint,
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
canReadEnvironment: this.canReadEnvironment,
},
});
},
......
......@@ -30,10 +30,6 @@ export default {
required: false,
default: '',
},
canReadEnvironment: {
type: Boolean,
required: true,
},
},
methods: {
successCallback(resp) {
......@@ -72,7 +68,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
</div>
......
......@@ -32,7 +32,6 @@ export default () => {
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
},
render(createElement) {
......@@ -42,7 +41,6 @@ export default () => {
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
canCreateEnvironment: this.canCreateEnvironment,
canReadEnvironment: this.canReadEnvironment,
},
});
},
......
......@@ -36,7 +36,6 @@ export const initHeader = () => {
environment: this.environment,
canDestroyEnvironment: dataset.canDestroyEnvironment,
canUpdateEnvironment: dataset.canUpdateEnvironment,
canReadEnvironment: dataset.canReadEnvironment,
canStopEnvironment: dataset.canStopEnvironment,
canAdminEnvironment: dataset.canAdminEnvironment,
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
......
<script>
export default {
props: {
url: {
type: String,
required: true,
},
alt: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="gl-text-center gl-p-7 gl-bg-gray-50">
<img :src="url" :alt="alt" data-testid="image" />
</div>
</template>
......@@ -6,6 +6,8 @@ export const loadViewer = (type) => {
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
case 'download':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
case 'image':
return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue');
default:
return null;
}
......@@ -23,5 +25,9 @@ export const viewerProps = (type, blob) => {
filePath: blob.rawPath,
fileSize: blob.rawSize,
},
image: {
url: blob.rawPath,
alt: blob.name,
},
}[type];
};
......@@ -2,7 +2,7 @@ export default (search = '') => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
const searchTerm = search.toLowerCase();
const blobs = contentBody.querySelectorAll('.blob-result');
const blobs = contentBody.querySelectorAll('.js-blob-result');
blobs.forEach((blob) => {
const lines = blob.querySelectorAll('.line');
......
......@@ -73,7 +73,6 @@ module EnvironmentHelper
external_url: environment.external_url,
can_update_environment: can?(current_user, :update_environment, environment),
can_destroy_environment: can_destroy_environment?(environment),
can_read_environment: can?(current_user, :read_environment, environment),
can_stop_environment: can?(current_user, :stop_environment, environment),
can_admin_environment: can?(current_user, :admin_environment, project),
environment_metrics_path: environment_metrics_path(environment),
......
......@@ -14,12 +14,10 @@ module CronSchedulable
# The `next_run_at` column is set to the actual execution date of worker that
# triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
# in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
def calculate_next_run_at
now = Time.zone.now
def calculate_next_run_at(start_time = Time.zone.now)
ideal_next_run = ideal_next_run_from(start_time)
ideal_next_run = ideal_next_run_from(now)
if ideal_next_run == cron_worker_next_run_from(now)
if ideal_next_run == cron_worker_next_run_from(start_time)
ideal_next_run
else
cron_worker_next_run_from(ideal_next_run)
......
......@@ -12,11 +12,11 @@ module Ci
erased_by.name if erased_by_user?
end
def status_title
def status_title(status = detailed_status)
if auto_canceled?
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
else
tooltip_for_badge
tooltip_for_badge(status)
end
end
......@@ -41,8 +41,8 @@ module Ci
private
def tooltip_for_badge
detailed_status.badge_tooltip.capitalize
def tooltip_for_badge(status)
status.badge_tooltip.capitalize
end
def detailed_status
......
......@@ -15,18 +15,23 @@ module Ci
private
def preload_statuses(statuses)
loaded_statuses = statuses.load
statuses.tap do |statuses|
# rubocop: disable CodeReuse/ActiveRecord
ActiveRecord::Associations::Preloader.new.preload(preloadable_statuses(loaded_statuses), %w[pipeline tags job_artifacts_archive metadata])
# rubocop: enable CodeReuse/ActiveRecord
end
end
common_relations = [:pipeline]
def preloadable_statuses(statuses)
statuses.reject do |status|
status.instance_of?(::GenericCommitStatus) || status.instance_of?(::Ci::Bridge)
preloaders = {
::Ci::Build => [:metadata, :tags, :job_artifacts_archive],
::Ci::Bridge => [:metadata, :downstream_pipeline],
::GenericCommitStatus => []
}
# rubocop: disable CodeReuse/ActiveRecord
preloaders.each do |klass, relations|
ActiveRecord::Associations::Preloader
.new
.preload(statuses.select { |job| job.is_a?(klass) }, relations + common_relations)
end
# rubocop: enable CodeReuse/ActiveRecord
statuses
end
end
end
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Dast profile schedule cadence schema",
"type": "object",
"anyOf": [
{
"properties": {
"unit": { "enum": ["day"] },
"duration": { "enum": [1] }
}
},
{
"properties": {
"unit": { "enum": ["week"] },
"duration": { "enum": [1] }
}
},
{
"properties": {
"unit": { "enum": ["month"] },
"duration": { "enum": [1, 3 ,6] }
}
},
{
"properties": {
"unit": { "enum": ["year"] },
"duration": { "enum": [1] }
}
}
]
}
......@@ -52,7 +52,7 @@
.float-right
= form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
= f.hidden_field :runner_id, value: @runner.id
= f.submit _('Enable'), class: 'gl-button btn btn-sm'
= f.submit _('Enable'), class: 'gl-button btn btn-sm', data: { confirm: (s_('Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?') if @runner.instance_type?) }
= paginate_without_count @projects
.col-md-6
......
......@@ -7,10 +7,14 @@
- pipeline_link = local_assigns.fetch(:pipeline_link, false)
- stage = local_assigns.fetch(:stage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
-# This prevents initializing another Ci::Status object where 'status' is used
- status = job.detailed_status(current_user)
%tr.build.commit{ class: ('retried' if retried) }
%td.status
= render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title
-# Sending 'status' prevents calling the user relation inside the presenter, generating N+1,
-# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68743
= render "ci/status/badge", status: status, title: job.status_title(status)
%td
- if can?(current_user, :read_build, job)
......
.blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
.js-blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
.file-holder.file-holder-top-border
.js-file-title.file-title{ data: { qa_selector: 'file_title_content' } }
= link_to blob_link, data: {track_event: 'click_text', track_label: 'blob_path', track_property: 'search_result'} do
......
---
name: method_instrumentation_disable_initialization
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69091
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339665
milestone: '14.3'
type: development
group: group::memory
default_enabled: false
......@@ -178,27 +178,33 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
)
Gitlab::Metrics::Instrumentation
.instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
# Instrumenting the ApplicationSetting class can lead to an infinite
# loop. Since the data is cached any way we don't really need to
# instrument it.
if klass == ApplicationSetting
false
else
loc = method.source_location
loc && loc[0].start_with?(models_path) && method.source =~ regex
# We are removing the Instrumentation module entirely in steps.
# More in https://gitlab.com/gitlab-org/gitlab/-/issues/217978.
unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
Gitlab::Metrics::Instrumentation
.instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
# Instrumenting the ApplicationSetting class can lead to an infinite
# loop. Since the data is cached any way we don't really need to
# instrument it.
if klass == ApplicationSetting
false
else
loc = method.source_location
loc && loc[0].start_with?(models_path) && method.source =~ regex
end
end
end
# Ability is in app/models, is not an ActiveRecord model, but should still
# be instrumented.
Gitlab::Metrics::Instrumentation.instrument_methods(Ability)
# Ability is in app/models, is not an ActiveRecord model, but should still
# be instrumented.
Gitlab::Metrics::Instrumentation.instrument_methods(Ability)
end
end
Gitlab::Metrics::Instrumentation.configure do |config|
instrument_classes(config)
unless ::Feature.enabled?(:method_instrumentation_disable_initialization)
Gitlab::Metrics::Instrumentation.configure do |config|
instrument_classes(config)
end
end
GC::Profiler.enable
......
# frozen_string_literal: true
class AddCadenceToDastProfileSchedules < ActiveRecord::Migration[6.1]
def change
add_column :dast_profile_schedules, :cadence, :jsonb, null: false, default: {}
end
end
# frozen_string_literal: true
class AddTimezoneToDastProfileSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
# We disable these cops here because adding the column is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Rails/NotNullColumn
def up
execute('DELETE FROM dast_profile_schedules')
unless column_exists?(:dast_profile_schedules, :timezone)
add_column :dast_profile_schedules, :timezone, :text, null: false
end
add_text_limit :dast_profile_schedules, :timezone, 255
end
def down
return unless column_exists?(:dast_profile_schedules, :timezone)
remove_column :dast_profile_schedules, :timezone
end
end
# frozen_string_literal: true
class AddStartsAtToDastProfileSchedules < ActiveRecord::Migration[6.1]
def change
add_column :dast_profile_schedules, :starts_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
end
end
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddUniqueIndexOnDastProfileToDastProfileSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_dast_profile_schedules_on_dast_profile_id'
TABLE = :dast_profile_schedules
# We disable these cops here because changing this index is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Migration/AddIndex
# rubocop: disable Migration/RemoveIndex
def up
execute('DELETE FROM dast_profile_schedules')
if index_exists_by_name?(TABLE, INDEX_NAME)
remove_index TABLE, :dast_profile_id, name: INDEX_NAME
end
unless index_exists_by_name?(TABLE, INDEX_NAME)
add_index TABLE, :dast_profile_id, unique: true, name: INDEX_NAME
end
end
def down
execute('DELETE FROM dast_profile_schedules')
if index_exists_by_name?(TABLE, INDEX_NAME)
remove_index TABLE, :dast_profile_id, name: INDEX_NAME
end
unless index_exists_by_name?(TABLE, INDEX_NAME)
add_index TABLE, :dast_profile_id
end
end
end
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveProjectProfileCompoundIndexFromDastProfileSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
TABLE = :dast_profile_schedules
INDEX_NAME = 'index_dast_profile_schedules_on_project_id_and_dast_profile_id'
# We disable these cops here because changing this index is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Migration/AddIndex
# rubocop: disable Migration/RemoveIndex
def up
execute('DELETE FROM dast_profile_schedules')
if index_exists_by_name?(TABLE, INDEX_NAME)
remove_index TABLE, %i[project_id dast_profile_id], name: INDEX_NAME
end
end
def down
execute('DELETE FROM dast_profile_schedules')
unless index_exists_by_name?(TABLE, INDEX_NAME)
add_index TABLE, %i[project_id dast_profile_id], unique: true, name: INDEX_NAME
end
end
end
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexProjectIdOnDastProfileSchedule < ActiveRecord::Migration[6.1]
# We disable these cops here because changing this index is safe. The table does not
# have any data in it as it's behind a feature flag.
# rubocop: disable Migration/AddIndex
def change
add_index :dast_profile_schedules, :project_id
end
end
30e1463616c60b92afb28bbb76e3c55830a385af6df0e60e16ed96d9e75943b9
\ No newline at end of file
7e9b39914ade766357751953a4981225dbae7e5d371d4824af61b01af70f46ae
\ No newline at end of file
a2454f9fca3b1cedf7a0f2288b69abe799fe1f9ff4e2fe26d2cadfdddea73a83
\ No newline at end of file
d1ad234656f49861d2ca7694d23116e930bba597fca32b1015db698cc23bdc1c
\ No newline at end of file
23becdc9ad558882f4ce42e76391cdc2f760322a09c998082465fcb6d29dfeb5
\ No newline at end of file
9c5114dac05e90c15567bb3274f20f03a82f9e4d73d5c72d89c26bc9d742cc35
\ No newline at end of file
......@@ -12089,7 +12089,11 @@ CREATE TABLE dast_profile_schedules (
updated_at timestamp with time zone NOT NULL,
active boolean DEFAULT true NOT NULL,
cron text NOT NULL,
CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255))
cadence jsonb DEFAULT '{}'::jsonb NOT NULL,
timezone text NOT NULL,
starts_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT check_86531ea73f CHECK ((char_length(cron) <= 255)),
CONSTRAINT check_be4d1c3af1 CHECK ((char_length(timezone) <= 255))
);
COMMENT ON TABLE dast_profile_schedules IS '{"owner":"group::dynamic analysis","description":"Scheduling for scans using DAST Profiles"}';
......@@ -23812,9 +23816,9 @@ CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON ci_
CREATE INDEX index_dast_profile_schedules_active_next_run_at ON dast_profile_schedules USING btree (active, next_run_at);
CREATE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id);
CREATE UNIQUE INDEX index_dast_profile_schedules_on_dast_profile_id ON dast_profile_schedules USING btree (dast_profile_id);
CREATE UNIQUE INDEX index_dast_profile_schedules_on_project_id_and_dast_profile_id ON dast_profile_schedules USING btree (project_id, dast_profile_id);
CREATE INDEX index_dast_profile_schedules_on_project_id ON dast_profile_schedules USING btree (project_id);
CREATE INDEX index_dast_profile_schedules_on_user_id ON dast_profile_schedules USING btree (user_id);
......@@ -95,7 +95,7 @@ GitLab provides a series of [CI templates that you can include in your project](
To automate deployments of your application to your [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (AWS ECS)
cluster, you can `include` the `AWS/Deploy-ECS.gitlab-ci.yml` template in your `.gitlab-ci.yml` file.
GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `gitlab-ci.yml` file to simplify working with AWS:
GitLab also provides [Docker images](https://gitlab.com/gitlab-org/cloud-deploy/-/tree/master/aws) that can be used in your `.gitlab-ci.yml` file to simplify working with AWS:
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest` to use AWS CLI commands.
- Use `registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest` to deploy your application to AWS ECS.
......
......@@ -136,10 +136,10 @@ connect the CD project to your development projects by using [multi-project pipe
A `.gitlab-ci.yml` may contain rules to deploy an application to the production server. This
deployment usually runs automatically after pushing a merge request. To prevent developers from
changing the `gitlab-ci.yml`, you can define it in a different repository. The configuration can
changing the `.gitlab-ci.yml`, you can define it in a different repository. The configuration can
reference a file in another project with a completely different set of permissions (similar to
[separating a project for deployments](#separate-project-for-deployments)).
In this scenario, the `gitlab-ci.yml` is publicly accessible, but can only be edited by users with
In this scenario, the `.gitlab-ci.yml` is publicly accessible, but can only be edited by users with
appropriate permissions in the other project.
For more information, see [Custom CI/CD configuration path](../pipelines/settings.md#specify-a-custom-cicd-configuration-file).
......
......@@ -135,5 +135,5 @@ to switch to a different deployment. Both deployments are running in parallel, a
can be switched to at any time.
An [example deployable application](https://gitlab.com/gl-release/blue-green-example)
is available, with a [`gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml)
is available, with a [`.gitlab-ci.yml` CI/CD configuration file](https://gitlab.com/gl-release/blue-green-example/blob/master/.gitlab-ci.yml)
that demonstrates blue-green deployments.
......@@ -177,7 +177,7 @@ You can find the play button in the pipelines, environments, deployments, and jo
If you are deploying to a [Kubernetes cluster](../../user/project/clusters/index.md)
associated with your project, you can configure these deployments from your
`gitlab-ci.yml` file.
`.gitlab-ci.yml` file.
NOTE:
Kubernetes configuration isn't supported for Kubernetes clusters that are
......
......@@ -251,7 +251,7 @@ To protect a group-level environment:
1. Make sure your environments have the correct
[`deployment_tier`](index.md#deployment-tier-of-environments) defined in
`gitlab-ci.yml`.
`.gitlab-ci.yml`.
1. Configure the group-level protected environments via the
[REST API](../../api/group_protected_environments.md).
......
......@@ -56,7 +56,7 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.12.
To view a visualization of your `gitlab-ci.yml` configuration, in your project,
To view a visualization of your `.gitlab-ci.yml` configuration, in your project,
go to **CI/CD > Editor**, and then select the **Visualize** tab. The
visualization shows all stages and jobs. Any [`needs`](../yaml/index.md#needs)
relationships are displayed as lines connecting jobs together, showing the
......
......@@ -29,7 +29,7 @@ with your editor of choice.
### Verify syntax with CI Lint tool
The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration
file is correct. Paste in full `gitlab-ci.yml` files or individual jobs configuration,
file is correct. Paste in full `.gitlab-ci.yml` files or individual jobs configuration,
to verify the basic syntax.
When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint
......@@ -49,7 +49,7 @@ and check if their values are what you expect.
## GitLab CI/CD documentation
The [complete `gitlab-ci.yml` reference](yaml/index.md) contains a full list of
The [complete `.gitlab-ci.yml` reference](yaml/index.md) contains a full list of
every keyword you may need to use to configure your pipelines.
You can also look at a large number of pipeline configuration [examples](examples/index.md)
......
......@@ -386,7 +386,7 @@ does not block triggered pipelines.
> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Free in 11.4.
Use `include` to include external YAML files in your CI/CD configuration.
You can break down one long `gitlab-ci.yml` file into multiple files to increase readability,
You can break down one long `.gitlab-ci.yml` file into multiple files to increase readability,
or reduce duplication of the same configuration in multiple places.
You can also store template files in a central repository and `include` them in projects.
......@@ -4483,7 +4483,7 @@ deploy_review_job:
You can use only integers and strings for the variable's name and value.
If you define a variable at the top level of the `gitlab-ci.yml` file, it is global,
If you define a variable at the top level of the `.gitlab-ci.yml` file, it is global,
meaning it applies to all jobs. If you define a variable in a job, it's available
to that job only.
......
......@@ -98,6 +98,7 @@ EE: true
database records created during Cycle Analytics model spec."
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
- Any [GLEX experiment](experiment_guide/gitlab_experiment.md) changes **should not** have a changelog entry.
- [Removing](feature_flags/#changelog) a feature flag, when the new code is retained.
## Writing good changelog entries
......
......@@ -74,7 +74,7 @@ If your application utilizes Docker containers you have another option for deplo
After your Docker build job completes and your image is added to your container registry, you can use the image as a
[service](../../../ci/services/index.md).
By using service definitions in your `gitlab-ci.yml`, you can scan services with the DAST analyzer.
By using service definitions in your `.gitlab-ci.yml`, you can scan services with the DAST analyzer.
```yaml
stages:
......@@ -1307,9 +1307,9 @@ dast:
By default, DAST downloads all artifacts defined by previous jobs in the pipeline. If
your DAST job does not rely on `environment_url.txt` to define the URL under test or any other files created
in previous jobs, we recommend you don't download artifacts. To avoid downloading
artifacts, add the following to your `gitlab-ci.yml` file:
artifacts, add the following to your `.gitlab-ci.yml` file:
```json
```yaml
dast:
dependencies: []
```
......@@ -111,7 +111,7 @@ example of such a transfer:
GitLab provides a [vendored template](../../../ci/yaml/index.md#includetemplate)
to ease this process.
This template should be used in a new, empty project, with a `gitlab-ci.yml` file containing:
This template should be used in a new, empty project, with a `.gitlab-ci.yml` file containing:
```yaml
include:
......
......@@ -316,7 +316,7 @@ The optional `runtime` parameter can refer to one of the following runtime alias
| `openfaas/classic/python3` | OpenFaaS |
| `openfaas/classic/ruby` | OpenFaaS |
After the `gitlab-ci.yml` template has been added and the `serverless.yml` file
After the `.gitlab-ci.yml` template has been added and the `serverless.yml` file
has been created, pushing a commit to your project results in a CI pipeline
being executed which deploys each function as a Knative service. After the
deploy stage has finished, additional details for the function display
......
......@@ -129,7 +129,7 @@ The `source` is ignored if the path does not follow this pattern. The parser ass
### JavaScript example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/)
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example uses [Mocha](https://mochajs.org/)
JavaScript testing and [nyc](https://github.com/istanbuljs/nyc) coverage-tooling to
generate the coverage artifact:
......@@ -147,7 +147,7 @@ test:
#### Maven example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/)
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Maven](https://maven.apache.org/)
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
generate the coverage artifact.
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
......@@ -185,7 +185,7 @@ coverage-jdk11:
#### Gradle example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/)
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Java or Kotlin uses [Gradle](https://gradle.org/)
to build the project and [JaCoCo](https://www.eclemma.org/jacoco/) coverage-tooling to
generate the coverage artifact.
You can check the [Docker image configuration and scripts](https://gitlab.com/haynes/jacoco2cobertura) if you want to build your own image.
......@@ -223,7 +223,7 @@ coverage-jdk11:
### Python example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths.
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for Python uses [pytest-cov](https://pytest-cov.readthedocs.io/) to collect test coverage data and [coverage.py](https://coverage.readthedocs.io/) to convert the report to use full relative paths.
The information isn't displayed without the conversion.
This example assumes that the code for your package is in `src/` and your tests are in `tests.py`:
......@@ -243,7 +243,7 @@ run tests:
### C/C++ example
The following [`gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with
The following [`.gitlab-ci.yml`](../../../ci/yaml/index.md) example for C/C++ with
`gcc` or `g++` as the compiler uses [`gcovr`](https://gcovr.com/en/stable/) to generate the coverage
output file in Cobertura XML format.
......
......@@ -46,7 +46,7 @@ To create a GitLab Pages website:
| Document | Description |
| -------- | ----------- |
| [Create a `gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. |
| [Create a `.gitlab-ci.yml` file from scratch](getting_started/pages_from_scratch.md) | Add a Pages site to an existing project. Learn how to create and configure your own CI file. |
| [Use a `.gitlab-ci.yml` template](getting_started/pages_ci_cd_template.md) | Add a Pages site to an existing project. Use a pre-populated CI template file. |
| [Fork a sample project](getting_started/pages_forked_sample_project.md) | Create a new project with Pages already configured by forking a sample project. |
| [Use a project template](getting_started/pages_new_project_template.md) | Create a new project with Pages already configured by using a template. |
......
......@@ -200,7 +200,7 @@ If the job that's executing is within a freeze period, GitLab CI/CD creates an e
variable named `$CI_DEPLOY_FREEZE`.
To prevent the deployment job from executing, create a `rules` entry in your
`gitlab-ci.yml`, for example:
`.gitlab-ci.yml`, for example:
```yaml
deploy_to_production:
......
......@@ -3,7 +3,7 @@ import setHighlightClass from '~/search/highlight_blob_search_result';
export default (searchTerm) => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
const blobs = contentBody.querySelectorAll('.blob-result');
const blobs = contentBody.querySelectorAll('.js-blob-result');
// Supports Basic (backed by Gitaly) Search highlighting
setHighlightClass(searchTerm);
......
......@@ -10,7 +10,7 @@ module Dast
has_many :secret_variables, through: :dast_site_profile, class_name: 'Dast::SiteProfileSecretVariable'
has_many :dast_profile_schedules, class_name: 'Dast::ProfileSchedule', foreign_key: :dast_profile_id, inverse_of: :dast_profile
has_one :dast_profile_schedule, class_name: 'Dast::ProfileSchedule', foreign_key: :dast_profile_id, inverse_of: :dast_profile
validates :description, length: { maximum: 255 }
validates :name, length: { maximum: 255 }, uniqueness: { scope: :project_id }, presence: true
......
......@@ -3,27 +3,72 @@
class Dast::ProfileSchedule < ApplicationRecord
include CronSchedulable
CRON_DEFAULT = '* * * * *'
self.table_name = 'dast_profile_schedules'
belongs_to :project
belongs_to :dast_profile, class_name: 'Dast::Profile', optional: false, inverse_of: :dast_profile_schedules
belongs_to :dast_profile, class_name: 'Dast::Profile', optional: false, inverse_of: :dast_profile_schedule
belongs_to :owner, class_name: 'User', optional: true, foreign_key: :user_id
validates :cron, presence: true
validates :next_run_at, presence: true
validates :timezone, presence: true, inclusion: { in: :timezones }
validates :starts_at, presence: true
validates :cadence, json_schema: { filename: 'dast_profile_schedule_cadence', draft: 7 }
validates :dast_profile_id, uniqueness: true
serialize :cadence, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
scope :with_project, -> { includes(:project) }
scope :with_profile, -> { includes(dast_profile: [:dast_site_profile, :dast_scanner_profile]) }
scope :with_owner, -> { includes(:owner) }
scope :active, -> { where(active: true) }
before_save :set_cron, :set_next_run_at
def repeat?
cadence.present?
end
def schedule_next_run!
return deactivate! unless repeat?
super
end
def audit_details
owner&.name
end
private
def deactivate!
update!(active: false)
end
def cron_timezone
next_run_at.zone
Time.zone.name
end
def set_cron
self.cron =
if repeat?
Gitlab::Ci::CronParser.parse_natural_with_timestamp(starts_at, cadence)
else
CRON_DEFAULT
end
end
def set_next_run_at
return super unless will_save_change_to_starts_at?
self.next_run_at = cron_worker_next_run_from(starts_at)
end
def worker_cron_expression
Settings.cron_jobs['app_sec_dast_profile_schedule_worker']['cron']
end
def timezones
@timezones ||= ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier }
end
end
......@@ -5,7 +5,8 @@ FactoryBot.define do
project
dast_profile
owner { association(:user) }
cron { '*/10 * * * *' }
next_run_at { Time.now }
timezone { FFaker::Address.time_zone }
starts_at { Time.now }
cadence { { unit: %w(day month year week).sample, duration: 1 } }
end
end
......@@ -11,7 +11,6 @@ describe('Environment', () => {
const mockData = {
canCreateEnvironment: true,
canReadEnvironment: true,
endpoint: 'environments.json',
helpCanaryDeploymentsPath: 'help/canary-deployments',
helpPagePath: 'help',
......
......@@ -36,7 +36,6 @@ describe('Environment table', () => {
{
propsData: {
environments: [mockItem],
canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
......
......@@ -11,7 +11,7 @@ describe('ee/search/highlight_blob_search_result', () => {
setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(4);
});
// Advanced search support
......@@ -20,6 +20,6 @@ describe('ee/search/highlight_blob_search_result', () => {
setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(3);
expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(3);
});
});
......@@ -7,14 +7,52 @@ RSpec.describe Dast::ProfileSchedule, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:dast_profile).class_name('Dast::Profile').required.inverse_of(:dast_profile_schedules) }
it { is_expected.to belong_to(:dast_profile).class_name('Dast::Profile').required.inverse_of(:dast_profile_schedule) }
it { is_expected.to belong_to(:owner).class_name('User').with_foreign_key(:user_id) }
end
describe 'validations' do
let(:timezones) { ActiveSupport::TimeZone.all.map { |tz| tz.tzinfo.identifier } }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:cron) }
it { is_expected.to validate_presence_of(:next_run_at) }
it { is_expected.to validate_presence_of(:timezone) }
it { is_expected.to validate_inclusion_of(:timezone).in_array(timezones) }
it { is_expected.to validate_presence_of(:starts_at) }
it { is_expected.to validate_uniqueness_of(:dast_profile_id) }
describe 'cadence' do
context 'when valid values' do
[
{ unit: 'day', duration: 1 },
{ unit: 'week', duration: 1 },
{ unit: 'month', duration: 1 },
{ unit: 'month', duration: 3 },
{ unit: 'month', duration: 6 },
{ unit: 'year', duration: 1 },
{}
].each do |cadence|
it "allows #{cadence[:unit]} values" do
schedule = build(:dast_profile_schedule, cadence: cadence)
expect(schedule).to be_valid
expect(schedule.cadence).to eq(cadence.stringify_keys)
end
end
end
context 'when invalid values' do
[
{ unit: 'day', duration: 3 },
{ unit: 'month_foo', duration: 100 }
].each do |cadence|
it "disallow #{cadence[:unit]} values" do
expect { build(:dast_profile_schedule, cadence: cadence).validate! }.to raise_error(ActiveRecord::RecordInvalid) do |err|
expect(err.record.errors.full_messages).to include('Cadence must be a valid json schema')
end
end
end
end
end
end
describe 'scopes' do
......@@ -31,13 +69,13 @@ RSpec.describe Dast::ProfileSchedule, type: :model do
end
end
describe 'runnable_schedules' do
describe '.runnable_schedules' do
subject { described_class.runnable_schedules }
context 'when there are runnable schedules' do
let!(:profile_schedule) do
travel_to(1.day.ago) do
create(:dast_profile_schedule)
travel_to(2.days.ago) do
create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 })
end
end
......@@ -80,15 +118,73 @@ RSpec.describe Dast::ProfileSchedule, type: :model do
end
end
describe 'before_save' do
describe '#set_cron' do
context 'when repeat? is true' do
it 'sets the cron value' do
freeze_time do
cron_statement = Gitlab::Ci::CronParser.parse_natural_with_timestamp(subject.starts_at, subject.cadence)
expect(subject.cron).to eq cron_statement
end
end
end
context 'when repeat? is false' do
subject { create(:dast_profile_schedule, cadence: {}) }
it 'sets the cron value to default when non repeating' do
expect(subject.cron).to eq Dast::ProfileSchedule::CRON_DEFAULT
end
end
end
end
describe '#set_next_run_at' do
it_behaves_like 'handles set_next_run_at' do
let(:schedule) { create(:dast_profile_schedule, cron: '*/1 * * * *') }
let(:schedule_1) { create(:dast_profile_schedule) }
let(:schedule_2) { create(:dast_profile_schedule) }
let(:new_cron) { '0 0 1 1 *' }
let(:ideal_next_run_at) { schedule.send(:ideal_next_run_from, Time.zone.now) }
let(:cron_worker_next_run_at) { schedule.send(:cron_worker_next_run_from, Time.zone.now) }
let(:schedule) { create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 }, starts_at: Time.zone.now) }
let(:schedule_1) { create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 }) }
let(:schedule_2) { create(:dast_profile_schedule, cadence: { unit: 'day', duration: 1 }) }
let(:cron_worker_next_run_at) { schedule.send(:cron_worker_next_run_from, Time.zone.now) }
context 'when schedule runs every minute' do
it "updates next_run_at to the worker's execution time" do
travel_to(1.day.ago) do
expect(schedule.next_run_at.to_i).to eq(cron_worker_next_run_at.to_i)
end
end
end
context 'when there are two different schedules in the same time zones' do
it 'sets the sames next_run_at' do
expect(schedule_1.next_run_at.to_i).to eq(schedule_2.next_run_at.to_i)
end
end
context 'when starts_at is updated for existing schedules' do
it 'updates next_run_at automatically' do
expect { schedule.update!(starts_at: Time.zone.now + 2.days) }.to change { schedule.next_run_at }
end
end
end
describe '#schedule_next_run!' do
context 'when repeat? is true' do
it 'sets active to true' do
subject.schedule_next_run!
expect(subject.active).to be true
end
end
context 'when repeat? is false' do
it 'sets active to false' do
subject.update_column(:cadence, {})
subject.schedule_next_run!
expect(subject.active).to be false
end
end
end
end
......@@ -12,7 +12,7 @@ RSpec.describe Dast::Profile, type: :model do
it { is_expected.to belong_to(:dast_site_profile) }
it { is_expected.to belong_to(:dast_scanner_profile) }
it { is_expected.to have_many(:secret_variables).through(:dast_site_profile).class_name('Dast::SiteProfileSecretVariable') }
it { is_expected.to have_many(:dast_profile_schedules).class_name('Dast::ProfileSchedule').with_foreign_key(:dast_profile_id).inverse_of(:dast_profile) }
it { is_expected.to have_one(:dast_profile_schedule).class_name('Dast::ProfileSchedule').with_foreign_key(:dast_profile_id).inverse_of(:dast_profile) }
end
describe 'validations' do
......
......@@ -101,5 +101,19 @@ RSpec.describe AppSec::Dast::ProfileScheduleWorker do
subject
end
end
context 'when single run schedule exists' do
before do
schedule.update_columns(next_run_at: 1.minute.ago, cadence: {})
end
it 'executes the rule schedule service and deactivate the schedule', :aggregate_failures do
expect(schedule.repeat?).to be(false)
subject
expect(schedule.reload.active).to be(false)
end
end
end
end
......@@ -6,8 +6,40 @@ module Gitlab
VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC'
VALID_SYNTAX_SAMPLE_CRON = '* * * * *'
def self.parse_natural(expression, cron_timezone = 'UTC')
new(Fugit::Nat.parse(expression)&.original, cron_timezone)
class << self
def parse_natural(expression, cron_timezone = 'UTC')
new(Fugit::Nat.parse(expression)&.original, cron_timezone)
end
# This method generates compatible expressions that can be
# parsed by Fugit::Nat.parse to generate a cron line.
# It takes start date of the cron and cadence in the following format:
# cadence = {
# unit: 'day/week/month/year'
# duration: 1
# }
def parse_natural_with_timestamp(starts_at, cadence)
case cadence[:unit]
when 'day' # Currently supports only 'every 1 day'.
"#{starts_at.min} #{starts_at.hour} * * *"
when 'week' # Currently supports only 'every 1 week'.
"#{starts_at.min} #{starts_at.hour} * * #{starts_at.wday}"
when 'month'
unless [1, 3, 6, 12].include?(cadence[:duration])
raise NotImplementedError, "The cadence #{cadence} is not supported"
end
"#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{fall_in_months(cadence[:duration], starts_at)} *"
when 'year' # Currently supports only 'every 1 year'.
"#{starts_at.min} #{starts_at.hour} #{starts_at.mday} #{starts_at.month} *"
else
raise NotImplementedError, "The cadence unit #{cadence[:unit]} is not implemented"
end
end
def fall_in_months(offset, start_date)
(1..(12 / offset)).map { |i| start_date.next_month(offset * i).month }.join(',')
end
end
def initialize(cron, cron_timezone = 'UTC')
......
......@@ -29006,6 +29006,9 @@ msgstr ""
msgid "Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. Default choice for Windows Shell executor."
msgstr ""
msgid "Runners|You are about to change this instance runner to a project runner. This operation is not reversible. Are you sure you want to continue?"
msgstr ""
msgid "Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner."
msgstr ""
......
......@@ -311,23 +311,42 @@ RSpec.describe Projects::PipelinesController do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
def create_build_with_artifacts(stage, stage_idx, name)
create(:ci_build, :artifacts, :tags, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
def create_build_with_artifacts(stage, stage_idx, name, status)
create(:ci_build, :artifacts, :tags, status, user: user, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
end
def create_bridge(stage, stage_idx, name, status)
create(:ci_bridge, status, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
end
before do
create_build_with_artifacts('build', 0, 'job1')
create_build_with_artifacts('build', 0, 'job2')
create_build_with_artifacts('build', 0, 'job1', :failed)
create_build_with_artifacts('build', 0, 'job2', :running)
create_build_with_artifacts('build', 0, 'job3', :pending)
create_bridge('deploy', 1, 'deploy-a', :failed)
create_bridge('deploy', 1, 'deploy-b', :created)
end
it 'avoids N+1 database queries', :request_store do
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_html }.count
it 'avoids N+1 database queries', :request_store, :use_sql_query_cache do
# warm up
get_pipeline_html
expect(response).to have_gitlab_http_status(:ok)
create_build_with_artifacts('build', 0, 'job3')
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get_pipeline_html
expect(response).to have_gitlab_http_status(:ok)
end
create_build_with_artifacts('build', 0, 'job4', :failed)
create_build_with_artifacts('build', 0, 'job5', :running)
create_build_with_artifacts('build', 0, 'job6', :pending)
create_bridge('deploy', 1, 'deploy-c', :failed)
create_bridge('deploy', 1, 'deploy-d', :created)
expect { get_pipeline_html }.not_to exceed_query_limit(control_count)
expect(response).to have_gitlab_http_status(:ok)
expect do
get_pipeline_html
expect(response).to have_gitlab_http_status(:ok)
end.not_to exceed_all_query_limit(control)
end
end
......
......@@ -31,7 +31,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environment,
canReadEnvironment: true,
tableData,
},
});
......@@ -135,7 +134,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environmentWithoutDeployable,
canReadEnvironment: true,
tableData,
},
});
......@@ -161,7 +159,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environmentWithoutUpcomingDeployment,
canReadEnvironment: true,
tableData,
},
});
......@@ -177,7 +174,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: environment,
canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
......@@ -205,7 +201,6 @@ describe('Environment item', () => {
...environment,
auto_stop_at: futureDate,
},
canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
......@@ -241,7 +236,6 @@ describe('Environment item', () => {
...environment,
auto_stop_at: pastDate,
},
canReadEnvironment: true,
tableData,
shouldShowAutoStopDate: true,
},
......@@ -360,7 +354,6 @@ describe('Environment item', () => {
factory({
propsData: {
model: folder,
canReadEnvironment: true,
tableData,
},
});
......
......@@ -28,7 +28,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: [folder],
canReadEnvironment: true,
...eeOnlyProps,
},
});
......@@ -50,7 +49,6 @@ describe('Environment table', () => {
await factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
......@@ -78,7 +76,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
......@@ -114,7 +111,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
......@@ -151,7 +147,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
......@@ -179,7 +174,6 @@ describe('Environment table', () => {
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
......@@ -230,7 +224,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps,
},
});
......@@ -296,7 +289,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps,
},
});
......@@ -335,7 +327,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps,
},
});
......@@ -364,7 +355,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps,
},
});
......@@ -415,7 +405,6 @@ describe('Environment table', () => {
factory({
propsData: {
environments: mockItems,
canReadEnvironment: true,
...eeOnlyProps,
},
});
......
......@@ -20,7 +20,6 @@ describe('Environment', () => {
const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
canReadEnvironment: true,
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
userCalloutsPath: '/callouts',
......
......@@ -44,7 +44,6 @@ describe('Environments detail header component', () => {
TimeAgo,
},
propsData: {
canReadEnvironment: false,
canAdminEnvironment: false,
canUpdateEnvironment: false,
canStopEnvironment: false,
......@@ -60,7 +59,7 @@ describe('Environments detail header component', () => {
describe('default state with minimal access', () => {
beforeEach(() => {
createWrapper({ props: { environment: createEnvironment() } });
createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
});
it('displays the environment name', () => {
......@@ -164,7 +163,6 @@ describe('Environments detail header component', () => {
createWrapper({
props: {
environment: createEnvironment({ hasTerminals: true, externalUrl }),
canReadEnvironment: true,
},
});
});
......@@ -178,8 +176,7 @@ describe('Environments detail header component', () => {
beforeEach(() => {
createWrapper({
props: {
environment: createEnvironment(),
canReadEnvironment: true,
environment: createEnvironment({ metricsUrl: 'my metrics url' }),
metricsPath,
},
});
......@@ -195,7 +192,6 @@ describe('Environments detail header component', () => {
createWrapper({
props: {
environment: createEnvironment(),
canReadEnvironment: true,
canAdminEnvironment: true,
canStopEnvironment: true,
canUpdateEnvironment: true,
......
......@@ -11,7 +11,6 @@ describe('Environments Folder View', () => {
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
canReadEnvironment: true,
cssContainerClass: 'container',
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
......
......@@ -14,7 +14,6 @@ describe('Environments Folder View', () => {
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
canReadEnvironment: true,
cssContainerClass: 'container',
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
......
import { shallowMount } from '@vue/test-utils';
import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
describe('Image Viewer', () => {
let wrapper;
const propsData = {
url: 'some/image.png',
alt: 'image.png',
};
const createComponent = () => {
wrapper = shallowMount(ImageViewer, { propsData });
};
const findImage = () => wrapper.find('[data-testid="image"]');
it('renders a Source Editor component', () => {
createComponent();
expect(findImage().exists()).toBe(true);
expect(findImage().attributes('src')).toBe(propsData.url);
expect(findImage().attributes('alt')).toBe(propsData.alt);
});
});
......@@ -9,6 +9,6 @@ describe('search/highlight_blob_search_result', () => {
it('highlights lines with search term occurrence', () => {
setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
expect(document.querySelectorAll('.js-blob-result .hll').length).toBe(4);
});
});
......@@ -43,7 +43,6 @@ RSpec.describe EnvironmentHelper do
external_url: environment.external_url,
can_update_environment: true,
can_destroy_environment: true,
can_read_environment: true,
can_stop_environment: true,
can_admin_environment: true,
environment_metrics_path: environment_metrics_path(environment),
......
......@@ -297,4 +297,65 @@ RSpec.describe Gitlab::Ci::CronParser do
it { is_expected.to eq(true) }
end
end
describe '.parse_natural', :aggregate_failures do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'day', duration: 1 }) }
let(:time) { Time.parse('Mon, 30 Aug 2021 06:29:44.067132000 UTC +00:00') }
let(:hours) { Fugit::Cron.parse(cron_line).hours }
let(:minutes) { Fugit::Cron.parse(cron_line).minutes }
let(:weekdays) { Fugit::Cron.parse(cron_line).weekdays.first }
let(:months) { Fugit::Cron.parse(cron_line).months }
context 'when repeat cycle is day' do
it 'generates daily cron expression', :aggregate_failures do
expect(hours).to include time.hour
expect(minutes).to include time.min
end
end
context 'when repeat cycle is week' do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'week', duration: 1 }) }
it 'generates weekly cron expression', :aggregate_failures do
expect(hours).to include time.hour
expect(minutes).to include time.min
expect(weekdays).to include time.wday
end
end
context 'when repeat cycle is month' do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 3 }) }
it 'generates monthly cron expression', :aggregate_failures do
expect(minutes).to include time.min
expect(months).to include time.month
end
context 'when an unsupported duration is specified' do
subject { described_class.parse_natural_with_timestamp(time, { unit: 'month', duration: 7 }) }
it 'raises an exception' do
expect { subject }.to raise_error(NotImplementedError, 'The cadence {:unit=>"month", :duration=>7} is not supported')
end
end
end
context 'when repeat cycle is year' do
let(:cron_line) { described_class.parse_natural_with_timestamp(time, { unit: 'year', duration: 1 }) }
it 'generates yearly cron expression', :aggregate_failures do
expect(hours).to include time.hour
expect(minutes).to include time.min
expect(months).to include time.month
end
end
context 'when the repeat cycle is not implemented' do
subject { described_class.parse_natural_with_timestamp(time, { unit: 'quarterly', duration: 1 }) }
it 'raises an exception' do
expect { subject }.to raise_error(NotImplementedError, 'The cadence unit quarterly is not implemented')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment