Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ce42a2ec96
commit
8018200540
2
Gemfile
2
Gemfile
|
@ -493,7 +493,7 @@ gem 'kas-grpc', '~> 0.0.2'
|
|||
|
||||
gem 'grpc', '~> 1.42.0'
|
||||
|
||||
gem 'google-protobuf', '~> 3.19.0'
|
||||
gem 'google-protobuf', '~> 3.21'
|
||||
|
||||
gem 'toml-rb', '~> 2.0'
|
||||
|
||||
|
|
|
@ -580,7 +580,7 @@ GEM
|
|||
signet (~> 0.12)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-protobuf (3.19.4)
|
||||
google-protobuf (3.21.3)
|
||||
googleapis-common-protos-types (1.3.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.14.0)
|
||||
|
@ -1580,7 +1580,7 @@ DEPENDENCIES
|
|||
gitlab_omniauth-ldap (~> 2.2.0)
|
||||
gon (~> 6.4.0)
|
||||
google-api-client (~> 0.33)
|
||||
google-protobuf (~> 3.19.0)
|
||||
google-protobuf (~> 3.21)
|
||||
gpgme (~> 2.0.19)
|
||||
grape (~> 1.5.2)
|
||||
grape-entity (~> 0.10.0)
|
||||
|
|
|
@ -2,6 +2,7 @@ import { DEFAULT_PER_PAGE } from '~/api';
|
|||
import axios from '../lib/utils/axios_utils';
|
||||
import { buildApiUrl } from './api_utils';
|
||||
|
||||
const GROUP_PATH = '/api/:version/groups/:id';
|
||||
const GROUPS_PATH = '/api/:version/groups.json';
|
||||
const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups';
|
||||
|
||||
|
@ -30,3 +31,9 @@ export function getDescendentGroups(parentGroupId, query, options, callback = ()
|
|||
const url = buildApiUrl(DESCENDANT_GROUPS_PATH.replace(':id', parentGroupId));
|
||||
return axiosGet(url, query, options, callback);
|
||||
}
|
||||
|
||||
export function updateGroup(groupId, data = {}) {
|
||||
const url = buildApiUrl(GROUP_PATH).replace(':id', groupId);
|
||||
|
||||
return axios.put(url, data);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlToggle, GlAlert } from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { updateGroup } from '~/api/groups_api';
|
||||
import { I18N_UPDATE_ERROR_MESSAGE, I18N_REFRESH_MESSAGE } from '../constants';
|
||||
|
||||
export default {
|
||||
|
@ -9,7 +9,7 @@ export default {
|
|||
GlAlert,
|
||||
},
|
||||
inject: [
|
||||
'updatePath',
|
||||
'groupId',
|
||||
'sharedRunnersSetting',
|
||||
'parentSharedRunnersSetting',
|
||||
'runnerEnabledValue',
|
||||
|
@ -54,8 +54,7 @@ export default {
|
|||
|
||||
this.isLoading = true;
|
||||
|
||||
axios
|
||||
.put(this.updatePath, { shared_runners_setting: setting })
|
||||
updateGroup(this.groupId, { shared_runners_setting: setting })
|
||||
.then(() => {
|
||||
this.value = setting;
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@ export default (containerId = 'update-shared-runners-form') => {
|
|||
const containerEl = document.getElementById(containerId);
|
||||
|
||||
const {
|
||||
updatePath,
|
||||
groupId,
|
||||
sharedRunnersSetting,
|
||||
parentSharedRunnersSetting,
|
||||
runnerEnabledValue,
|
||||
|
@ -16,7 +16,7 @@ export default (containerId = 'update-shared-runners-form') => {
|
|||
return new Vue({
|
||||
el: containerEl,
|
||||
provide: {
|
||||
updatePath,
|
||||
groupId,
|
||||
sharedRunnersSetting,
|
||||
parentSharedRunnersSetting,
|
||||
runnerEnabledValue,
|
||||
|
|
|
@ -91,7 +91,7 @@ export default {
|
|||
modalId: 'set-user-status-modal',
|
||||
noEmoji: true,
|
||||
availability: isUserBusy(this.currentAvailability),
|
||||
clearStatusAfter: statusTimeRanges[0].label,
|
||||
clearStatusAfter: statusTimeRanges[0],
|
||||
clearStatusAfterMessage: sprintf(s__('SetStatusModal|Your status resets on %{date}.'), {
|
||||
date: this.currentClearStatusAfter,
|
||||
}),
|
||||
|
@ -178,9 +178,7 @@ export default {
|
|||
message,
|
||||
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
|
||||
clearStatusAfter:
|
||||
clearStatusAfter === statusTimeRanges[0].label
|
||||
? null
|
||||
: clearStatusAfter.replace(' ', '_'),
|
||||
clearStatusAfter.label === statusTimeRanges[0].label ? null : clearStatusAfter.shortcut,
|
||||
})
|
||||
.then(this.onUpdateSuccess)
|
||||
.catch(this.onUpdateFail);
|
||||
|
@ -279,12 +277,12 @@ export default {
|
|||
<div class="form-group">
|
||||
<div class="gl-display-flex gl-align-items-baseline">
|
||||
<span class="gl-mr-3">{{ s__('SetStatusModal|Clear status after') }}</span>
|
||||
<gl-dropdown :text="clearStatusAfter" data-testid="clear-status-at-dropdown">
|
||||
<gl-dropdown :text="clearStatusAfter.label" data-testid="clear-status-at-dropdown">
|
||||
<gl-dropdown-item
|
||||
v-for="after in $options.statusTimeRanges"
|
||||
:key="after.name"
|
||||
:data-testid="after.name"
|
||||
@click="setClearStatusAfter(after.label)"
|
||||
@click="setClearStatusAfter(after)"
|
||||
>{{ after.label }}</gl-dropdown-item
|
||||
>
|
||||
</gl-dropdown>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { __, sprintf } from '~/locale';
|
||||
import { __, n__, sprintf } from '~/locale';
|
||||
import { IssuableType, WorkspaceType } from '~/issues/constants';
|
||||
|
||||
const INTERVALS = {
|
||||
|
@ -15,51 +15,62 @@ export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';
|
|||
|
||||
export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT];
|
||||
|
||||
const getTimeLabel = (days) => n__('1 day', '%d days', days);
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export const timeRanges = [
|
||||
{
|
||||
label: __('30 minutes'),
|
||||
label: n__('1 minute', '%d minutes', 30),
|
||||
shortcut: '30_minutes',
|
||||
duration: { seconds: 60 * 30 },
|
||||
name: 'thirtyMinutes',
|
||||
interval: INTERVALS.minute,
|
||||
},
|
||||
{
|
||||
label: __('3 hours'),
|
||||
label: n__('1 hour', '%d hours', 3),
|
||||
shortcut: '3_hours',
|
||||
duration: { seconds: 60 * 60 * 3 },
|
||||
name: 'threeHours',
|
||||
interval: INTERVALS.hour,
|
||||
},
|
||||
{
|
||||
label: __('8 hours'),
|
||||
label: n__('1 hour', '%d hours', 8),
|
||||
shortcut: '8_hours',
|
||||
duration: { seconds: 60 * 60 * 8 },
|
||||
name: 'eightHours',
|
||||
default: true,
|
||||
interval: INTERVALS.hour,
|
||||
},
|
||||
{
|
||||
label: __('1 day'),
|
||||
label: getTimeLabel(1),
|
||||
shortcut: '1_day',
|
||||
duration: { seconds: 60 * 60 * 24 * 1 },
|
||||
name: 'oneDay',
|
||||
interval: INTERVALS.hour,
|
||||
},
|
||||
{
|
||||
label: __('3 days'),
|
||||
label: getTimeLabel(3),
|
||||
shortcut: '3_days',
|
||||
duration: { seconds: 60 * 60 * 24 * 3 },
|
||||
name: 'threeDays',
|
||||
interval: INTERVALS.hour,
|
||||
},
|
||||
{
|
||||
label: __('7 days'),
|
||||
label: getTimeLabel(7),
|
||||
shortcut: '7_days',
|
||||
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
|
||||
name: 'oneWeek',
|
||||
interval: INTERVALS.day,
|
||||
},
|
||||
{
|
||||
label: __('30 days'),
|
||||
label: getTimeLabel(30),
|
||||
shortcut: '30_days',
|
||||
duration: { seconds: 60 * 60 * 24 * 30 },
|
||||
name: 'oneMonth',
|
||||
interval: INTERVALS.day,
|
||||
},
|
||||
];
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
|
||||
export const defaultTimeRange = timeRanges.find((tr) => tr.default);
|
||||
export const getTimeWindow = (timeWindowName) =>
|
||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
|
@ -61,15 +61,17 @@ export default {
|
|||
:id="$options.labelId"
|
||||
:value="state"
|
||||
:options="$options.states"
|
||||
:disabled="loading"
|
||||
:disabled="disabled"
|
||||
class="gl-w-auto hide-select-decoration"
|
||||
:class="{ 'gl-bg-transparent! gl-cursor-text!': disabled }"
|
||||
@change="setState"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.hide-select-decoration:not(:focus, :hover) {
|
||||
.hide-select-decoration:not(:focus, :hover),
|
||||
.hide-select-decoration:disabled {
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default {
|
|||
<template>
|
||||
<h2
|
||||
class="gl-font-weight-normal gl-sm-font-weight-bold gl-mb-5 gl-mt-0 gl-w-full"
|
||||
:class="{ 'gl-cursor-not-allowed': disabled }"
|
||||
:class="{ 'gl-cursor-text': disabled }"
|
||||
aria-labelledby="item-title"
|
||||
>
|
||||
<div
|
||||
|
@ -46,7 +46,8 @@ export default {
|
|||
:aria-label="__('Title')"
|
||||
:data-placeholder="placeholder"
|
||||
:contenteditable="!disabled"
|
||||
class="gl-pseudo-placeholder gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
|
||||
class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base"
|
||||
:class="{ 'gl-hover-border-gray-200 gl-pseudo-placeholder': !disabled }"
|
||||
@blur="handleBlur"
|
||||
@keyup="handleInput"
|
||||
@keydown.enter.exact="handleSubmit"
|
||||
|
|
|
@ -206,11 +206,13 @@ export default {
|
|||
:work-item-title="workItem.title"
|
||||
:work-item-type="workItemType"
|
||||
:work-item-parent-id="workItemParentId"
|
||||
:can-update="canUpdate"
|
||||
@error="error = $event"
|
||||
/>
|
||||
<work-item-state
|
||||
:work-item="workItem"
|
||||
:work-item-parent-id="workItemParentId"
|
||||
:can-update="canUpdate"
|
||||
@error="error = $event"
|
||||
/>
|
||||
<template v-if="workItemsMvc2Enabled">
|
||||
|
|
|
@ -202,7 +202,8 @@ export default {
|
|||
:dropdown-items="searchLabels"
|
||||
:loading="isLoading"
|
||||
:view-only="!canUpdate"
|
||||
class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2!"
|
||||
class="gl-flex-grow-1 gl-border gl-border-white gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2!"
|
||||
:class="{ 'gl-hover-border-gray-200': canUpdate }"
|
||||
@input="focusTokenSelector"
|
||||
@text-input="debouncedSearchKeyUpdate"
|
||||
@focus="handleFocus"
|
||||
|
|
|
@ -27,6 +27,11 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -102,7 +107,7 @@ export default {
|
|||
<item-state
|
||||
v-if="workItem.state"
|
||||
:state="workItem.state"
|
||||
:loading="updateInProgress"
|
||||
:disabled="updateInProgress || !canUpdate"
|
||||
@changed="updateWorkItemState"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -31,6 +31,11 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tracking() {
|
||||
|
@ -84,5 +89,5 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<item-title :title="workItemTitle" @title-changed="updateTitle" />
|
||||
<item-title :title="workItemTitle" :disabled="!canUpdate" @title-changed="updateTitle" />
|
||||
</template>
|
||||
|
|
|
@ -98,6 +98,7 @@ export default {
|
|||
}
|
||||
},
|
||||
updateWeight(event) {
|
||||
if (!this.canUpdate) return;
|
||||
this.isEditing = false;
|
||||
|
||||
const weight = Number(event.target.value);
|
||||
|
|
|
@ -73,7 +73,7 @@ module Ci
|
|||
|
||||
def group_shared_runners_settings_data(group)
|
||||
{
|
||||
update_path: api_v4_groups_path(id: group.id),
|
||||
group_id: group.id,
|
||||
shared_runners_setting: group.shared_runners_setting,
|
||||
parent_shared_runners_setting: group.parent&.shared_runners_setting,
|
||||
runner_enabled_value: Namespace::SR_ENABLED,
|
||||
|
|
|
@ -1,31 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects < Gitlab::Database::Migration[2.0]
|
||||
MIGRATION = 'SetLegacyOpenSourceLicenseAvailableForNonPublicProjects'
|
||||
INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 5_000
|
||||
SUB_BATCH_SIZE = 200
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:projects,
|
||||
:id,
|
||||
job_interval: INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
# Replaced by 20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects.rb
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, :projects, :id, [])
|
||||
# Replaced by 20220722110026_reschedule_set_legacy_open_source_license_available_for_non_public_projects.rb
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RescheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects < Gitlab::Database::Migration[2.0]
|
||||
MIGRATION = 'SetLegacyOpenSourceLicenseAvailableForNonPublicProjects'
|
||||
INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 5_000
|
||||
MAX_BATCH_SIZE = 10_000
|
||||
SUB_BATCH_SIZE = 200
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, :projects, :id, [])
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:projects,
|
||||
:id,
|
||||
job_interval: INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
max_batch_size: MAX_BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, :projects, :id, [])
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
79c7847740cb02fffeaeae55f869889f201b7a9431693bea7249ddff9d405fb4
|
|
@ -101,6 +101,60 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
|
|||
}
|
||||
```
|
||||
|
||||
## Get all pipelines triggered by a pipeline schedule
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/368566) in GitLab 15.3.
|
||||
|
||||
Get all pipelines triggered by a pipeline schedule in a project.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/pipeline_schedules/:pipeline_schedule_id/pipelines
|
||||
```
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------------|----------------|----------|-------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID. |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/pipelines"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 47,
|
||||
"iid": 12,
|
||||
"project_id": 29,
|
||||
"status": "pending",
|
||||
"source": "scheduled",
|
||||
"ref": "new-pipeline",
|
||||
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"web_url": "https://example.com/foo/bar/pipelines/47",
|
||||
"created_at": "2016-08-11T11:28:34.085Z",
|
||||
"updated_at": "2016-08-11T11:32:35.169Z"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"iid": 13,
|
||||
"project_id": 29,
|
||||
"status": "pending",
|
||||
"source": "scheduled",
|
||||
"ref": "new-pipeline",
|
||||
"sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
|
||||
"web_url": "https://example.com/foo/bar/pipelines/48",
|
||||
"created_at": "2016-08-12T10:06:04.561Z",
|
||||
"updated_at": "2016-08-12T10:09:56.223Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Create a new pipeline schedule
|
||||
|
||||
Create a new pipeline schedule of a project.
|
||||
|
|
|
@ -7,9 +7,9 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo
|
|||
description: "Development Guidelines: learn how to contribute to GitLab."
|
||||
---
|
||||
|
||||
# Contributor and Development Docs
|
||||
# Contribute to the development of GitLab
|
||||
|
||||
Learn the processes and technical information needed for contributing to GitLab.
|
||||
Learn how to contribute to the development of the GitLab product.
|
||||
|
||||
This content is intended for members of the GitLab Team as well as community
|
||||
contributors. Content specific to the GitLab Team should instead be included in
|
||||
|
|
|
@ -385,6 +385,43 @@ To run the LDAP tests on your local with TLS disabled, follow these steps:
|
|||
GITLAB_LDAP_USERNAME="tanuki" GITLAB_LDAP_PASSWORD="password" QA_LOG_LEVEL=debug WEBDRIVER_HEADLESS=false bin/qa Test::Instance::All http://localhost qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
|
||||
```
|
||||
|
||||
## SMTP tests
|
||||
|
||||
Tests that are tagged with `:smtp` meta tag are orchestrated tests that ensure email notifications are received by a user.
|
||||
|
||||
These tests require a GitLab instance with SMTP enabled and integrated with an SMTP server, [MailHog](https://github.com/mailhog/MailHog).
|
||||
|
||||
To run these tests locally against the GDK:
|
||||
|
||||
1. Add these settings to your `gitlab.yml` file:
|
||||
|
||||
```yaml
|
||||
smtp:
|
||||
enabled: true
|
||||
address: "mailhog.test"
|
||||
port: 1025
|
||||
```
|
||||
|
||||
1. Start MailHog in a Docker container:
|
||||
|
||||
```shell
|
||||
docker network create test && docker run \
|
||||
--network test \
|
||||
--hostname mailhog.test \
|
||||
--name mailhog \
|
||||
--publish 1025:1025 \
|
||||
--publish 8025:8025 \
|
||||
mailhog/mailhog:v1.0.0
|
||||
```
|
||||
|
||||
1. Run the test from [`gitlab/qa`](https://gitlab.com/gitlab-org/gitlab/-/tree/d5447ebb5f99d4c72780681ddf4dc25b0738acba/qa) directory:
|
||||
|
||||
```shell
|
||||
QA_LOG_LEVEL=debug WEBDRIVER_HEADLESS=false bin/qa Test::Instance::All http://localhost:3000 qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb -- --tag orchestrated
|
||||
```
|
||||
|
||||
For instructions on how to run these tests using the `gitlab-qa` gem, please refer to [the GitLab QA documentation](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/master/docs/what_tests_can_be_run.md#testintegrationsmtp-ceeefull-image-address).
|
||||
|
||||
## Guide to the mobile suite
|
||||
|
||||
### What are mobile tests
|
||||
|
|
|
@ -42,6 +42,16 @@ module API
|
|||
present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails, user: current_user
|
||||
end
|
||||
|
||||
desc 'Get all pipelines triggered from a pipeline schedule' do
|
||||
success Entities::Ci::PipelineBasic
|
||||
end
|
||||
params do
|
||||
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule ID'
|
||||
end
|
||||
get ':id/pipeline_schedules/:pipeline_schedule_id/pipelines' do
|
||||
present paginate(pipeline_schedule.pipelines), with: Entities::Ci::PipelineBasic
|
||||
end
|
||||
|
||||
desc 'Create a new pipeline schedule' do
|
||||
success Entities::Ci::PipelineScheduleDetails
|
||||
end
|
||||
|
|
|
@ -33,8 +33,10 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
# We decrease the urgency of this endpoint until the maxmemory issue of redis-cache has been resolved.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/365575#note_1033611872 for more information.
|
||||
desc 'Get a list of features'
|
||||
get 'client/features' do
|
||||
get 'client/features', urgency: :low do
|
||||
if ::Feature.enabled?(:cache_unleash_client_api, project)
|
||||
present_feature_flags
|
||||
else
|
||||
|
|
|
@ -44,7 +44,8 @@ module Gitlab
|
|||
sidekiq_current_rss: ::Gitlab::Metrics.gauge(:sidekiq_current_rss, 'Current RSS of Sidekiq Worker'),
|
||||
sidekiq_memory_killer_soft_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_soft_limit_rss, 'Current soft_limit_rss of Sidekiq Worker'),
|
||||
sidekiq_memory_killer_hard_limit_rss: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_hard_limit_rss, 'Current hard_limit_rss of Sidekiq Worker'),
|
||||
sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker')
|
||||
sidekiq_memory_killer_phase: ::Gitlab::Metrics.gauge(:sidekiq_memory_killer_phase, 'Current phase of Sidekiq Worker'),
|
||||
sidekiq_memory_killer_running_jobs: ::Gitlab::Metrics.counter(:sidekiq_memory_killer_running_jobs_total, 'Current running jobs when limit was reached')
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -166,6 +167,8 @@ module Gitlab
|
|||
@soft_limit_rss,
|
||||
deadline_exceeded)
|
||||
|
||||
running_jobs = fetch_running_jobs
|
||||
|
||||
Sidekiq.logger.warn(
|
||||
class: self.class.to_s,
|
||||
pid: pid,
|
||||
|
@ -175,9 +178,17 @@ module Gitlab
|
|||
hard_limit_rss: @hard_limit_rss,
|
||||
reason: reason,
|
||||
running_jobs: running_jobs)
|
||||
|
||||
increment_worker_counters(running_jobs, deadline_exceeded)
|
||||
end
|
||||
|
||||
def running_jobs
|
||||
def increment_worker_counters(running_jobs, deadline_exceeded)
|
||||
running_jobs.each do |job|
|
||||
@metrics[:sidekiq_memory_killer_running_jobs].increment( { worker_class: job[:worker_class], deadline_exceeded: deadline_exceeded } )
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_running_jobs
|
||||
jobs = []
|
||||
Gitlab::SidekiqDaemon::Monitor.instance.jobs_mutex.synchronize do
|
||||
jobs = Gitlab::SidekiqDaemon::Monitor.instance.jobs.map do |jid, job|
|
||||
|
|
|
@ -1508,15 +1508,9 @@ msgstr ""
|
|||
msgid "2FADevice|Registered On"
|
||||
msgstr ""
|
||||
|
||||
msgid "3 days"
|
||||
msgstr ""
|
||||
|
||||
msgid "3 hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "30 days"
|
||||
msgstr ""
|
||||
|
||||
msgid "30 minutes"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1544,9 +1538,6 @@ msgstr ""
|
|||
msgid "409|There was a conflict with your request."
|
||||
msgstr ""
|
||||
|
||||
msgid "7 days"
|
||||
msgstr ""
|
||||
|
||||
msgid "8 hours"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import httpStatus from '~/lib/utils/http_status';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { updateGroup } from '~/api/groups_api';
|
||||
|
||||
const mockApiVersion = 'v4';
|
||||
const mockUrlRoot = '/gitlab';
|
||||
|
||||
describe('GroupsApi', () => {
|
||||
let originalGon;
|
||||
let mock;
|
||||
|
||||
const dummyGon = {
|
||||
api_version: mockApiVersion,
|
||||
relative_url_root: mockUrlRoot,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
originalGon = window.gon;
|
||||
window.gon = { ...dummyGon };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
window.gon = originalGon;
|
||||
});
|
||||
|
||||
describe('updateGroup', () => {
|
||||
const mockGroupId = '99';
|
||||
const mockData = { attr: 'value' };
|
||||
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}`;
|
||||
|
||||
beforeEach(() => {
|
||||
mock.onPut(expectedUrl).reply(({ data }) => {
|
||||
return [httpStatus.OK, { id: mockGroupId, ...JSON.parse(data) }];
|
||||
});
|
||||
});
|
||||
|
||||
it('updates group', async () => {
|
||||
const res = await updateGroup(mockGroupId, mockData);
|
||||
|
||||
expect(res.data).toMatchObject({ id: mockGroupId, ...mockData });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,24 +1,24 @@
|
|||
import { GlAlert } from '@gitlab/ui';
|
||||
import MockAxiosAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { updateGroup } from '~/api/groups_api';
|
||||
|
||||
const UPDATE_PATH = '/test/update';
|
||||
jest.mock('~/api/groups_api');
|
||||
|
||||
const GROUP_ID = '99';
|
||||
const RUNNER_ENABLED_VALUE = 'enabled';
|
||||
const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable';
|
||||
const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_with_override';
|
||||
|
||||
describe('group_settings/components/shared_runners_form', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
|
||||
const createComponent = (provide = {}) => {
|
||||
wrapper = shallowMountExtended(SharedRunnersForm, {
|
||||
provide: {
|
||||
updatePath: UPDATE_PATH,
|
||||
groupId: GROUP_ID,
|
||||
sharedRunnersSetting: RUNNER_ENABLED_VALUE,
|
||||
parentSharedRunnersSetting: null,
|
||||
runnerEnabledValue: RUNNER_ENABLED_VALUE,
|
||||
|
@ -36,18 +36,19 @@ describe('group_settings/components/shared_runners_form', () => {
|
|||
.at(0);
|
||||
const findSharedRunnersToggle = () => wrapper.findByTestId('shared-runners-toggle');
|
||||
const findOverrideToggle = () => wrapper.findByTestId('override-runners-toggle');
|
||||
const getSharedRunnersSetting = () => JSON.parse(mock.history.put[0].data).shared_runners_setting;
|
||||
const getSharedRunnersSetting = () => {
|
||||
return updateGroup.mock.calls[0][1].shared_runners_setting;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAxiosAdapter(axios);
|
||||
mock.onPut(UPDATE_PATH).reply(200);
|
||||
updateGroup.mockResolvedValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
|
||||
mock.restore();
|
||||
updateGroup.mockReset();
|
||||
});
|
||||
|
||||
describe('default state', () => {
|
||||
|
@ -115,7 +116,7 @@ describe('group_settings/components/shared_runners_form', () => {
|
|||
findSharedRunnersToggle().vm.$emit('change', false);
|
||||
await waitForPromises();
|
||||
|
||||
expect(mock.history.put.length).toBe(1);
|
||||
expect(updateGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is not loading state after completed request', async () => {
|
||||
|
@ -170,12 +171,14 @@ describe('group_settings/components/shared_runners_form', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
errorObj | message
|
||||
errorData | message
|
||||
${{}} | ${'An error occurred while updating configuration. Refresh the page and try again.'}
|
||||
${{ error: 'Undefined error' }} | ${'Undefined error Refresh the page and try again.'}
|
||||
`(`with error $errorObj`, ({ errorObj, message }) => {
|
||||
`(`with error $errorObj`, ({ errorData, message }) => {
|
||||
beforeEach(async () => {
|
||||
mock.onPut(UPDATE_PATH).reply(500, errorObj);
|
||||
updateGroup.mockRejectedValue({
|
||||
response: { data: errorData },
|
||||
});
|
||||
|
||||
createComponent();
|
||||
findSharedRunnersToggle().vm.$emit('change', false);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { GlFormSelect } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
|
||||
import ItemState from '~/work_items/components/item_state.vue';
|
||||
|
@ -6,6 +7,7 @@ describe('ItemState', () => {
|
|||
let wrapper;
|
||||
|
||||
const findLabel = () => wrapper.find('label').text();
|
||||
const findFormSelect = () => wrapper.findComponent(GlFormSelect);
|
||||
const selectedValue = () => wrapper.find('option:checked').element.value;
|
||||
|
||||
const clickOpen = () => wrapper.findAll('option').at(0).setSelected();
|
||||
|
@ -51,4 +53,18 @@ describe('ItemState', () => {
|
|||
|
||||
expect(wrapper.emitted('changed')).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('form select disabled prop', () => {
|
||||
describe.each`
|
||||
description | disabled | value
|
||||
${'when not disabled'} | ${false} | ${undefined}
|
||||
${'when disabled'} | ${true} | ${'disabled'}
|
||||
`('$description', ({ disabled, value }) => {
|
||||
it(`renders form select component with disabled=${value}`, () => {
|
||||
createComponent({ disabled });
|
||||
|
||||
expect(findFormSelect().attributes('disabled')).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('ItemTitle', () => {
|
|||
disabled: true,
|
||||
});
|
||||
|
||||
expect(wrapper.classes()).toContain('gl-cursor-not-allowed');
|
||||
expect(wrapper.classes()).toContain('gl-cursor-text');
|
||||
expect(findInputEl().attributes('contenteditable')).toBe('false');
|
||||
});
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('WorkItemState component', () => {
|
|||
const createComponent = ({
|
||||
state = STATE_OPEN,
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
canUpdate = true,
|
||||
} = {}) => {
|
||||
const { id, workItemType } = workItemQueryResponse.data.workItem;
|
||||
wrapper = shallowMount(WorkItemState, {
|
||||
|
@ -39,6 +40,7 @@ describe('WorkItemState component', () => {
|
|||
state,
|
||||
workItemType,
|
||||
},
|
||||
canUpdate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -53,6 +55,20 @@ describe('WorkItemState component', () => {
|
|||
expect(findItemState().props('state')).toBe(workItemQueryResponse.data.workItem.state);
|
||||
});
|
||||
|
||||
describe('item state disabled prop', () => {
|
||||
describe.each`
|
||||
description | canUpdate | value
|
||||
${'when cannot update'} | ${false} | ${true}
|
||||
${'when can update'} | ${true} | ${false}
|
||||
`('$description', ({ canUpdate, value }) => {
|
||||
it(`renders item state component with disabled=${value}`, () => {
|
||||
createComponent({ canUpdate });
|
||||
|
||||
expect(findItemState().props('disabled')).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating the state', () => {
|
||||
it('calls a mutation', () => {
|
||||
createComponent();
|
||||
|
|
|
@ -20,7 +20,11 @@ describe('WorkItemTitle component', () => {
|
|||
|
||||
const findItemTitle = () => wrapper.findComponent(ItemTitle);
|
||||
|
||||
const createComponent = ({ workItemParentId, mutationHandler = mutationSuccessHandler } = {}) => {
|
||||
const createComponent = ({
|
||||
workItemParentId,
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
canUpdate = true,
|
||||
} = {}) => {
|
||||
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
|
||||
wrapper = shallowMount(WorkItemTitle, {
|
||||
apolloProvider: createMockApollo([
|
||||
|
@ -32,6 +36,7 @@ describe('WorkItemTitle component', () => {
|
|||
workItemTitle: title,
|
||||
workItemType: workItemType.name,
|
||||
workItemParentId,
|
||||
canUpdate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -46,6 +51,20 @@ describe('WorkItemTitle component', () => {
|
|||
expect(findItemTitle().props('title')).toBe(workItemQueryResponse.data.workItem.title);
|
||||
});
|
||||
|
||||
describe('item title disabled prop', () => {
|
||||
describe.each`
|
||||
description | canUpdate | value
|
||||
${'when cannot update'} | ${false} | ${true}
|
||||
${'when can update'} | ${true} | ${false}
|
||||
`('$description', ({ canUpdate, value }) => {
|
||||
it(`renders item title component with disabled=${value}`, () => {
|
||||
createComponent({ canUpdate });
|
||||
|
||||
expect(findItemTitle().props('disabled')).toBe(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating the title', () => {
|
||||
it('calls a mutation', () => {
|
||||
const title = 'new title!';
|
||||
|
|
|
@ -135,7 +135,12 @@ describe('WorkItemWeight component', () => {
|
|||
describe('when blurred', () => {
|
||||
it('calls a mutation to update the weight when the input value is different', () => {
|
||||
const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
||||
createComponent({ isEditing: true, weight: 0, mutationHandler: mutationSpy });
|
||||
createComponent({
|
||||
isEditing: true,
|
||||
weight: 0,
|
||||
mutationHandler: mutationSpy,
|
||||
canUpdate: true,
|
||||
});
|
||||
|
||||
findInput().vm.$emit('blur', { target: { value: 1 } });
|
||||
|
||||
|
@ -151,7 +156,7 @@ describe('WorkItemWeight component', () => {
|
|||
|
||||
it('does not call a mutation to update the weight when the input value is the same', () => {
|
||||
const mutationSpy = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
||||
createComponent({ isEditing: true, mutationHandler: mutationSpy });
|
||||
createComponent({ isEditing: true, mutationHandler: mutationSpy, canUpdate: true });
|
||||
|
||||
findInput().trigger('blur');
|
||||
|
||||
|
@ -170,6 +175,7 @@ describe('WorkItemWeight component', () => {
|
|||
createComponent({
|
||||
isEditing: true,
|
||||
mutationHandler: jest.fn().mockResolvedValue(response),
|
||||
canUpdate: true,
|
||||
});
|
||||
|
||||
findInput().trigger('blur');
|
||||
|
@ -182,6 +188,7 @@ describe('WorkItemWeight component', () => {
|
|||
createComponent({
|
||||
isEditing: true,
|
||||
mutationHandler: jest.fn().mockRejectedValue(new Error()),
|
||||
canUpdate: true,
|
||||
});
|
||||
|
||||
findInput().trigger('blur');
|
||||
|
@ -192,7 +199,7 @@ describe('WorkItemWeight component', () => {
|
|||
|
||||
it('tracks updating the weight', () => {
|
||||
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
createComponent();
|
||||
createComponent({ canUpdate: true });
|
||||
|
||||
findInput().trigger('blur');
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ RSpec.describe Ci::RunnersHelper do
|
|||
|
||||
it 'returns group data for top level group' do
|
||||
result = {
|
||||
update_path: "/api/v4/groups/#{parent.id}",
|
||||
group_id: parent.id,
|
||||
shared_runners_setting: Namespace::SR_ENABLED,
|
||||
parent_shared_runners_setting: nil
|
||||
}.merge(runner_constants)
|
||||
|
@ -119,7 +119,7 @@ RSpec.describe Ci::RunnersHelper do
|
|||
|
||||
it 'returns group data for child group' do
|
||||
result = {
|
||||
update_path: "/api/v4/groups/#{group.id}",
|
||||
group_id: group.id,
|
||||
shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
|
||||
parent_shared_runners_setting: Namespace::SR_ENABLED
|
||||
}.merge(runner_constants)
|
||||
|
|
|
@ -4,14 +4,14 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::BackgroundMigration::SetLegacyOpenSourceLicenseAvailableForNonPublicProjects,
|
||||
:migration,
|
||||
schema: 20220520040416 do
|
||||
schema: 20220722110026 do
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:projects_table) { table(:projects) }
|
||||
let(:project_settings_table) { table(:project_settings) }
|
||||
|
||||
subject(:perform_migration) do
|
||||
described_class.new(start_id: 1,
|
||||
end_id: 30,
|
||||
described_class.new(start_id: projects_table.minimum(:id),
|
||||
end_id: projects_table.maximum(:id),
|
||||
batch_table: :projects,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 2,
|
||||
|
@ -20,35 +20,34 @@ RSpec.describe Gitlab::BackgroundMigration::SetLegacyOpenSourceLicenseAvailableF
|
|||
.perform
|
||||
end
|
||||
|
||||
let(:queries) { ActiveRecord::QueryRecorder.new { perform_migration } }
|
||||
|
||||
before do
|
||||
namespaces_table.create!(id: 1, name: 'namespace', path: 'namespace-path-1')
|
||||
namespaces_table.create!(id: 2, name: 'namespace', path: 'namespace-path-2', type: 'Project')
|
||||
namespaces_table.create!(id: 3, name: 'namespace', path: 'namespace-path-3', type: 'Project')
|
||||
namespaces_table.create!(id: 4, name: 'namespace', path: 'namespace-path-4', type: 'Project')
|
||||
|
||||
projects_table
|
||||
.create!(id: 11, name: 'proj-1', path: 'path-1', namespace_id: 1, project_namespace_id: 2, visibility_level: 0)
|
||||
projects_table
|
||||
.create!(id: 12, name: 'proj-2', path: 'path-2', namespace_id: 1, project_namespace_id: 3, visibility_level: 10)
|
||||
projects_table
|
||||
.create!(id: 13, name: 'proj-3', path: 'path-3', namespace_id: 1, project_namespace_id: 4, visibility_level: 20)
|
||||
|
||||
project_settings_table.create!(project_id: 11, legacy_open_source_license_available: true)
|
||||
project_settings_table.create!(project_id: 12, legacy_open_source_license_available: true)
|
||||
project_settings_table.create!(project_id: 13, legacy_open_source_license_available: true)
|
||||
end
|
||||
|
||||
it 'sets `legacy_open_source_license_available` attribute to false for non-public projects', :aggregate_failures do
|
||||
expect(queries.count).to eq(3)
|
||||
private_project = create_legacy_license_project('private-project', visibility_level: 0)
|
||||
internal_project = create_legacy_license_project('internal-project', visibility_level: 10)
|
||||
public_project = create_legacy_license_project('public-project', visibility_level: 20)
|
||||
|
||||
expect(migrated_attribute(11)).to be_falsey
|
||||
expect(migrated_attribute(12)).to be_falsey
|
||||
expect(migrated_attribute(13)).to be_truthy
|
||||
queries = ActiveRecord::QueryRecorder.new { perform_migration }
|
||||
|
||||
expect(queries.count).to eq(5)
|
||||
|
||||
expect(migrated_attribute(private_project)).to be_falsey
|
||||
expect(migrated_attribute(internal_project)).to be_falsey
|
||||
expect(migrated_attribute(public_project)).to be_truthy
|
||||
end
|
||||
|
||||
def migrated_attribute(project_id)
|
||||
project_settings_table.find(project_id).legacy_open_source_license_available
|
||||
def create_legacy_license_project(path, visibility_level:)
|
||||
namespace = namespaces_table.create!(name: "namespace-#{path}", path: "namespace-#{path}")
|
||||
project_namespace = namespaces_table.create!(name: "project-namespace-#{path}", path: path, type: 'Project')
|
||||
project = projects_table.create!(name: path,
|
||||
path: path,
|
||||
namespace_id: namespace.id,
|
||||
project_namespace_id: project_namespace.id,
|
||||
visibility_level: visibility_level)
|
||||
project_settings_table.create!(project_id: project.id, legacy_open_source_license_available: true)
|
||||
|
||||
project
|
||||
end
|
||||
|
||||
def migrated_attribute(project)
|
||||
project_settings_table.find(project.id).legacy_open_source_license_available
|
||||
end
|
||||
end
|
||||
|
|
|
@ -355,6 +355,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
|
|||
let(:reason) { 'rss out of range reason description' }
|
||||
let(:queue) { 'default' }
|
||||
let(:running_jobs) { [{ jid: jid, worker_class: 'DummyWorker' }] }
|
||||
let(:metrics) { memory_killer.instance_variable_get(:@metrics) }
|
||||
let(:worker) do
|
||||
Class.new do
|
||||
def self.name
|
||||
|
@ -390,6 +391,9 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
|
|||
reason: reason,
|
||||
running_jobs: running_jobs)
|
||||
|
||||
expect(metrics[:sidekiq_memory_killer_running_jobs]).to receive(:increment)
|
||||
.with({ worker_class: "DummyWorker", deadline_exceeded: true })
|
||||
|
||||
Gitlab::SidekiqDaemon::Monitor.instance.within_job(DummyWorker, jid, queue) do
|
||||
subject
|
||||
end
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects do
|
||||
context 'on gitlab.com' do
|
||||
RSpec.describe RescheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects do
|
||||
context 'when on gitlab.com' do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
before do
|
||||
|
@ -21,6 +21,7 @@ RSpec.describe ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects d
|
|||
column_name: :id,
|
||||
interval: described_class::INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
max_batch_size: described_class::MAX_BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
)
|
||||
|
@ -37,7 +38,7 @@ RSpec.describe ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects d
|
|||
end
|
||||
end
|
||||
|
||||
context 'on self-managed instance' do
|
||||
context 'when on self-managed instance' do
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
before do
|
|
@ -98,7 +98,7 @@ RSpec.describe API::Ci::PipelineSchedules do
|
|||
end
|
||||
|
||||
matcher :return_pipeline_schedule_sucessfully do
|
||||
match_unless_raises do |reponse|
|
||||
match_unless_raises do |response|
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('pipeline_schedule')
|
||||
end
|
||||
|
@ -207,6 +207,110 @@ RSpec.describe API::Ci::PipelineSchedules do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id/pipelines' do
|
||||
let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) }
|
||||
|
||||
before do
|
||||
create_list(:ci_pipeline, 2, project: project, pipeline_schedule: pipeline_schedule, source: :schedule)
|
||||
end
|
||||
|
||||
let(:url) { "/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/pipelines" }
|
||||
|
||||
matcher :return_pipeline_schedule_pipelines_successfully do
|
||||
match_unless_raises do |reponse|
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(response).to match_response_schema('public_api/v4/pipelines')
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'request with project permissions' do
|
||||
context 'authenticated user with project permissions' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns the details of pipelines triggered from the pipeline schedule' do
|
||||
get api(url, user)
|
||||
|
||||
expect(response).to return_pipeline_schedule_pipelines_successfully
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'request with schedule ownership' do
|
||||
context 'authenticated user with pipeline schedule ownership' do
|
||||
it 'returns the details of pipelines triggered from the pipeline schedule' do
|
||||
get api(url, developer)
|
||||
|
||||
expect(response).to return_pipeline_schedule_pipelines_successfully
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'request with unauthenticated user' do
|
||||
context 'with unauthenticated user' do
|
||||
it 'does not return the details of pipelines triggered from the pipeline schedule' do
|
||||
get api(url)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'request with non-existing pipeline_schedule' do
|
||||
it "responds with 404 Not Found if requesting for a non-existing pipeline schedule's pipelines" do
|
||||
get api("/projects/#{project.id}/pipeline_schedules/#{non_existing_record_id}/pipelines", developer)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private project' do
|
||||
it_behaves_like 'request with schedule ownership'
|
||||
it_behaves_like 'request with project permissions'
|
||||
it_behaves_like 'request with unauthenticated user'
|
||||
it_behaves_like 'request with non-existing pipeline_schedule'
|
||||
|
||||
context 'authenticated user with no project permissions' do
|
||||
it 'does not return the details of pipelines triggered from the pipeline schedule' do
|
||||
get api(url, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticated user with insufficient project permissions' do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
end
|
||||
|
||||
it 'does not return the details of pipelines triggered from the pipeline schedule' do
|
||||
get api(url, user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with public project' do
|
||||
let_it_be(:project) { create(:project, :repository, :public, public_builds: false) }
|
||||
|
||||
it_behaves_like 'request with schedule ownership'
|
||||
it_behaves_like 'request with project permissions'
|
||||
it_behaves_like 'request with unauthenticated user'
|
||||
it_behaves_like 'request with non-existing pipeline_schedule'
|
||||
|
||||
context 'authenticated user with no project permissions' do
|
||||
it 'returns the details of pipelines triggered from the pipeline schedule' do
|
||||
get api(url, user)
|
||||
|
||||
expect(response).to return_pipeline_schedule_pipelines_successfully
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/:id/pipeline_schedules' do
|
||||
let(:params) { attributes_for(:ci_pipeline_schedule) }
|
||||
|
||||
|
|
Loading…
Reference in New Issue