Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-01-03 15:10:11 +00:00
parent b9ae930d02
commit 4eef6c2c97
36 changed files with 506 additions and 136 deletions

View File

@ -28,6 +28,9 @@ GITALY_SERVER_VERSION @project_278964_bot6 @gitlab-org/maintainers/rails-backend
/doc/.vale/ @marcel.amirault @eread @aqualls @gitlab-org/tw-leadership
/lib/tasks/gitlab/tw/codeowners.rake @aqualls @gitlab-org/tw-leadership
^[Source code editing]
.solargraph.yml.example @igor.drozdov
^[Backend]
*.rb @gitlab-org/maintainers/rails-backend
*.rake @gitlab-org/maintainers/rails-backend

View File

@ -25,6 +25,9 @@ export default {
setCookie(PREFERRED_LANGUAGE_COOKIE_KEY, code);
window.location.reload();
},
itemTestSelector(locale) {
return `language_switcher_lang_${locale}`;
},
},
};
</script>
@ -41,7 +44,10 @@ export default {
@select="onLanguageSelected"
>
<template #list-item="{ item: locale }">
<span :data-testid="`language_switcher_lang_${locale.value}`">
<span
:data-testid="itemTestSelector(locale.value)"
:data-qa-selector="itemTestSelector(locale.value)"
>
{{ locale.text }}
</span>
</template>

View File

@ -5,31 +5,7 @@ export default {
components: {
ListboxInput,
},
inject: {
label: {
from: 'label',
default: '',
},
name: {
from: 'name',
},
emails: {
from: 'emails',
default: () => [],
},
emptyValueText: {
from: 'emptyValueText',
required: true,
},
value: {
from: 'value',
default: '',
},
disabled: {
from: 'disabled',
default: false,
},
},
inject: ['label', 'name', 'emails', 'emptyValueText', 'value', 'disabled'],
data() {
return {
selected: this.value,

View File

@ -10,7 +10,7 @@ const initNotificationEmailListboxInputs = () => {
const els = [...document.querySelectorAll('.js-notification-email-listbox-input')];
els.forEach((el, index) => {
const { label, name, emptyValueText, value } = el.dataset;
const { label, name, emptyValueText, value = '' } = el.dataset;
return new Vue({
el,

View File

@ -30,7 +30,7 @@ export default {
computed: {
pypiPipCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `pip install ${this.packageEntity.name} --extra-index-url ${this.packageEntity.pypiUrl}`;
return `pip install ${this.packageEntity.name} --index-url ${this.packageEntity.pypiUrl}`;
},
pypiSetupCommand() {
return `[gitlab]

View File

@ -12,7 +12,7 @@ import {
import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
@ -191,6 +191,9 @@ export default {
this.parentWorkItemType,
);
},
showConfidentialityTooltip() {
return this.isCreateForm && this.parentConfidential;
},
addOrCreateMethod() {
return this.isCreateForm ? this.createChild : this.addChild;
},
@ -207,7 +210,10 @@ export default {
return this.parentMilestone?.id;
},
isSubmitButtonDisabled() {
return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0;
if (this.isCreateForm) {
return this.search.length === 0;
}
return this.workItemsToAdd.length === 0 || !this.areWorkItemsToAddValid;
},
isLoading() {
return this.$apollo.queries.availableWorkItems.loading;
@ -215,6 +221,32 @@ export default {
addInputPlaceholder() {
return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
},
tokenSelectorContainerClass() {
return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
},
invalidWorkItemsToAdd() {
return this.parentConfidential
? this.workItemsToAdd.filter((workItem) => !workItem.confidential)
: [];
},
areWorkItemsToAddValid() {
return this.invalidWorkItemsToAdd.length === 0;
},
showWorkItemsToAddInvalidMessage() {
return !this.isCreateForm && !this.areWorkItemsToAddValid;
},
workItemsToAddInvalidMessage() {
return sprintf(
s__(
'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
),
{
invalidWorkItemsList: this.invalidWorkItemsToAdd.map(({ title }) => title).join(', '),
childWorkItemType: this.childrenTypeName,
parentWorkItemType: this.parentWorkItemType,
},
);
},
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
@ -334,6 +366,7 @@ export default {
/>
</gl-form-group>
<gl-form-checkbox
v-if="isCreateForm"
ref="confidentialityCheckbox"
v-model="confidential"
name="isConfidential"
@ -342,35 +375,44 @@ export default {
>{{ confidentialityCheckboxLabel }}</gl-form-checkbox
>
<gl-tooltip
v-if="parentConfidential"
v-if="showConfidentialityTooltip"
:target="getConfidentialityTooltipTarget"
triggers="hover"
>{{ confidentialityCheckboxTooltip }}</gl-tooltip
>
<gl-token-selector
v-if="!isCreateForm"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
:placeholder="addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
class="gl-mb-4"
data-testid="work-item-token-select-input"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
<template #token-content="{ token }">
{{ token.title }}
</template>
<template #dropdown-item-content="{ dropdownItem }">
<div class="gl-display-flex">
<div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
<div class="gl-text-truncate">{{ dropdownItem.title }}</div>
</div>
</template>
</gl-token-selector>
<div class="gl-mb-4">
<gl-token-selector
v-if="!isCreateForm"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
:placeholder="addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
:container-class="tokenSelectorContainerClass"
data-testid="work-item-token-select-input"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
<template #token-content="{ token }">
{{ token.title }}
</template>
<template #dropdown-item-content="{ dropdownItem }">
<div class="gl-display-flex">
<div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
<div class="gl-text-truncate">{{ dropdownItem.title }}</div>
</div>
</template>
</gl-token-selector>
<div
v-if="showWorkItemsToAddInvalidMessage"
class="gl-text-red-500"
data-testid="work-items-invalid"
>
{{ workItemsToAddInvalidMessage }}
</div>
</div>
<gl-button
category="primary"
variant="confirm"

View File

@ -11,6 +11,7 @@ query projectWorkItems(
id
title
state
confidential
}
}
}

View File

@ -11,13 +11,21 @@ module Mutations
null: true,
description: 'Job after the mutation.'
argument :variables, [::Types::Ci::VariableInputType],
required: false,
default_value: [],
replace_null_with_default: true,
description: 'Variables to use when playing a manual job.'
authorize :update_build
def resolve(id:)
def resolve(id:, variables:)
job = authorized_find!(id: id)
project = job.project
variables = variables.map(&:to_h)
::Ci::PlayBuildService.new(project, current_user).execute(job, variables)
::Ci::PlayBuildService.new(project, current_user).execute(job)
{
job: job,
errors: errors_on_object(job)

View File

@ -3704,6 +3704,7 @@ Input type: `JobPlayInput`
| ---- | ---- | ----------- |
| <a id="mutationjobplayclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationjobplayid"></a>`id` | [`CiBuildID!`](#cibuildid) | ID of the job to mutate. |
| <a id="mutationjobplayvariables"></a>`variables` | [`[CiVariableInput!]`](#civariableinput) | Variables to use when playing a manual job. |
#### Fields

View File

@ -143,30 +143,35 @@ For example: `gitlab-org/dast@1.0`.
### The component path
A component path must contain at least the metadata YAML and optionally a related `README.md` documentation file.
A component path must contain at least the component YAML and optionally a related `README.md` documentation file.
The component path can be:
- A path to a project: `gitlab-org/dast`. In this case the 2 files are defined in the root directory of the repository.
- A path to a project subdirectory: `gitlab-org/dast/api-scan`. In this case the 2 files are defined in the `api-scan` directory.
- A path to a local directory: `/path/to/component`. This path must contain the metadata YAML that defines the component.
- A path to a project: `gitlab-org/dast`. The default component is processed.
- A path to an explicit component: `gitlab-org/dast/api-scan`. In this case the explicit `api-scan` component is processed.
- A path to a local directory: `/path/to/component`. This path must contain the component YAML that defines the component.
The path must start with `/` to indicate a full path in the repository.
The metadata YAML file follows the filename convention `gitlab-<component-type>.yml` where component type is one of:
The component YAML file follows the filename convention `<type>.yml` where component type is one of:
| Component type | Context |
| -------------- | ------- |
| `template` | For components used under `include:` keyword |
| `step` | For components used under `steps:` keyword |
| `workflow` | For components used under `trigger:` keyword |
Based on the context where the component is used we fetch the correct YAML file.
For example, if we are including a component `gitlab-org/dast@1.0` we expect a YAML file named `gitlab-template.yml` in the
top level directory of `gitlab-org/dast` repository.
For example:
A `gitlab-<component-type>.yml` file:
- if we are including a component `gitlab-org/dast@1.0` we expect a YAML file named `template.yml` in the
root directory of `gitlab-org/dast` repository.
- if we are including a component `gitlab-org/dast/api-scan@1.0` we expect a YAML file named `template.yml` inside a
directory `api-scan` of `gitlab-org/dast` repository.
- if we are using a step component `gitlab-org/dast/api-scan@1.0` we expect a YAML file named `step.yml` inside a
directory `api-scan` of `gitlab-org/dast` repository.
- Must have a **name** to be referenced to and **description** for extra details.
A component YAML file:
- Must have a **name** to be referenced to.
- Must specify its **type** in the filename, which defines how it can be used (raw configuration to be `include`d, child pipeline workflow, job step).
- Must define its **content** based on the type.
- Must specify **input parameters** that it accepts. Components should depend on input parameters for dynamic values and not environment variables.
@ -187,8 +192,8 @@ spec:
content: { ... }
```
Components that are released in the catalog must have a `README.md` file in the same directory as the
metadata YAML file. The `README.md` represents the documentation for the specific component, hence it's recommended
Components that are released in the catalog must have a `README.md` file at the root directory of the repository.
The `README.md` represents the documentation for the specific component, hence it's recommended
even when not releasing versions in the catalog.
### The component version
@ -230,30 +235,28 @@ The following directory structure would support 1 component per project:
```plaintext
.
├── gitlab-<type>.yml
├── template.yml
├── README.md
└── .gitlab-ci.yml
```
The `.gitlab-ci.yml` is recommended for the project to ensure changes are verified accordingly.
The component is now identified by the path `myorg/rails-rspec`. In other words, this means that
the `gitlab-<type>.yml` and `README.md` are located in the root directory of the repository.
The component is now identified by the path `myorg/rails-rspec` and we expect a `template.yml` file
and `README.md` located in the root directory of the repository.
The following directory structure would support multiple components per project:
```plaintext
.
├── .gitlab-ci.yml
├── README.md
├── unit/
│ ├── gitlab-workflow.yml
│ └── README.md
│ └── template.yml
├── integration/
│ ├── gitlab-workflow.yml
│ └── README.md
│ └── template.yml
└── feature/
├── gitlab-workflow.yml
└── README.md
└── template.yml
```
In this example we are defining multiple test profiles that are executed with RSpec.
@ -266,18 +269,20 @@ This directory structure could also support both strategies:
```plaintext
.
├── gitlab-template.yml # myorg/rails-rspec
├── template.yml # myorg/rails-rspec
├── README.md
├── LICENSE
├── .gitlab-ci.yml
├── unit/
│ ├── gitlab-workflow.yml # myorg/rails-rspec/unit
│ └── README.md
│ └── template.yml # myorg/rails-rspec/unit
├── integration/
│ ├── gitlab-workflow.yml # myorg/rails-rspec/integration
│ └── README.md
└── feature/
├── gitlab-workflow.yml # myorg/rails-rspec/feature
└── README.md
│ └── template.yml # myorg/rails-rspec/integration
├── feature/
│ └── template.yml # myorg/rails-rspec/feature
└── report/
├── step.yml # myorg/rails-rspec/report
├── Dockerfile
└── ... other files
```
With the above structure we could have a top-level component that can be used as the
@ -285,7 +290,7 @@ default component. For example, `myorg/rails-rspec` could run all the test profi
However, more specific test profiles could be used separately (for example `myorg/rails-rspec/integration`).
NOTE:
Any nesting more than 1 level is initially not permitted.
Nesting of components is not permitted.
This limitation encourages cohesion at project level and keeps complexity low.
## Input parameters `spec:inputs:` parameters

View File

@ -254,6 +254,7 @@ module API
mount ::API::NugetProjectPackages
mount ::API::PackageFiles
mount ::API::Pages
mount ::API::PagesDomains
mount ::API::PersonalAccessTokens::SelfInformation
mount ::API::PersonalAccessTokens
mount ::API::ProjectClusters
@ -319,7 +320,6 @@ module API
mount ::API::Labels
mount ::API::Notes
mount ::API::NotificationSettings
mount ::API::PagesDomains
mount ::API::ProjectEvents
mount ::API::ProjectMilestones
mount ::API::ProtectedTags

View File

@ -54,7 +54,7 @@ module API
end
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
@ -63,6 +63,8 @@ module API
desc 'Get all pages domains' do
success Entities::PagesDomain
tags %w[pages_domains]
is_array true
end
params do
use :pagination

View File

@ -15,10 +15,12 @@
module Gitlab
module Database
module GitlabSchema
UnknownSchemaError = Class.new(StandardError)
DICTIONARY_PATH = 'db/docs/'
def self.table_schemas(tables)
tables.map { |table| table_schema(table) }.to_set
def self.table_schemas(tables, undefined: true)
tables.map { |table| table_schema(table, undefined: undefined) }.to_set
end
def self.table_schema(name, undefined: true)
@ -82,6 +84,13 @@ module Gitlab
@views_and_tables_to_schema ||= self.tables_to_schema.merge(self.views_to_schema)
end
def self.table_schema!(name)
self.table_schema(name, undefined: false) || raise(
UnknownSchemaError,
"Could not find gitlab schema for table #{name}: Any new tables must be added to the database dictionary"
)
end
def self.deleted_views_and_tables_to_schema
@deleted_views_and_tables_to_schema ||= self.deleted_tables_to_schema.merge(self.deleted_views_to_schema)
end

View File

@ -22,7 +22,7 @@ module Gitlab
{
column: config.fetch('column'),
on_delete: config.fetch('on_delete').to_sym,
gitlab_schema: GitlabSchema.table_schema(child_table_name)
gitlab_schema: GitlabSchema.table_schema!(child_table_name)
}
)
end

View File

@ -22,13 +22,16 @@ module Gitlab
return unless allowed_schemas
invalid_schemas = table_schemas - allowed_schemas
if invalid_schemas.any?
message = "The query tried to access #{tables} (of #{table_schemas.to_a}) "
message += "which is outside of allowed schemas (#{allowed_schemas}) "
message += "for the current connection '#{Gitlab::Database.db_config_name(parsed.connection)}'"
raise CrossSchemaAccessError, message
end
return if invalid_schemas.empty?
schema_list = table_schemas.sort.join(',')
message = "The query tried to access #{tables} (of #{schema_list}) "
message += "which is outside of allowed schemas (#{allowed_schemas}) "
message += "for the current connection '#{Gitlab::Database.db_config_name(parsed.connection)}'"
raise CrossSchemaAccessError, message
end
end
end

View File

@ -87,15 +87,15 @@ module Gitlab
return if tables == ['schema_migrations']
context[:modified_tables_by_db][database].merge(tables)
all_tables = context[:modified_tables_by_db].values.map(&:to_a).flatten
all_tables = context[:modified_tables_by_db].values.flat_map(&:to_a)
schemas = ::Gitlab::Database::GitlabSchema.table_schemas(all_tables)
schemas += ApplicationRecord.gitlab_transactions_stack
if schemas.many?
message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
"a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \
"Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."
"a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \
"Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."
if schemas.any? { |s| s.to_s.start_with?("undefined") }
message += " The gitlab_schema was undefined for one or more of the tables in this transaction. Any new tables must be added to lib/gitlab/database/gitlab_schemas.yml ."

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Sidebars
module YourWork
module Menus
class ActivityMenu < ::Sidebars::Menu
override :link
def link
activity_dashboard_path
end
override :title
def title
_('Activity')
end
override :sprite_icon
def sprite_icon
'history'
end
override :render?
def render?
!!context.current_user
end
override :active_routes
def active_routes
{ path: 'dashboard#activity' }
end
end
end
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Sidebars
module YourWork
module Menus
class MilestonesMenu < ::Sidebars::Menu
override :link
def link
dashboard_milestones_path
end
override :title
def title
_('Milestones')
end
override :sprite_icon
def sprite_icon
'clock'
end
override :render?
def render?
!!context.current_user
end
override :active_routes
def active_routes
{ controller: 'dashboard/milestones' }
end
end
end
end
end

View File

@ -22,6 +22,8 @@ module Sidebars
def add_menus
add_menu(Sidebars::YourWork::Menus::ProjectsMenu.new(context))
add_menu(Sidebars::YourWork::Menus::MilestonesMenu.new(context))
add_menu(Sidebars::YourWork::Menus::ActivityMenu.new(context))
end
end
end

View File

@ -47093,6 +47093,9 @@ msgstr ""
msgid "WorkItem|%{count} more assignees"
msgstr ""
msgid "WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again."
msgstr ""
msgid "WorkItem|%{workItemType} deleted"
msgstr ""

View File

@ -253,7 +253,7 @@ end
# Show only cross-schema foreign keys
if $options[:cross_schema]
all_foreign_keys.select! do |definition|
Gitlab::Database::GitlabSchema.table_schema(definition.from_table) != Gitlab::Database::GitlabSchema.table_schema(definition.to_table)
Gitlab::Database::GitlabSchema.table_schema!(definition.from_table) != Gitlab::Database::GitlabSchema.table_schema!(definition.to_table)
end
end

View File

@ -299,21 +299,21 @@ HELM_CMD=$(cat << EOF
--set global.appConfig.sentry.dsn="${REVIEW_APPS_SENTRY_DSN}" \
--set global.appConfig.sentry.environment="review" \
--set gitlab.migrations.image.repository="${gitlab_toolbox_image_repository}" \
--set gitlab.migrations.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.migrations.image.tag="${CI_COMMIT_SHA}" \
--set gitlab.gitaly.image.repository="${gitlab_gitaly_image_repository}" \
--set gitlab.gitaly.image.tag="${gitaly_image_tag}" \
--set gitlab.gitlab-shell.image.repository="${gitlab_shell_image_repository}" \
--set gitlab.gitlab-shell.image.tag="v${GITLAB_SHELL_VERSION}" \
--set gitlab.sidekiq.annotations.commit="${CI_COMMIT_SHORT_SHA}" \
--set gitlab.sidekiq.image.repository="${gitlab_sidekiq_image_repository}" \
--set gitlab.sidekiq.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.sidekiq.image.tag="${CI_COMMIT_SHA}" \
--set gitlab.webservice.annotations.commit="${CI_COMMIT_SHORT_SHA}" \
--set gitlab.webservice.image.repository="${gitlab_webservice_image_repository}" \
--set gitlab.webservice.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.webservice.image.tag="${CI_COMMIT_SHA}" \
--set gitlab.webservice.workhorse.image="${gitlab_workhorse_image_repository}" \
--set gitlab.webservice.workhorse.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.webservice.workhorse.tag="${CI_COMMIT_SHA}" \
--set gitlab.toolbox.image.repository="${gitlab_toolbox_image_repository}" \
--set gitlab.toolbox.image.tag="${CI_COMMIT_REF_SLUG}"
--set gitlab.toolbox.image.tag="${CI_COMMIT_SHA}"
EOF
)

View File

@ -184,6 +184,20 @@ module Trigger
true
end
def gitlab_ref_slug
if ENV['CI_COMMIT_TAG']
ENV['CI_COMMIT_REF_NAME']
else
ENV['CI_COMMIT_SHA']
end
end
def base_variables
super.merge(
'GITLAB_REF_SLUG' => gitlab_ref_slug
)
end
def extra_variables
{
"TRIGGER_BRANCH" => ref,

View File

@ -9,6 +9,8 @@ RSpec.describe 'Dashboard > Activity', feature_category: :users do
sign_in(user)
end
it_behaves_like 'a dashboard page with sidebar', :activity_dashboard_path, :activity
context 'tabs' do
it 'shows Your Projects' do
visit activity_dashboard_path

View File

@ -26,6 +26,8 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
visit dashboard_milestones_path
end
it_behaves_like 'a dashboard page with sidebar', :dashboard_milestones_path, :milestones
it 'sees milestones' do
expect(page).to have_current_path dashboard_milestones_path, ignore_query: true
expect(page).to have_content(milestone.title)

View File

@ -114,7 +114,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
aria-live="polite"
class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
data-clipboard-handle-tooltip="false"
data-clipboard-text="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
data-clipboard-text="pip install @gitlab-org/package-15 --index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
id="clipboard-button-6"
title="Copy Pip command"
type="button"

View File

@ -16,7 +16,7 @@ const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_PYPI };
describe('PypiInstallation', () => {
let wrapper;
const pipCommandStr = `pip install @gitlab-org/package-15 --extra-index-url ${packageEntity.pypiUrl}`;
const pipCommandStr = `pip install @gitlab-org/package-15 --index-url ${packageEntity.pypiUrl}`;
const pypiSetupStr = `[gitlab]
repository = ${packageEntity.pypiSetupUrl}
username = __token__

View File

@ -1,7 +1,7 @@
import Vue from 'vue';
import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip, GlTokenSelector } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { sprintf } from '~/locale';
import { sprintf, s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -170,6 +170,16 @@ describe('WorkItemLinksForm', () => {
});
describe('adding an existing work item', () => {
const selectAvailableWorkItemTokens = async () => {
findTokenSelector().vm.$emit(
'input',
availableWorkItemsResponse.data.workspace.workItems.nodes,
);
findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
await waitForPromises();
};
beforeEach(async () => {
await createComponent({ formType: FORM_TYPES.add });
});
@ -179,6 +189,7 @@ describe('WorkItemLinksForm', () => {
expect(findTokenSelector().exists()).toBe(true);
expect(findAddChildButton().text()).toBe('Add task');
expect(findInput().exists()).toBe(false);
expect(findConfidentialCheckbox().exists()).toBe(false);
});
it('searches for available work items as prop when typing in input', async () => {
@ -190,13 +201,7 @@ describe('WorkItemLinksForm', () => {
});
it('selects and adds children', async () => {
findTokenSelector().vm.$emit(
'input',
availableWorkItemsResponse.data.workspace.workItems.nodes,
);
findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
await waitForPromises();
await selectAvailableWorkItemTokens();
expect(findAddChildButton().text()).toBe('Add tasks');
findForm().vm.$emit('submit', {
@ -205,6 +210,31 @@ describe('WorkItemLinksForm', () => {
await waitForPromises();
expect(updateMutationResolver).toHaveBeenCalled();
});
it('shows validation error when non-confidential child items are being added to confidential parent', async () => {
await createComponent({ formType: FORM_TYPES.add, parentConfidential: true });
await selectAvailableWorkItemTokens();
const validationEl = wrapper.findByTestId('work-items-invalid');
expect(validationEl.exists()).toBe(true);
expect(validationEl.text().trim()).toBe(
sprintf(
s__(
'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
),
{
// Only non-confidential work items are shown in the error message
invalidWorkItemsList: availableWorkItemsResponse.data.workspace.workItems.nodes
.filter((wi) => !wi.confidential)
.map((wi) => wi.title)
.join(', '),
childWorkItemType: 'Task',
parentWorkItemType: 'Issue',
},
),
);
});
});
describe('associate iteration with task', () => {

View File

@ -1177,6 +1177,7 @@ export const availableWorkItemsResponse = {
title: 'Task 1',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
confidential: false,
__typename: 'WorkItem',
},
{
@ -1184,6 +1185,15 @@ export const availableWorkItemsResponse = {
title: 'Task 2',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/460',
title: 'Task 3',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
confidential: true,
__typename: 'WorkItem',
},
],

View File

@ -17,6 +17,25 @@ RSpec.shared_examples 'validate schema data' do |tables_and_views|
end
RSpec.describe Gitlab::Database::GitlabSchema do
shared_examples 'maps table name to table schema' do
using RSpec::Parameterized::TableSyntax
where(:name, :classification) do
'ci_builds' | :gitlab_ci
'my_schema.ci_builds' | :gitlab_ci
'information_schema.columns' | :gitlab_internal
'audit_events_part_5fc467ac26' | :gitlab_main
'_test_gitlab_main_table' | :gitlab_main
'_test_gitlab_ci_table' | :gitlab_ci
'_test_my_table' | :gitlab_shared
'pg_attribute' | :gitlab_internal
end
with_them do
it { is_expected.to eq(classification) }
end
end
describe '.deleted_views_and_tables_to_schema' do
include_examples 'validate schema data', described_class.deleted_views_and_tables_to_schema
end
@ -97,25 +116,85 @@ RSpec.describe Gitlab::Database::GitlabSchema do
end
end
describe '.table_schema' do
using RSpec::Parameterized::TableSyntax
describe '.table_schemas' do
let(:tables) { %w[users projects ci_builds] }
where(:name, :classification) do
'ci_builds' | :gitlab_ci
'my_schema.ci_builds' | :gitlab_ci
'information_schema.columns' | :gitlab_internal
'audit_events_part_5fc467ac26' | :gitlab_main
'_test_gitlab_main_table' | :gitlab_main
'_test_gitlab_ci_table' | :gitlab_ci
'_test_my_table' | :gitlab_shared
'pg_attribute' | :gitlab_internal
'my_other_table' | :undefined_my_other_table
subject { described_class.table_schemas(tables) }
it 'returns the matched schemas' do
expect(subject).to match_array %i[gitlab_main gitlab_ci].to_set
end
with_them do
subject { described_class.table_schema(name) }
context 'when one of the tables does not have a matching table schema' do
let(:tables) { %w[users projects unknown ci_builds] }
it { is_expected.to eq(classification) }
context 'and undefined parameter is false' do
subject { described_class.table_schemas(tables, undefined: false) }
it 'includes a nil value' do
is_expected.to match_array [:gitlab_main, nil, :gitlab_ci].to_set
end
end
context 'and undefined parameter is true' do
subject { described_class.table_schemas(tables, undefined: true) }
it 'includes "undefined_<table_name>"' do
is_expected.to match_array [:gitlab_main, :undefined_unknown, :gitlab_ci].to_set
end
end
context 'and undefined parameter is not specified' do
it 'includes a nil value' do
is_expected.to match_array [:gitlab_main, :undefined_unknown, :gitlab_ci].to_set
end
end
end
end
describe '.table_schema' do
subject { described_class.table_schema(name) }
it_behaves_like 'maps table name to table schema'
context 'when mapping fails' do
let(:name) { 'unknown_table' }
context "and parameter 'undefined' is set to true" do
subject { described_class.table_schema(name, undefined: true) }
it { is_expected.to eq(:undefined_unknown_table) }
end
context "and parameter 'undefined' is set to false" do
subject { described_class.table_schema(name, undefined: false) }
it { is_expected.to be_nil }
end
context "and parameter 'undefined' is not set" do
subject { described_class.table_schema(name) }
it { is_expected.to eq(:undefined_unknown_table) }
end
end
end
describe '.table_schema!' do
subject { described_class.table_schema!(name) }
it_behaves_like 'maps table name to table schema'
context 'when mapping fails' do
let(:name) { 'non_existing_table' }
it "raises error" do
expect { subject }.to raise_error(
Gitlab::Database::GitlabSchema::UnknownSchemaError,
"Could not find gitlab schema for table #{name}: " \
"Any new tables must be added to the database dictionary"
)
end
end
end
end

View File

@ -112,4 +112,31 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
end
end
end
describe '.build_definition' do
context 'when child table schema is not defined' do
let(:loose_foreign_keys_yaml) do
{
'ci_unknown_table' => [
{
'table' => 'projects',
'column' => 'project_id',
'on_delete' => 'async_delete'
}
]
}
end
subject { described_class.definitions }
before do
described_class.instance_variable_set(:@definitions, nil)
described_class.instance_variable_set(:@loose_foreign_keys_yaml, loose_foreign_keys_yaml)
end
it 'raises Gitlab::Database::GitlabSchema::UnknownSchemaError error' do
expect { subject }.to raise_error(Gitlab::Database::GitlabSchema::UnknownSchemaError)
end
end
end
end

View File

@ -53,6 +53,14 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
gitlab_schemas: "gitlab_ci",
db_config_name: "ci"
}
},
"for query accessing gitlab_main and unknown schema" => {
model: ApplicationRecord,
sql: "SELECT 1 FROM projects LEFT JOIN not_in_schema ON not_in_schema.project_id=projects.id",
expectations: {
gitlab_schemas: "gitlab_main,undefined_not_in_schema",
db_config_name: "main"
}
}
}
end

View File

@ -51,6 +51,12 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
sql: "SELECT 1 FROM ci_builds",
expect_error: /The query tried to access \["ci_builds"\]/,
setup: -> (_) { skip_if_multiple_databases_not_setup }
},
"for query accessing unknown gitlab_schema" => {
model: ::ApplicationRecord,
sql: "SELECT 1 FROM new_table",
expect_error: /The query tried to access \["new_table"\] \(of undefined_new_table\)/,
setup: -> (_) { skip_if_multiple_databases_not_setup }
}
}
end

View File

@ -8,17 +8,25 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
let_it_be(:job) { create(:ci_build, pipeline: pipeline, name: 'build') }
let_it_be(:job) { create(:ci_build, :playable, pipeline: pipeline, name: 'build') }
let(:mutation) do
variables = {
let(:variables) do
{
id: job.to_global_id.to_s
}
end
let(:mutation) do
graphql_mutation(:job_play, variables,
<<-QL
errors
job {
id
manualVariables {
nodes {
key
}
}
}
QL
)
@ -43,4 +51,29 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['job']['id']).to eq(job_id)
end
context 'when given variables' do
let(:variables) do
{
id: job.to_global_id.to_s,
variables: [
{ key: 'MANUAL_VAR_1', value: 'test var' },
{ key: 'MANUAL_VAR_2', value: 'test var 2' }
]
}
end
it 'provides those variables to the job', :aggregated_errors do
expect_next_instance_of(Ci::PlayBuildService) do |instance|
expect(instance).to receive(:execute).with(an_instance_of(Ci::Build), variables[:variables]).and_call_original
end
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['job']['manualVariables']['nodes'].pluck('key')).to contain_exactly(
'MANUAL_VAR_1', 'MANUAL_VAR_2'
)
end
end
end

View File

@ -6,7 +6,7 @@ require 'rspec-parameterized'
require_relative '../../scripts/trigger-build'
RSpec.describe Trigger do
RSpec.describe Trigger, feature_category: :tooling do
let(:env) do
{
'CI_JOB_URL' => 'ci_job_url',
@ -362,6 +362,28 @@ RSpec.describe Trigger do
end
end
describe "GITLAB_REF_SLUG" do
context 'when CI_COMMIT_TAG is set' do
before do
stub_env('CI_COMMIT_TAG', 'true')
end
it 'sets GITLAB_REF_SLUG to CI_COMMIT_REF_NAME' do
expect(subject.variables['GITLAB_REF_SLUG']).to eq(env['CI_COMMIT_REF_NAME'])
end
end
context 'when CI_COMMIT_TAG is nil' do
before do
stub_env('CI_COMMIT_TAG', nil)
end
it 'sets GITLAB_REF_SLUG to CI_COMMIT_SHA' do
expect(subject.variables['GITLAB_REF_SLUG']).to eq(env['CI_COMMIT_SHA'])
end
end
end
describe "#version_param_value" do
using RSpec::Parameterized::TableSyntax

View File

@ -232,6 +232,14 @@ RSpec.shared_context 'dashboard navbar structure' do
{
nav_item: _("Projects"),
nav_sub_items: []
},
{
nav_item: _("Milestones"),
nav_sub_items: []
},
{
nav_item: _("Activity"),
nav_sub_items: []
}
]
end