Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d9b3085d45
commit
d4a47f4e96
|
|
@ -18,4 +18,4 @@ variables:
|
|||
# Retry failed specs in separate process
|
||||
QA_RETRY_FAILED_SPECS: "true"
|
||||
# helm chart ref used by test-on-cng pipeline
|
||||
GITLAB_HELM_CHART_REF: "38b68934edec88e8f8a816338d815cc2abdc8e03"
|
||||
GITLAB_HELM_CHART_REF: "435313a4c06496fb81a1b00cb6970e9cdc536e41"
|
||||
|
|
|
|||
15
.stylelintrc
15
.stylelintrc
|
|
@ -27,21 +27,6 @@
|
|||
"rules": {
|
||||
"gitlab/no-gl-class": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"app/assets/builds/tailwind.css"
|
||||
],
|
||||
"rules": {
|
||||
"selector-class-pattern": null,
|
||||
"gitlab/no-gl-class": null,
|
||||
"length-zero-no-unit": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"scss/at-rule-no-unknown": null,
|
||||
"tailwind/disallow-max-width-media-query": true
|
||||
},
|
||||
"reportNeedlessDisables": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
56
CHANGELOG.md
56
CHANGELOG.md
|
|
@ -2,6 +2,29 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 17.5.2 (2024-11-12)
|
||||
|
||||
### Fixed (4 changes)
|
||||
|
||||
- [Fix group wiki activity events breaking the user feed](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2c10d817d961bf6ae229fb436126713d0199aece)
|
||||
- [Add param filtering to avoid error while saving project settings](https://gitlab.com/gitlab-org/security/gitlab/-/commit/7e1bf6aa4087c0789ecff48ca716b30d841a3140) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171554)) **GitLab Enterprise Edition**
|
||||
- [Fix new project group templates pagination](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3fed777c0e1f52816206b546f2063043febedd0b) **GitLab Enterprise Edition**
|
||||
- [Update pdf worker file path in pdf viewer](https://gitlab.com/gitlab-org/security/gitlab/-/commit/406b66e9140b4ee4e79edc84e2870e0fbb90d149)
|
||||
|
||||
### Security (7 changes)
|
||||
|
||||
- [Add missing project_id for build_chat_data](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5a4e1bd3443cc786ab7558b1d6fa77962318c173) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4602))
|
||||
- [Use custom adapter for parsing FogBugz XML](https://gitlab.com/gitlab-org/security/gitlab/-/commit/f8c4b8942e6fca667c6a2b975d9fa792b0d559fa) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4592))
|
||||
- [Removed id from authorize buttons and added specs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/7e9ac80271a0c8a7ed73f1cb4a34f053652f07f6) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4573))
|
||||
- [HTML injection in vulnerability Code flow leads to XSS on self hosted instances](https://gitlab.com/gitlab-org/security/gitlab/-/commit/fbff5c445ecc99f438ab56a0c5add0ff5cd1e2aa) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4564))
|
||||
- [Remove is-unsafe-link from product analytics tables to prevent XSS](https://gitlab.com/gitlab-org/security/gitlab/-/commit/605d8bf88e03ec6f447141049952b623eab2200c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4579))
|
||||
- [Details of blocking merge request can be exposed via list](https://gitlab.com/gitlab-org/security/gitlab/-/commit/0fe3d3020954f79337b6138e7b1ee6baed346c3c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4545))
|
||||
- [Prevent agent access via unconfirmed or disallowed group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/fa41ba0bc926e7b0091e4fb1cb6298b0b86eace5) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4559))
|
||||
|
||||
### Performance (1 change)
|
||||
|
||||
- [Remove permissions JSONB column from the condition](https://gitlab.com/gitlab-org/security/gitlab/-/commit/a5b902c35e60e36f3e98db2af221976093fe2278)
|
||||
|
||||
## 17.5.1 (2024-10-22)
|
||||
|
||||
### Security (2 changes)
|
||||
|
|
@ -733,6 +756,28 @@ entry.
|
|||
- [Adjust signup page items for more clarity](https://gitlab.com/gitlab-org/gitlab/-/commit/e272c8a4c7b243758454d6f15363d0c13ca05c04) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165202)) **GitLab Enterprise Edition**
|
||||
- [Removes Unused CSS class](https://gitlab.com/gitlab-org/gitlab/-/commit/4e17154650ee4afc8b1ae4238d27efb908855a19) by @NIKU-SINGH ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164637))
|
||||
|
||||
## 17.4.4 (2024-11-12)
|
||||
|
||||
### Fixed (4 changes)
|
||||
|
||||
- [Fix bug where car left after branch deletion](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d88a8a2b0d5a864220e7ca612a73433fb61aa1e7) **GitLab Enterprise Edition**
|
||||
- [Ensure auto_merge_enabled is set when validating merge trains](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ec63d25c51b5e129ab9b8fea6c8bb5730ca1ff81) **GitLab Enterprise Edition**
|
||||
- [Update pdf worker file path in pdf viewer](https://gitlab.com/gitlab-org/security/gitlab/-/commit/bd1436d5e7900ac7ca815302b5bbd8297e43c52d)
|
||||
- [Security patch upgrade alert: Only expose to admins](https://gitlab.com/gitlab-org/security/gitlab/-/commit/6e852f3bde76486452977159f9597b1947ee84b3)
|
||||
|
||||
### Security (6 changes)
|
||||
|
||||
- [Use custom adapter for parsing FogBugz XML](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d8cf278590e2f1b496fe7cec05bd58b8adf0703b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4593))
|
||||
- [Removed id from authorize buttons and added specs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/577432b6e46b9cd6edd4e00a4667e249406f1026) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4574))
|
||||
- [HTML injection in vulnerability Code flow leads to XSS on self hosted instances](https://gitlab.com/gitlab-org/security/gitlab/-/commit/24eaacb474ad08e0bcd41b6f5a1cdada51ca8d7f) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4565))
|
||||
- [Remove is-unsafe-link from product analytics tables to prevent XSS](https://gitlab.com/gitlab-org/security/gitlab/-/commit/6ed52422fcfb1b5ab6702a57df0d564bb552472b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4580))
|
||||
- [Details of blocking merge request can be exposed via list](https://gitlab.com/gitlab-org/security/gitlab/-/commit/4d5b45a67287865c3e9a80f27755c05c46ae2bea) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4526))
|
||||
- [Prevent agent access via unconfirmed or disallowed group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/e8fd87425e9c7d045986bc50b6f9e401eb695b95) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4560))
|
||||
|
||||
### Performance (1 change)
|
||||
|
||||
- [Remove permissions JSONB column from the condition](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2f2ae57d46d3774cd483adcb8651c7bc52b2e67c)
|
||||
|
||||
## 17.4.3 (2024-10-22)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
|
@ -1635,6 +1680,17 @@ entry.
|
|||
|
||||
- [Update learn more link and docs formatting](https://gitlab.com/gitlab-org/gitlab/-/commit/6f536fdb20c2d2b96124afe693042c91483a32b2) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164889))
|
||||
|
||||
## 17.3.7 (2024-11-12)
|
||||
|
||||
### Security (6 changes)
|
||||
|
||||
- [Use custom adapter for parsing FogBugz XML](https://gitlab.com/gitlab-org/security/gitlab/-/commit/8952776336f65ba2f7a182cb42e6714f4f17b97b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4594))
|
||||
- [Removed id from authorize buttons and added specs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5f2a1b9a8cd823901e1184177fa55d43f20a3200) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4575))
|
||||
- [HTML injection in vulnerability Code flow leads to XSS on self hosted instances](https://gitlab.com/gitlab-org/security/gitlab/-/commit/59ac206c9475b5713e8aee79dffad95fda802384) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4566))
|
||||
- [Remove is-unsafe-link from product analytics tables to prevent XSS](https://gitlab.com/gitlab-org/security/gitlab/-/commit/1420ca36c7c8fa50949d934ee9eb8a1a2dc3d6a5) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4581))
|
||||
- [Details of blocking merge request can be exposed via list](https://gitlab.com/gitlab-org/security/gitlab/-/commit/aa81586dd7ca7fa7fc2d5c4b74b8d5971c573df7) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4527))
|
||||
- [Prevent agent access via unconfirmed or disallowed group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/58ddb6195652c2d04fb90db5b53889273090c18c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/4561))
|
||||
|
||||
## 17.3.6 (2024-10-22)
|
||||
|
||||
### Security (2 changes)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
7608c9704fb3dd503b9a7c18ea8d1a7ed6bdd02f
|
||||
839d9cc922e06ce778a0b4b8569a42f5d7a4b9d1
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
59a9a4683512829b6ec9c9819b78d42e8e02f091
|
||||
0bd021b3916f1cf7ab43fbb0e327292bb2e15510
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.
|
|||
import createProtectionRuleMutation from '~/packages_and_registries/settings/project/graphql/mutations/create_container_protection_rule.mutation.graphql';
|
||||
import { s__, __ } from '~/locale';
|
||||
|
||||
const GRAPHQL_ACCESS_LEVEL_VALUE_NULL = null;
|
||||
const GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER = 'MAINTAINER';
|
||||
const GRAPHQL_ACCESS_LEVEL_VALUE_OWNER = 'OWNER';
|
||||
const GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN = 'ADMIN';
|
||||
|
|
@ -42,7 +41,6 @@ export default {
|
|||
protectionRuleFormData: {
|
||||
repositoryPathPattern: '',
|
||||
minimumAccessLevelForPush: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
minimumAccessLevelForDelete: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER,
|
||||
},
|
||||
updateInProgress: false,
|
||||
alertErrorMessages: [],
|
||||
|
|
@ -66,12 +64,10 @@ export default {
|
|||
projectPath: this.projectPath,
|
||||
repositoryPathPattern: this.protectionRuleFormData.repositoryPathPattern,
|
||||
minimumAccessLevelForPush: this.protectionRuleFormData.minimumAccessLevelForPush,
|
||||
minimumAccessLevelForDelete: this.protectionRuleFormData.minimumAccessLevelForDelete,
|
||||
};
|
||||
},
|
||||
minimumAccessLevelOptions() {
|
||||
return [
|
||||
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_NULL, text: __('Developer (default)') },
|
||||
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_MAINTAINER, text: __('Maintainer') },
|
||||
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_OWNER, text: __('Owner') },
|
||||
{ value: GRAPHQL_ACCESS_LEVEL_VALUE_ADMIN, text: __('Admin') },
|
||||
|
|
@ -170,19 +166,6 @@ export default {
|
|||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
:label="s__('ContainerRegistry|Minimum access level for delete')"
|
||||
label-for="input-minimum-access-level-for-delete"
|
||||
:disabled="isFieldDisabled"
|
||||
>
|
||||
<gl-form-select
|
||||
id="input-minimum-access-level-for-delete"
|
||||
v-model="protectionRuleFormData.minimumAccessLevelForDelete"
|
||||
:options="minimumAccessLevelOptions"
|
||||
:disabled="isFieldDisabled"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<div class="gl-flex gl-justify-start">
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ import { s__, __ } from '~/locale';
|
|||
const PAGINATION_DEFAULT_PER_PAGE = 10;
|
||||
|
||||
const I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH = s__('ContainerRegistry|Minimum access level for push');
|
||||
const I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE = s__(
|
||||
'ContainerRegistry|Minimum access level for delete',
|
||||
);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -48,7 +45,7 @@ export default {
|
|||
i18n: {
|
||||
settingBlockTitle: s__('ContainerRegistry|Protected containers'),
|
||||
settingBlockDescription: s__(
|
||||
'ContainerRegistry|When a container is protected, only certain user roles can push and delete the protected container image, which helps to avoid tampering with the container image.',
|
||||
'ContainerRegistry|When a container is protected, only certain user roles can push the protected container image, which helps to avoid tampering with the container image.',
|
||||
),
|
||||
protectionRuleDeletionConfirmModal: {
|
||||
title: s__('ContainerRegistry|Delete container protection rule?'),
|
||||
|
|
@ -60,7 +57,6 @@ export default {
|
|||
),
|
||||
},
|
||||
minimumAccessLevelForPush: I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH,
|
||||
minimumAccessLevelForDelete: I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE,
|
||||
},
|
||||
apollo: {
|
||||
protectionRulesQueryPayload: {
|
||||
|
|
@ -94,7 +90,6 @@ export default {
|
|||
return this.protectionRulesQueryResult.map((protectionRule) => {
|
||||
return {
|
||||
id: protectionRule.id,
|
||||
minimumAccessLevelForDelete: protectionRule.minimumAccessLevelForDelete,
|
||||
minimumAccessLevelForPush: protectionRule.minimumAccessLevelForPush,
|
||||
repositoryPathPattern: protectionRule.repositoryPathPattern,
|
||||
};
|
||||
|
|
@ -210,11 +205,6 @@ export default {
|
|||
minimumAccessLevelForPush: protectionRule.minimumAccessLevelForPush,
|
||||
});
|
||||
},
|
||||
updateProtectionRuleMinimumAccessLevelForDelete(protectionRule) {
|
||||
this.updateProtectionRule(protectionRule, {
|
||||
minimumAccessLevelForDelete: protectionRule.minimumAccessLevelForDelete,
|
||||
});
|
||||
},
|
||||
updateProtectionRule(protectionRule, updateData) {
|
||||
this.clearAlertMessage();
|
||||
|
||||
|
|
@ -259,11 +249,6 @@ export default {
|
|||
label: I18N_MINIMUM_ACCESS_LEVEL_FOR_PUSH,
|
||||
tdClass: '!gl-align-middle',
|
||||
},
|
||||
{
|
||||
key: 'minimumAccessLevelForDelete',
|
||||
label: I18N_MINIMUM_ACCESS_LEVEL_FOR_DELETE,
|
||||
tdClass: '!gl-align-middle',
|
||||
},
|
||||
{
|
||||
key: 'rowActions',
|
||||
label: __('Actions'),
|
||||
|
|
@ -326,18 +311,6 @@ export default {
|
|||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(minimumAccessLevelForDelete)="{ item }">
|
||||
<gl-form-select
|
||||
v-model="item.minimumAccessLevelForDelete"
|
||||
class="gl-max-w-34"
|
||||
required
|
||||
:aria-label="$options.i18n.minimumAccessLevelForDelete"
|
||||
:options="minimumAccessLevelOptions"
|
||||
:disabled="isProtectionRuleMinimumAccessLevelForPushFormSelectDisabled(item)"
|
||||
@change="updateProtectionRuleMinimumAccessLevelForDelete(item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(rowActions)="{ item }">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ mutation createContainerProtectionRule($input: CreateContainerRegistryProtection
|
|||
id
|
||||
repositoryPathPattern
|
||||
minimumAccessLevelForPush
|
||||
minimumAccessLevelForDelete
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ mutation deleteContainerRegistryProtectionRule(
|
|||
id
|
||||
repositoryPathPattern
|
||||
minimumAccessLevelForPush
|
||||
minimumAccessLevelForDelete
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ mutation updateContainerRegistryProtectionRule(
|
|||
id
|
||||
repositoryPathPattern
|
||||
minimumAccessLevelForPush
|
||||
minimumAccessLevelForDelete
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ query getProjectContainerProtectionRules(
|
|||
id
|
||||
repositoryPathPattern
|
||||
minimumAccessLevelForPush
|
||||
minimumAccessLevelForDelete
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
|
|
|
|||
|
|
@ -119,6 +119,12 @@ export default {
|
|||
return this.currentTab === 0 && !this.showEmptyState;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChanged);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChanged);
|
||||
},
|
||||
methods: {
|
||||
nextPage(item) {
|
||||
this.cursor = {
|
||||
|
|
@ -152,6 +158,11 @@ export default {
|
|||
this.alert?.dismiss();
|
||||
this.queryFilterValues = { ...data };
|
||||
},
|
||||
handleVisibilityChanged() {
|
||||
if (!document.hidden) {
|
||||
this.updateAllQueries();
|
||||
}
|
||||
},
|
||||
async handleItemChanged(id, markedAsDone) {
|
||||
await this.updateAllQueries(false);
|
||||
this.showUndoToast(id, markedAsDone);
|
||||
|
|
|
|||
|
|
@ -697,6 +697,12 @@
|
|||
color: $gl-text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
/* Vertically align badges with icons */
|
||||
.gl-badge:has(> svg) {
|
||||
position: relative;
|
||||
bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ class Import::FogbugzController < Import::BaseController
|
|||
extend ::Gitlab::Utils::Override
|
||||
|
||||
before_action :verify_fogbugz_import_enabled
|
||||
before_action -> { check_rate_limit!(:fogbugz_import, scope: current_user, redirect_back: true) }, only: :callback
|
||||
|
||||
before_action :user_map, only: [:new_user_map, :create_user_map]
|
||||
before_action :verify_blocked_uri, only: :callback
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module WorkItems
|
||||
class DescriptionTemplatesResolver < BaseResolver
|
||||
include LooksAhead
|
||||
|
||||
type Types::WorkItems::TypeType.connection_type, null: true
|
||||
|
||||
argument :name, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: "Fetches the specific DescriptionTemplate."
|
||||
|
||||
argument :search, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: "Search for DescriptionTemplates by name."
|
||||
|
||||
def resolve_with_lookahead(**_args)
|
||||
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/501097
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,7 +18,7 @@ module Types
|
|||
description: 'Human-readable display name for the access level.'
|
||||
|
||||
def human_access
|
||||
::Gitlab::Access.human_access(object)
|
||||
::Gitlab::Access.human_access_with_none(object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ module Types
|
|||
Gitlab::Access.option_descriptions
|
||||
end
|
||||
|
||||
value 'GUEST', value: Gitlab::Access::GUEST, description: descriptions[:guest]
|
||||
value 'REPORTER', value: Gitlab::Access::REPORTER, description: descriptions[:reporter]
|
||||
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: descriptions[:developer]
|
||||
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: descriptions[:maintainer]
|
||||
value 'OWNER', value: Gitlab::Access::OWNER, description: descriptions[:owner]
|
||||
value 'GUEST', value: Gitlab::Access::GUEST, description: descriptions[Gitlab::Access::GUEST]
|
||||
value 'REPORTER', value: Gitlab::Access::REPORTER, description: descriptions[Gitlab::Access::REPORTER]
|
||||
value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: descriptions[Gitlab::Access::DEVELOPER]
|
||||
value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: descriptions[Gitlab::Access::MAINTAINER]
|
||||
value 'OWNER', value: Gitlab::Access::OWNER, description: descriptions[Gitlab::Access::OWNER]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ module Types
|
|||
method: :itself,
|
||||
experiment: { milestone: '17.6' }
|
||||
|
||||
field :work_item_description_templates,
|
||||
Types::WorkItems::DescriptionTemplateType.connection_type,
|
||||
resolver: Resolvers::WorkItems::DescriptionTemplatesResolver,
|
||||
null: true, experiment: { milestone: '17.6' },
|
||||
description: 'Work item description templates available to the namespace.'
|
||||
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
def achievements_path
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module WorkItems
|
||||
class DescriptionTemplateType < BaseObject
|
||||
graphql_name 'WorkItemDescriptionTemplate'
|
||||
|
||||
authorize :read_work_item
|
||||
|
||||
field :content, GraphQL::Types::String,
|
||||
description: 'Content of Description Template.', null: false
|
||||
field :name, GraphQL::Types::String,
|
||||
description: 'Name of Description Template.', null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -17,6 +17,15 @@ module Ci
|
|||
validates :component, :catalog_resource, :component_project, :used_by_project_id, presence: true
|
||||
|
||||
validates :last_used_date, uniqueness: { scope: [:component_id, :used_by_project_id] }, presence: true
|
||||
|
||||
def self.get_usage_for(component, used_by_project)
|
||||
Ci::Catalog::Resources::Components::LastUsage.find_or_initialize_by(
|
||||
component: component,
|
||||
catalog_resource: component.catalog_resource,
|
||||
component_project: component.project,
|
||||
used_by_project_id: used_by_project.id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ module Clusters
|
|||
end
|
||||
|
||||
def groups_with_direct_membership_for(user)
|
||||
::Group.joins("INNER JOIN members ON " \
|
||||
"members.source_id = namespaces.id AND members.source_type = 'Namespace'")
|
||||
.where(members: { user_id: user.id, access_level: Gitlab::Access::DEVELOPER.. })
|
||||
.select('namespaces.id AS id, members.access_level AS access_level')
|
||||
user
|
||||
.groups_with_active_memberships
|
||||
.merge(GroupMember.by_access_level(Gitlab::Access::DEVELOPER..))
|
||||
.select('namespaces.id AS id, members.access_level AS access_level')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def member_role_description; end
|
||||
def member_role_description
|
||||
member.role_description
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class MemberEntity < Grape::Entity
|
|||
end
|
||||
|
||||
expose :access_level do
|
||||
expose :human_access, as: :string_value
|
||||
expose :human_access_with_none, as: :string_value
|
||||
expose :access_level, as: :integer_value
|
||||
expose :member_role_id
|
||||
expose :member_role_description, as: :description
|
||||
|
|
|
|||
|
|
@ -19,12 +19,22 @@ module Ci
|
|||
used_by_project_id: used_by_project.id
|
||||
)
|
||||
|
||||
component_last_usage = Ci::Catalog::Resources::Components::LastUsage.get_usage_for(component, used_by_project)
|
||||
|
||||
if component_last_usage.new_record?
|
||||
component_last_usage.last_used_date = Time.current.to_date
|
||||
else
|
||||
component_last_usage.touch(:last_used_date)
|
||||
end
|
||||
|
||||
component_last_usage.save # Save last usage regardless of component_usage
|
||||
|
||||
if component_usage.save
|
||||
ServiceResponse.success(message: 'Usage recorded')
|
||||
else
|
||||
errors = component_usage.errors
|
||||
errors = component_usage.errors || component_last_usage.errors
|
||||
|
||||
if errors.size == 1 && errors.first.type == :taken # Only unique validation failed
|
||||
if errors.size == 1 && errors.first.type == :taken
|
||||
ServiceResponse.success(message: 'Usage already recorded for today')
|
||||
else
|
||||
exception = ValidationError.new(errors.full_messages.join(', '))
|
||||
|
|
|
|||
|
|
@ -28,5 +28,5 @@
|
|||
.div
|
||||
= render Pajamas::ButtonComponent.new(type: :submit,
|
||||
variant: :confirm,
|
||||
button_options: { id: 'commit-changes', testid: 'authorization-button'}) do
|
||||
button_options: {data: { testid: 'authorization-button'} }) do
|
||||
= s_('DeviceAuth|Confirm')
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@
|
|||
.div
|
||||
= render Pajamas::ButtonComponent.new(type: :submit,
|
||||
variant: :confirm,
|
||||
button_options: { id: 'commit-changes', testid: 'authorization-button'}) do
|
||||
button_options: { data: { testid: 'authorization-button' }}) do
|
||||
= s_('DeviceAuth|Authorize')
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
- page_title _('Group members')
|
||||
|
||||
- if can_admin_group_member?(@group)
|
||||
= render_if_exists 'groups/group_members/link_to_pending_members'
|
||||
= render ::Layouts::PageHeadingComponent.new(_('Group members')) do |c|
|
||||
- c.with_description do
|
||||
= group_member_header_subtext(@group)
|
||||
- c.with_actions do
|
||||
= render_if_exists 'groups/group_members/link_to_pending_members'
|
||||
- if current_appearance&.member_guidelines?
|
||||
.gl-w-full.order-md-1
|
||||
= brand_member_guidelines
|
||||
|
|
|
|||
|
|
@ -290,9 +290,6 @@ group_security_exclusions:
|
|||
column: group_id
|
||||
on_delete: async_delete
|
||||
group_type_ci_runners_e59bb2812d:
|
||||
- table: users
|
||||
column: creator_id
|
||||
on_delete: async_nullify
|
||||
- table: namespaces
|
||||
column: sharding_key_id
|
||||
on_delete: async_delete
|
||||
|
|
@ -303,10 +300,6 @@ groups_visits:
|
|||
- table: users
|
||||
column: user_id
|
||||
on_delete: async_delete
|
||||
instance_type_ci_runners_e59bb2812d:
|
||||
- table: users
|
||||
column: creator_id
|
||||
on_delete: async_nullify
|
||||
member_approvals:
|
||||
- table: users
|
||||
column: requested_by_id
|
||||
|
|
@ -466,9 +459,6 @@ project_security_statistics:
|
|||
column: project_id
|
||||
on_delete: async_delete
|
||||
project_type_ci_runners_e59bb2812d:
|
||||
- table: users
|
||||
column: creator_id
|
||||
on_delete: async_nullify
|
||||
- table: projects
|
||||
column: sharding_key_id
|
||||
on_delete: async_delete
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ feature_category: code_review_workflow
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155492
|
||||
milestone: '17.1'
|
||||
queued_migration_version: 20240605193142
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: '20241111232623'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
table_name: users
|
||||
classes:
|
||||
- TmpUser
|
||||
- User
|
||||
feature_categories:
|
||||
- user_profile
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeBackfillMergeRequestAssigneesProjectId < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.6'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'BackfillMergeRequestAssigneesProjectId',
|
||||
table_name: :merge_request_assignees,
|
||||
column_name: :id,
|
||||
job_arguments: [:project_id, :merge_requests, :target_project_id, :merge_request_id],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
c6073984bfd3ae4a438324e9e448d6e0c925d3fe22e5395d49d77e4a6fac913f
|
||||
|
|
@ -29,9 +29,7 @@ Install one of the following GitLab-approved LLM models:
|
|||
| Mistral Codestral | [Codestral 22B](https://huggingface.co/mistralai/Codestral-22B-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
|
||||
| Mistral | [Mistral 7B](https://huggingface.co/mistralai/Mistral-7B-v0.1) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
|
||||
| Mistral | [Mistral 7B-it](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| Mistral | [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-v0.1) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
|
||||
| Mistral | [Mixtral 8x7B-it](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| Mistral | [Mixtral 8x22B](https://huggingface.co/mistralai/Mixtral-8x22B-v0.1) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
|
||||
| Mistral | [Mixtral 8x22B-it](https://huggingface.co/mistralai/Mixtral-8x22B-Instruct-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| Claude 3 | [Claude 3.5 Sonnet](https://www.anthropic.com/news/claude-3-5-sonnet) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| GPT | [GPT-3.5-Turbo](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-35) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
|
||||
|
|
|
|||
|
|
@ -17546,6 +17546,29 @@ The connection type for [`WorkItem`](#workitem).
|
|||
| <a id="workitemconnectionnodes"></a>`nodes` | [`[WorkItem]`](#workitem) | A list of nodes. |
|
||||
| <a id="workitemconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
#### `WorkItemDescriptionTemplateConnection`
|
||||
|
||||
The connection type for [`WorkItemDescriptionTemplate`](#workitemdescriptiontemplate).
|
||||
|
||||
##### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemdescriptiontemplateconnectionedges"></a>`edges` | [`[WorkItemDescriptionTemplateEdge]`](#workitemdescriptiontemplateedge) | A list of edges. |
|
||||
| <a id="workitemdescriptiontemplateconnectionnodes"></a>`nodes` | [`[WorkItemDescriptionTemplate]`](#workitemdescriptiontemplate) | A list of nodes. |
|
||||
| <a id="workitemdescriptiontemplateconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
#### `WorkItemDescriptionTemplateEdge`
|
||||
|
||||
The edge type for [`WorkItemDescriptionTemplate`](#workitemdescriptiontemplate).
|
||||
|
||||
##### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemdescriptiontemplateedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||
| <a id="workitemdescriptiontemplateedgenode"></a>`node` | [`WorkItemDescriptionTemplate`](#workitemdescriptiontemplate) | The item at the end of the edge. |
|
||||
|
||||
#### `WorkItemEdge`
|
||||
|
||||
The edge type for [`WorkItem`](#workitem).
|
||||
|
|
@ -25519,6 +25542,27 @@ Returns [`WorkItem`](#workitem).
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="groupworkitemiid"></a>`iid` | [`String!`](#string) | IID of the work item. |
|
||||
|
||||
##### `Group.workItemDescriptionTemplates`
|
||||
|
||||
Work item description templates available to the namespace.
|
||||
|
||||
DETAILS:
|
||||
**Introduced** in GitLab 17.6.
|
||||
**Status**: Experiment.
|
||||
|
||||
Returns [`WorkItemDescriptionTemplateConnection`](#workitemdescriptiontemplateconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, and `last: Int`.
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="groupworkitemdescriptiontemplatesname"></a>`name` | [`String`](#string) | Fetches the specific DescriptionTemplate. |
|
||||
| <a id="groupworkitemdescriptiontemplatessearch"></a>`search` | [`String`](#string) | Search for DescriptionTemplates by name. |
|
||||
|
||||
##### `Group.workItemStateCounts`
|
||||
|
||||
Counts of work items by state for the namespace. Returns `null` if the `namespace_level_work_items` feature flag is disabled.
|
||||
|
|
@ -29286,6 +29330,27 @@ Returns [`WorkItem`](#workitem).
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="namespaceworkitemiid"></a>`iid` | [`String!`](#string) | IID of the work item. |
|
||||
|
||||
##### `Namespace.workItemDescriptionTemplates`
|
||||
|
||||
Work item description templates available to the namespace.
|
||||
|
||||
DETAILS:
|
||||
**Introduced** in GitLab 17.6.
|
||||
**Status**: Experiment.
|
||||
|
||||
Returns [`WorkItemDescriptionTemplateConnection`](#workitemdescriptiontemplateconnection).
|
||||
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, and `last: Int`.
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="namespaceworkitemdescriptiontemplatesname"></a>`name` | [`String`](#string) | Fetches the specific DescriptionTemplate. |
|
||||
| <a id="namespaceworkitemdescriptiontemplatessearch"></a>`search` | [`String`](#string) | Search for DescriptionTemplates by name. |
|
||||
|
||||
##### `Namespace.workItemTypes`
|
||||
|
||||
Work item types available to the namespace.
|
||||
|
|
@ -36438,6 +36503,15 @@ Returns [`String!`](#string).
|
|||
| <a id="workitemclosingmergerequestid"></a>`id` | [`MergeRequestsClosingIssuesID!`](#mergerequestsclosingissuesid) | Global ID of the closing merge request association. |
|
||||
| <a id="workitemclosingmergerequestmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Related merge request. |
|
||||
|
||||
### `WorkItemDescriptionTemplate`
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemdescriptiontemplatecontent"></a>`content` | [`String!`](#string) | Content of Description Template. |
|
||||
| <a id="workitemdescriptiontemplatename"></a>`name` | [`String!`](#string) | Name of Description Template. |
|
||||
|
||||
### `WorkItemPermissions`
|
||||
|
||||
Check permissions for the current user on a work item.
|
||||
|
|
|
|||
|
|
@ -1105,6 +1105,10 @@ Example response:
|
|||
|
||||
Shows information about the merge request dependencies that must be resolved before merging.
|
||||
|
||||
NOTE:
|
||||
If the user does not have access to the blocking merge request, no `blocking_merge_request`
|
||||
attribute is returned.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/merge_requests/:merge_request_iid/blocks
|
||||
```
|
||||
|
|
|
|||
|
|
@ -80,15 +80,16 @@ Configure pull mirroring settings.
|
|||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:---------------------------------|:--------|:---------|:------------|
|
||||
| `enabled` | boolean | No | Enables pull mirroring on project when set to `true`. |
|
||||
| `url` | string | No | URL of remote repository being mirrored. |
|
||||
| `auth_user` | string | No | Username used for authentication of a project to pull mirror. |
|
||||
| `auth_password` | string | No | Password used for authentication of a project to pull mirror. |
|
||||
| `mirror_trigger_builds` | boolean | No | Trigger pipelines for mirror updates when set to `true`. |
|
||||
| `only_mirror_protected_branches` | boolean | No | Limits mirroring to only protected branches when set to `true`. |
|
||||
| `mirror_branch_regex` | String | No | Contains a regular expression. Only branches with names matching the regex are mirrored. Requires `only_mirror_protected_branches` to be disabled. |
|
||||
| Attribute | Type | Required | Description |
|
||||
|:----------|:-----|:---------|:------------|
|
||||
| `enabled` | boolean | No | Enables pull mirroring on project when set to `true`. |
|
||||
| `url` | string | No | URL of remote repository being mirrored. |
|
||||
| `auth_user` | string | No | Username used for authentication of a project to pull mirror. |
|
||||
| `auth_password` | string | No | Password used for authentication of a project to pull mirror. |
|
||||
| `mirror_trigger_builds` | boolean | No | Trigger pipelines for mirror updates when set to `true`. |
|
||||
| `only_mirror_protected_branches` | boolean | No | Limits mirroring to only protected branches when set to `true`. |
|
||||
| `mirror_overwrites_diverged_branches` | boolean | No | Overwrite diverged branches. |
|
||||
| `mirror_branch_regex` | String | No | Contains a regular expression. Only branches with names matching the regex are mirrored. Requires `only_mirror_protected_branches` to be disabled. |
|
||||
|
||||
Example request to add pull mirroring:
|
||||
|
||||
|
|
|
|||
|
|
@ -296,6 +296,62 @@ the entire line, each class that requires it should be applied on its own line.
|
|||
|
||||
This ensures that `!important` applies only where intended without affecting other classes in the same line.
|
||||
|
||||
## Responsive design
|
||||
|
||||
Our UI should work well on mobile and desktop. To accomplish this we use [CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries). In general we should take a mobile first approach to media queries. This means writing CSS for mobile, then using min-width media queries to override styles on desktop.
|
||||
|
||||
### Tailwind CSS classes
|
||||
|
||||
```html
|
||||
<!-- Bad -->
|
||||
<div class="gl-mt-5 max-lg:gl-mt-3"></div>
|
||||
|
||||
<!-- Good -->
|
||||
<div class="gl-mt-3 md:gl-mt-5"></div>
|
||||
|
||||
<!-- Bad -->
|
||||
<div class="gl-mt-3 sm:max-lg:gl-mt-5"></div>
|
||||
|
||||
<!-- Good -->
|
||||
<div class="gl-mt-3 sm:gl-mt-5 lg:gl-mt-3"></div>
|
||||
|
||||
<!-- Okay if display property is dynamic (via Vue props or similar) and unknown -->
|
||||
<!-- eslint-disable-next-line @gitlab/vue-tailwind-no-max-width-media-queries -->
|
||||
<div class="max-lg:gl-hidden"></div>
|
||||
```
|
||||
|
||||
### Component classes
|
||||
|
||||
```scss
|
||||
// Bad
|
||||
.class-name {
|
||||
@apply gl-mt-5 max-lg:gl-mt-3;
|
||||
}
|
||||
|
||||
// Good
|
||||
.class-name {
|
||||
@apply gl-mt-3 lg:gl-mt-5;
|
||||
}
|
||||
|
||||
// Bad
|
||||
.class-name {
|
||||
display: block;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Good
|
||||
.class-name {
|
||||
display: flex;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
Filenames should use `snake_case`.
|
||||
|
|
|
|||
|
|
@ -206,6 +206,14 @@ There is a rate limit for notification emails related to a project or group.
|
|||
|
||||
The **rate limit** is 1,000 notifications per 24 hours per project or group per user.
|
||||
|
||||
### FogBugz import
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/439101) in GitLab 17.6.
|
||||
|
||||
There is a rate limit for triggering project imports from FogBugz.
|
||||
|
||||
The **rate limit** is 1 triggered import per minute per user.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Rack Attack is denylisting the load balancer
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ First, create a step with:
|
|||
inputs:
|
||||
who:
|
||||
default: world
|
||||
type: string
|
||||
```
|
||||
|
||||
- `spec` has one input called `who`.
|
||||
|
|
@ -69,6 +70,7 @@ First, create a step with:
|
|||
inputs:
|
||||
who:
|
||||
default: world
|
||||
type: string
|
||||
---
|
||||
exec:
|
||||
command:
|
||||
|
|
@ -281,20 +283,22 @@ Add an output to your `hello` step.
|
|||
inputs:
|
||||
who:
|
||||
default: world
|
||||
type: string
|
||||
outputs:
|
||||
greeting: {}
|
||||
greeting:
|
||||
type: string
|
||||
---
|
||||
exec:
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- "echo greeting=hello ${{ inputs.who }} | tee ${{ output_file }}"
|
||||
- "echo greeting=\"hello ${{ inputs.who }}\" | tee ${{ output_file }}"
|
||||
```
|
||||
|
||||
- In this `spec`, you've defined a single output `greeting` without a default. Because
|
||||
there is no default, the output `greeting` is required.
|
||||
- Outputs are written to a file `${{ output_file }}` (provided at run time) in the form `key=value`.
|
||||
- This step runs `echo greeting=hello ${{ inputs.name }}` and sends the output to the logs and the output file (`tee ${{ output_file }}`).
|
||||
- This step runs `echo greeting=\"hello ${{ inputs.name }}\"` and sends the output to the logs and the output file (`tee ${{ output_file }}`).
|
||||
|
||||
1. In `step.yml`, add an output to the step:
|
||||
|
||||
|
|
@ -338,7 +342,7 @@ The echo step takes a single input `echo`, prints it to the logs, and outputs it
|
|||
- name: hello_everybody
|
||||
step: .
|
||||
- name: all_my_greetings
|
||||
step: gitlab.com/gitlab-org/ci-cd/runner-tools/echo-step@master
|
||||
step: gitlab.com/gitlab-org/ci-cd/runner-tools/echo-step@main
|
||||
inputs:
|
||||
echo: "all my greetings say ${{ steps.hello_everybody.outputs.all_greetings }}"
|
||||
image: registry.gitlab.com/gitlab-org/step-runner:v0
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ When a container repository is protected, the default behavior enforces these re
|
|||
| Protect a container repository and its container images | The Maintainer role. |
|
||||
| Push or create a new image in a container repository | The role set in the [**Minimum access level for push**](#protect-a-container-repository-and-create-a-protection-rule) setting. |
|
||||
| Push or update an existing image in a container repository | The role set in the [**Minimum access level for push**](#protect-a-container-repository-and-create-a-protection-rule) setting |
|
||||
| Delete an existing image from a container repository | The role set in the [**Minimum access level for delete**](#protect-a-container-repository-and-create-a-protection-rule) setting. |
|
||||
|
||||
You can use a wildcard (`*`) to protect multiple container repositories with the same container protection rule.
|
||||
For example, you can protect different container repositories containing temporary container images built during a CI/CD pipeline.
|
||||
|
|
@ -65,8 +64,6 @@ To protect a container repository:
|
|||
The pattern can include a wildcard (`*`).
|
||||
- **Minimum access level for push** describes the minimum access level required
|
||||
to push (create or update) to the protected container repository path.
|
||||
- **Minimum access level for delete** describes the minimum access level required
|
||||
to delete from the protected container repository path.
|
||||
1. Select **Protect**.
|
||||
|
||||
The container protection rule is created, and appears in the settings.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const extendConfigs = [
|
|||
// rewrite.
|
||||
let jhConfigs = [];
|
||||
if (existsSync(path.resolve(dirname, 'jh'))) {
|
||||
const pathToJhConfig = path.resolve(dirname, 'jh/eslint.config.js')
|
||||
const pathToJhConfig = path.resolve(dirname, 'jh/eslint.config.js');
|
||||
// eslint-disable-next-line import/no-dynamic-require, no-unsanitized/method
|
||||
jhConfigs = (await import(pathToJhConfig)).default;
|
||||
}
|
||||
|
|
@ -161,6 +161,8 @@ export default [
|
|||
'@gitlab/vue-no-undef-apollo-properties': 'error',
|
||||
'@gitlab/tailwind-no-interpolation': 'error',
|
||||
'@gitlab/vue-tailwind-no-interpolation': 'error',
|
||||
'@gitlab/tailwind-no-max-width-media-queries': 'error',
|
||||
'@gitlab/vue-tailwind-no-max-width-media-queries': 'error',
|
||||
|
||||
'no-param-reassign': [
|
||||
'error',
|
||||
|
|
@ -403,6 +405,8 @@ export default [
|
|||
'@gitlab/no-runtime-template-compiler': 'off',
|
||||
'@gitlab/tailwind-no-interpolation': 'off',
|
||||
'@gitlab/vue-tailwind-no-interpolation': 'off',
|
||||
'@gitlab/no-max-width-media-queries': 'off',
|
||||
'@gitlab/vue-tailwind-no-max-width-media-queries': 'off',
|
||||
'require-await': 'error',
|
||||
'import/no-dynamic-require': 'off',
|
||||
'no-import-assign': 'off',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'helpers/file_helper'
|
||||
require_relative 'helpers/milestones'
|
||||
require_relative '../lib/generators/post_deployment_migration/post_deployment_migration_generator'
|
||||
|
||||
module Keeps
|
||||
# This is an implementation of ::Gitlab::Housekeeper::Keep.
|
||||
# This initializes the conversion of bigint columns for a given table.
|
||||
#
|
||||
# You can run it individually with:
|
||||
#
|
||||
# ```
|
||||
# bundle exec gitlab-housekeeper -d -k Keeps::InitializeBigIntConversion
|
||||
# ```
|
||||
class InitializeBigIntConversion < ::Gitlab::Housekeeper::Keep
|
||||
INTEGER_COLUMNS_FILE = 'db/integer_ids_not_yet_initialized_to_bigint.yml'
|
||||
MIGRATION_TEMPLATE = 'generator_templates/active_record/migration/'
|
||||
FALLBACK_REVIEWER_FEATURE_CATEGORY = 'database'
|
||||
CLASS_WITH_NAMESPACE = /class\s+([A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)+)\s*(?:<\s*[A-Z][A-Za-z0-9:]*\s*)?$/
|
||||
CLASS_WITHOUT_NAMESPACE = /class\s+([A-Z][A-Za-z0-9]*)\s*(?:<\s*[A-Z][A-Za-z0-9:]*\s*)?$/
|
||||
|
||||
TABLE_INT_IDS_YAML_FILE_COMMENT = <<~MESSAGE
|
||||
# -- DON'T MANUALLY EDIT --
|
||||
# Contains the list of integer IDs which were converted to bigint for new installations in
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/438124, but they are still integers for existing instances.
|
||||
# On initialize_conversion_of_integer_to_bigint those integer IDs will be removed automatically from here.
|
||||
MESSAGE
|
||||
|
||||
def initialize(...)
|
||||
::PostDeploymentMigration::PostDeploymentMigrationGenerator.source_root(MIGRATION_TEMPLATE)
|
||||
|
||||
reset_db
|
||||
migrate
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def each_change
|
||||
integer_columns_to_migrate.each do |table_name, columns|
|
||||
change = build_change(table_name, columns)
|
||||
change.changed_files = []
|
||||
migration_file_1, migration_number_1 = generate_initialization_migration_file(table_name, columns)
|
||||
migration_file_2, migration_number_2 = generate_backfill_migration_file(table_name, columns)
|
||||
|
||||
change.changed_files << migration_file_1
|
||||
change.changed_files << migration_file_2
|
||||
change.changed_files << Pathname.new('db').join('schema_migrations', migration_number_1).to_s
|
||||
change.changed_files << Pathname.new('db').join('schema_migrations', migration_number_2).to_s
|
||||
|
||||
file_path = update_model(table_name, columns)
|
||||
update_integer_columns_file(table_name)
|
||||
|
||||
migrate
|
||||
change.changed_files << Pathname.new('db').join('structure.sql').to_s
|
||||
change.changed_files << file_path
|
||||
change.changed_files << INTEGER_COLUMNS_FILE
|
||||
|
||||
yield(change)
|
||||
|
||||
reset_db
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def integer_columns_to_migrate
|
||||
YAML.safe_load_file(INTEGER_COLUMNS_FILE)
|
||||
end
|
||||
|
||||
def build_change(table_name, columns)
|
||||
change = ::Gitlab::Housekeeper::Change.new
|
||||
change.title = "Prepare conversion of #{table_name} to bigint".truncate(70, omission: '')
|
||||
change.identifiers = [self.class.name.demodulize, table_name]
|
||||
change.changelog_type = 'added'
|
||||
change.labels = labels(table_name)
|
||||
change.reviewers = pick_reviewers(table_name, change.identifiers).uniq
|
||||
|
||||
change.description = <<~MARKDOWN
|
||||
Prepares conversion of `#{table_name}` to bigint for `#{columns.join(', ')}`
|
||||
|
||||
You can read more about the process for preparing bigint conversion in
|
||||
https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#migrating-integer-primary-keys-to-bigint.
|
||||
|
||||
As part of our process we want to ensure all ID columns are bigint to avoid the risk of overflowing while we continue our growth.
|
||||
|
||||
See https://gitlab.com/gitlab-org/gitlab/-/issues/465805+
|
||||
|
||||
Verify this MR as it was automatically created by `gitlab-housekeeper`.
|
||||
|
||||
Ensure that those columns are not being converted yet in the production database by checking Joe Bot through https://console.postgres.ai/gitlab.
|
||||
If the columns were already converted in another merge request, consider closing this merge request
|
||||
MARKDOWN
|
||||
|
||||
change
|
||||
end
|
||||
|
||||
def labels(table_name)
|
||||
table_info = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name)
|
||||
|
||||
group_labels = table_info.feature_categories.flat_map do |feature_category|
|
||||
groups_helper.labels_for_feature_category(feature_category)
|
||||
end
|
||||
|
||||
group_labels << 'maintenance::scalability'
|
||||
end
|
||||
|
||||
def pick_reviewers(table_name, identifiers)
|
||||
table_info = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name)
|
||||
|
||||
table_info.feature_categories.map do |feature_category|
|
||||
groups_helper.pick_reviewer_for_feature_category(feature_category, identifiers,
|
||||
fallback_feature_category: FALLBACK_REVIEWER_FEATURE_CATEGORY)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_initialization_migration_file(table_name, columns)
|
||||
migration_name = "initialize_conversion_of_#{table_name}_to_bigint".truncate(100, omission: '')
|
||||
generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name])
|
||||
|
||||
migration_content = <<~RUBY.strip
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = :#{table_name}
|
||||
COLUMNS = %i[#{columns.join(' ')}]
|
||||
|
||||
def up
|
||||
initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS, primary_key: :#{primary_key(table_name)})
|
||||
end
|
||||
|
||||
def down
|
||||
revert_initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS)
|
||||
end
|
||||
RUBY
|
||||
|
||||
migration_file = generator.invoke_all.first
|
||||
file_helper = ::Keeps::Helpers::FileHelper.new(migration_file)
|
||||
file_helper.replace_method_content(:change, migration_content, strip_comments_from_file: true)
|
||||
|
||||
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', migration_file)
|
||||
|
||||
[migration_file, generator.migration_number]
|
||||
end
|
||||
|
||||
def generate_backfill_migration_file(table_name, columns)
|
||||
migration_name = "backfill_#{table_name}_for_bigint_conversion".truncate(100, omission: '')
|
||||
generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name])
|
||||
gitlab_schema = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).gitlab_schema
|
||||
|
||||
migration_content = <<~RUBY.strip
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :#{gitlab_schema}
|
||||
|
||||
TABLE_NAME = :#{table_name}
|
||||
COLUMNS = %i[#{columns.join(' ')}]
|
||||
|
||||
def up
|
||||
backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS, primary_key: :#{primary_key(table_name)})
|
||||
end
|
||||
|
||||
def down
|
||||
revert_backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS)
|
||||
end
|
||||
RUBY
|
||||
|
||||
migration_file = generator.invoke_all.first
|
||||
file_helper = ::Keeps::Helpers::FileHelper.new(migration_file)
|
||||
file_helper.replace_method_content(:change, migration_content, strip_comments_from_file: true)
|
||||
|
||||
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', migration_file)
|
||||
|
||||
[migration_file, generator.migration_number]
|
||||
end
|
||||
|
||||
def model_path(table_name)
|
||||
model = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first
|
||||
nested_model = model.underscore
|
||||
file_path = Rails.root.join('app', 'models', "#{nested_model}.rb").to_s
|
||||
|
||||
if File.exist?(file_path)
|
||||
file_path
|
||||
else
|
||||
Rails.root.join('ee', 'app', 'models', "#{nested_model}.rb").to_s
|
||||
end
|
||||
end
|
||||
|
||||
def update_model(table_name, columns)
|
||||
class_name = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first.split("::").last
|
||||
file_path = model_path(table_name)
|
||||
|
||||
ignore_columns = columns.map do |column|
|
||||
"ignore_column :#{column}_convert_to_bigint, remove_with: '#{n_3_milestone.version}', " \
|
||||
"remove_after: '#{n_2_milestone.date}'"
|
||||
end
|
||||
|
||||
new_content = <<~RUBY
|
||||
#{ignore_columns.join("\n")}
|
||||
RUBY
|
||||
|
||||
insert_after_class_definition(file_path, class_name, new_content)
|
||||
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', file_path)
|
||||
|
||||
file_path
|
||||
end
|
||||
|
||||
def insert_after_class_definition(file_path, class_name, new_content)
|
||||
content = File.read(file_path)
|
||||
pattern = class_name.include?('::') ? CLASS_WITH_NAMESPACE : CLASS_WITHOUT_NAMESPACE
|
||||
|
||||
matches = content.scan(pattern)
|
||||
return unless matches.flatten.include?(class_name)
|
||||
|
||||
updated_content = content.gsub(pattern) do |match|
|
||||
if ::Regexp.last_match(1) == class_name
|
||||
"#{match}\n#{new_content}\n"
|
||||
else
|
||||
match
|
||||
end
|
||||
end
|
||||
|
||||
File.write(file_path, updated_content)
|
||||
end
|
||||
|
||||
def update_integer_columns_file(table_name)
|
||||
file_path = Rails.root.join(INTEGER_COLUMNS_FILE)
|
||||
data = YAML.safe_load_file(file_path)
|
||||
data.delete(table_name)
|
||||
|
||||
File.open(INTEGER_COLUMNS_FILE, 'w') do |f|
|
||||
f.write(TABLE_INT_IDS_YAML_FILE_COMMENT)
|
||||
f.write(data.to_yaml)
|
||||
end
|
||||
end
|
||||
|
||||
def migrate
|
||||
::Gitlab::Housekeeper::Shell.execute('rails', 'db:migrate', env: { 'RAILS_ENV' => 'test' })
|
||||
end
|
||||
|
||||
def reset_db
|
||||
ApplicationRecord.clear_all_connections!
|
||||
::Gitlab::Housekeeper::Shell.execute('rails', 'db:reset', env: { 'RAILS_ENV' => 'test' })
|
||||
end
|
||||
|
||||
def n_2_milestone
|
||||
milestones_helper.upcoming_milestones[2]
|
||||
end
|
||||
|
||||
def n_3_milestone
|
||||
milestones_helper.upcoming_milestones[3]
|
||||
end
|
||||
|
||||
def groups_helper
|
||||
@groups_helper ||= ::Keeps::Helpers::Groups.new
|
||||
end
|
||||
|
||||
def milestones_helper
|
||||
@milestones_helper ||= ::Keeps::Helpers::Milestones.new
|
||||
end
|
||||
|
||||
def primary_key(table_name)
|
||||
Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first.constantize.primary_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -106,76 +106,14 @@ module API
|
|||
'campfire' => ::Integrations::Campfire.api_arguments,
|
||||
'confluence' => ::Integrations::Confluence.api_arguments,
|
||||
'custom-issue-tracker' => ::Integrations::CustomIssueTracker.api_arguments,
|
||||
'datadog' => [
|
||||
{
|
||||
required: true,
|
||||
name: :api_key,
|
||||
type: String,
|
||||
desc: 'API key used for authentication with Datadog'
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: :datadog_site,
|
||||
type: String,
|
||||
desc: 'The Datadog site to send data to. To send data to the EU site, use datadoghq.eu'
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: :api_url,
|
||||
type: String,
|
||||
desc: '(Advanced) The full URL for your Datadog site'
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: :archive_trace_events,
|
||||
type: ::Grape::API::Boolean,
|
||||
desc: 'When enabled, job logs will be collected by Datadog and shown along pipeline execution traces'
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: :datadog_service,
|
||||
type: String,
|
||||
desc: 'Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments'
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: :datadog_env,
|
||||
type: String,
|
||||
desc: 'For self-managed deployments, set the env tag for all the data sent to Datadog'
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: :datadog_tags,
|
||||
type: String,
|
||||
desc: 'Custom tags in Datadog. Specify one tag per line in the format: "key:value\nkey2:value2"'
|
||||
}
|
||||
],
|
||||
'datadog' => ::Integrations::Datadog.api_arguments,
|
||||
'diffblue-cover' => ::Integrations::DiffblueCover.api_arguments,
|
||||
'discord' => [
|
||||
::Integrations::Discord.api_arguments,
|
||||
chat_notification_flags,
|
||||
chat_notification_channels
|
||||
].flatten,
|
||||
'drone-ci' => [
|
||||
{
|
||||
required: true,
|
||||
name: :token,
|
||||
type: String,
|
||||
desc: 'Drone CI token'
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
name: :drone_url,
|
||||
type: String,
|
||||
desc: 'Drone CI URL'
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: :enable_ssl_verification,
|
||||
type: ::Grape::API::Boolean,
|
||||
desc: 'Enable SSL verification'
|
||||
}
|
||||
],
|
||||
'drone-ci' => ::Integrations::DroneCi.api_arguments,
|
||||
'emails-on-push' => [
|
||||
{
|
||||
required: true,
|
||||
|
|
|
|||
|
|
@ -65,11 +65,12 @@ module Gitlab
|
|||
|
||||
def option_descriptions
|
||||
{
|
||||
guest: s_('MemberRole|The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders.'),
|
||||
reporter: s_('MemberRole|The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code.'),
|
||||
developer: s_('MemberRole|The Developer role gives users access to contribute code while restricting sensitive administrative actions.'),
|
||||
maintainer: s_('MemberRole|The Maintainer role is primarily used for managing code reviews, approvals, and administrative settings for projects. This role can also manage project memberships.'),
|
||||
owner: s_('MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners.')
|
||||
NO_ACCESS => s_('MemberRole|The None role is assigned to the invited group users of a shared project when project sharing is disabled in group setting.'),
|
||||
GUEST => s_('MemberRole|The Guest role is for users who need visibility into a project or group but should not have the ability to make changes, such as external stakeholders.'),
|
||||
REPORTER => s_('MemberRole|The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code.'),
|
||||
DEVELOPER => s_('MemberRole|The Developer role gives users access to contribute code while restricting sensitive administrative actions.'),
|
||||
MAINTAINER => s_('MemberRole|The Maintainer role is primarily used for managing code reviews, approvals, and administrative settings for projects. This role can also manage project memberships.'),
|
||||
OWNER => s_('MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners.')
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -143,7 +144,11 @@ module Gitlab
|
|||
options_with_owner.key(access)
|
||||
end
|
||||
|
||||
def human_access_with_none(access)
|
||||
def role_description(access)
|
||||
option_descriptions[access]
|
||||
end
|
||||
|
||||
def human_access_with_none(access, _member_role = nil)
|
||||
options_with_none.key(access)
|
||||
end
|
||||
|
||||
|
|
@ -204,6 +209,10 @@ module Gitlab
|
|||
Gitlab::Access.human_access(access_field)
|
||||
end
|
||||
|
||||
def role_description
|
||||
Gitlab::Access.role_description(access_field)
|
||||
end
|
||||
|
||||
def human_access_with_none
|
||||
Gitlab::Access.human_access_with_none(access_field)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ module Gitlab
|
|||
vertex_embeddings_api: { threshold: 450, interval: 1.minute },
|
||||
jobs_index: { threshold: -> { application_settings.project_jobs_api_rate_limit }, interval: 1.minute },
|
||||
bulk_import: { threshold: 6, interval: 1.minute },
|
||||
fogbugz_import: { threshold: 1, interval: 1.minute },
|
||||
import_source_user_notification: { threshold: 1, interval: 8.hours },
|
||||
projects_api_rate_limit_unauthenticated: {
|
||||
threshold: -> { application_settings.projects_api_rate_limit_unauthenticated }, interval: 10.minutes
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ module Gitlab
|
|||
last_analyzed_at = connection.select_value(
|
||||
"SELECT pg_stat_get_last_analyze_time('#{table_to_query}'::regclass)"
|
||||
)
|
||||
last_analyzed_at.present? && last_analyzed_at >= Time.current - analyze_interval
|
||||
last_analyzed_at.present? && last_analyzed_at >= ::Time.current - analyze_interval
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module Partitioning
|
||||
module Time
|
||||
class BaseStrategy
|
||||
attr_reader :model, :partitioning_key, :retain_for, :retain_non_empty_partitions, :analyze_interval
|
||||
|
||||
delegate :table_name, to: :model
|
||||
|
||||
def initialize(
|
||||
model, partitioning_key, retain_for: nil, retain_non_empty_partitions: false,
|
||||
analyze_interval: nil)
|
||||
@model = model
|
||||
@partitioning_key = partitioning_key
|
||||
@retain_for = retain_for
|
||||
@retain_non_empty_partitions = retain_non_empty_partitions
|
||||
@analyze_interval = analyze_interval
|
||||
end
|
||||
|
||||
def current_partitions
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Check the currently existing partitions and determine which ones are missing
|
||||
def missing_partitions
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def extra_partitions
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def desired_partitions
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def relevant_range
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def after_adding_partitions
|
||||
# No-op, required by the partition manager
|
||||
end
|
||||
|
||||
def validate_and_fix
|
||||
# No-op, required by the partition manager
|
||||
end
|
||||
|
||||
def oldest_active_date
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def partition_name(lower_bound)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def partition_for(upper_bound:, lower_bound: nil)
|
||||
TimePartition.new(table_name, lower_bound, upper_bound, partition_name: partition_name(lower_bound))
|
||||
end
|
||||
|
||||
def pruning_old_partitions?
|
||||
retain_for.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,5 +7,8 @@ module Gitlab
|
|||
# Custom adapter to validate the URL before each request
|
||||
# This way we avoid DNS rebinds or other unsafe requests
|
||||
::Fogbugz.adapter[:http] = HttpAdapter
|
||||
# Custom adapter to validate size of incoming XML before
|
||||
# attempting to parse it.
|
||||
::Fogbugz.adapter[:xml] = XmlAdapter
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module FogbugzImport
|
||||
module NokogiriBackendWithLimits
|
||||
extend ActiveSupport::XmlMini_Nokogiri
|
||||
|
||||
module Conversions
|
||||
module Document
|
||||
def to_hash
|
||||
objects = object_count(root)
|
||||
|
||||
if objects > XmlAdapter::MAX_ALLOWED_OBJECTS
|
||||
raise XmlAdapter::ResponseTooLargeError,
|
||||
"XML exceeds permitted complexity: #{objects}/#{XmlAdapter::MAX_ALLOWED_OBJECTS} objects"
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def object_count(object)
|
||||
return 0 if object.text? || object.cdata?
|
||||
return 1 unless object.children.any?
|
||||
|
||||
1 + object.children.sum { |v| object_count(v) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Nokogiri::XML::Document.include(Conversions::Document)
|
||||
end
|
||||
|
||||
class XmlAdapter
|
||||
ResponseTooLargeError = Class.new(StandardError)
|
||||
|
||||
MAX_ALLOWED_BYTES = 5.megabytes
|
||||
MAX_ALLOWED_OBJECTS = 250_000
|
||||
|
||||
def self.parse(xml)
|
||||
if xml.bytesize > MAX_ALLOWED_BYTES
|
||||
raise ResponseTooLargeError, "XML exceeds permitted size: #{xml.bytesize}/#{MAX_ALLOWED_BYTES} bytes"
|
||||
end
|
||||
|
||||
# We use ActiveSupport::XmlMini to get a simplified hash structure,
|
||||
# aligned with what we were previously expecting from Crack, but we use
|
||||
# Nokogiri for performance and security reasons.
|
||||
ActiveSupport::XmlMini.with_backend(NokogiriBackendWithLimits) do
|
||||
Hash.from_xml(xml)['response']
|
||||
rescue Nokogiri::XML::SyntaxError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,8 +3,12 @@
|
|||
module Gitlab
|
||||
module Import
|
||||
class PlaceholderUserCreator
|
||||
LAMBDA_FOR_UNIQUE_USERNAME = ->(username) { User.username_exists?(username) }.freeze
|
||||
LAMBDA_FOR_UNIQUE_EMAIL = ->(email) { User.find_by_email(email) || ::Email.find_by_email(email) }.freeze
|
||||
LAMBDA_FOR_UNIQUE_USERNAME = ->(username) do
|
||||
::Namespace.by_path(username) || User.username_exists?(username)
|
||||
end.freeze
|
||||
LAMBDA_FOR_UNIQUE_EMAIL = ->(email) do
|
||||
User.find_by_email(email) || ::Email.find_by_email(email)
|
||||
end.freeze
|
||||
|
||||
delegate :import_type, :namespace, :source_user_identifier, :source_name, :source_username, to: :source_user,
|
||||
private: true
|
||||
|
|
|
|||
|
|
@ -571,6 +571,7 @@ namespace :gitlab do
|
|||
.reject { |c| c.name =~ /^(?:EE::)?Gitlab::(?:BackgroundMigration|DatabaseImporters)::/ }
|
||||
.reject { |c| c.name =~ /^HABTM_/ }
|
||||
.reject { |c| c < Gitlab::Database::Migration[1.0]::MigrationRecord }
|
||||
.reject { |c| c.name == 'TmpUser' }
|
||||
.each { |c| classes[c.table_name] << c.name if classes.has_key?(c.table_name) && c.name.present? }
|
||||
|
||||
sources.each do |source_name|
|
||||
|
|
|
|||
|
|
@ -15269,9 +15269,6 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Manifest media type: %{mediaType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Minimum access level for delete"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Minimum access level for push"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15463,7 +15460,7 @@ msgstr ""
|
|||
msgid "ContainerRegistry|We are having trouble connecting to the Container Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|When a container is protected, only certain user roles can push and delete the protected container image, which helps to avoid tampering with the container image."
|
||||
msgid "ContainerRegistry|When a container is protected, only certain user roles can push the protected container image, which helps to avoid tampering with the container image."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|While the rename is in progress, new uploads to the container registry are blocked. Ongoing uploads may fail and need to be retried."
|
||||
|
|
@ -33716,6 +33713,9 @@ msgstr ""
|
|||
msgid "MemberRole|The Minimal Access role is for users who need the least amount of access into groups and projects. You can assign this role as a default, before giving a user another role with more permissions."
|
||||
msgstr ""
|
||||
|
||||
msgid "MemberRole|The None role is assigned to the invited group users of a shared project when project sharing is disabled in group setting."
|
||||
msgstr ""
|
||||
|
||||
msgid "MemberRole|The Owner role is normally assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
"lint-docs": "scripts/lint-doc.sh",
|
||||
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives",
|
||||
"internal:stylelint": "stylelint -q --rd '{ee/,}app/assets/{stylesheets/**/*.{css,scss},builds/tailwind.css}'",
|
||||
"preinternal:stylelint": "yarn run tailwindcss:build",
|
||||
"prejest": "yarn check-dependencies",
|
||||
"build:css": "node scripts/frontend/build_css.mjs",
|
||||
"tailwindcss:build": "node scripts/frontend/tailwindcss.mjs",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ RSpec.describe Import::FogbugzController, feature_category: :importers do
|
|||
include_examples 'denies local request', 'Only allowed schemes are http, https'
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rate limited endpoint', rate_limit_key: :fogbugz_import, with_redirect: true do
|
||||
let(:current_user) { user }
|
||||
|
||||
def request
|
||||
post :callback, params: { uri: uri, email: 'test@example.com', password: 'mypassword' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create_user_map' do
|
||||
|
|
|
|||
|
|
@ -115,14 +115,16 @@ RSpec.describe 'Database schema',
|
|||
p_ci_pipelines: %w[partition_id auto_canceled_by_partition_id auto_canceled_by_id],
|
||||
p_ci_runner_machine_builds: %w[project_id],
|
||||
ci_runners: %w[sharding_key_id], # This value is meant to populate the partitioned table, no other usage
|
||||
ci_runners_e59bb2812d: %w[sharding_key_id], # This field is only used in the partitions, and has the appropriate FKs
|
||||
instance_type_ci_runners_e59bb2812d: %w[creator_id sharding_key_id], # No need for LFKs on partition, already handled on ci_runners_e59bb2812d routing table.
|
||||
group_type_ci_runners_e59bb2812d: %w[creator_id sharding_key_id], # No need for LFKs on partition, already handled on ci_runners_e59bb2812d routing table.
|
||||
project_type_ci_runners_e59bb2812d: %w[creator_id sharding_key_id], # No need for LFKs on partition, already handled on ci_runners_e59bb2812d routing table.
|
||||
ci_runner_machines: %w[sharding_key_id], # This value is meant to populate the partitioned table, no other usage
|
||||
ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # This field is only used in the partitions, and has the appropriate FKs. runner_id temporarily ignored due to incident 18792
|
||||
instance_type_ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # This field is always NULL in this partition. runner_id temporarily ignored due to incident 18792
|
||||
group_type_ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # No need for LFK, rows will be deleted by the FK to ci_runners. runner_id temporarily ignored due to incident 18792
|
||||
project_type_ci_runner_machines_687967fa8a: %w[runner_id sharding_key_id], # No need for LFK, rows will be deleted by the FK to ci_runners. runner_id temporarily ignored due to incident 18792
|
||||
ci_runner_projects: %w[runner_id],
|
||||
ci_runners_e59bb2812d: %w[sharding_key_id], # This field is only used in the partitions, and has the appropriate FKs
|
||||
instance_type_ci_runners_e59bb2812d: %w[sharding_key_id], # This field is always NULL in this partition
|
||||
ci_sources_pipelines: %w[partition_id source_partition_id source_job_id],
|
||||
ci_sources_projects: %w[partition_id],
|
||||
ci_stages: %w[partition_id project_id pipeline_id],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ RSpec.describe 'OAuth Provider', feature_category: :system_access do
|
|||
)
|
||||
end
|
||||
|
||||
def visit_oauth_device_authorization_path
|
||||
visit oauth_device_authorizations_index_path(
|
||||
user_code: user_code
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
|
@ -27,6 +33,16 @@ RSpec.describe 'OAuth Provider', feature_category: :system_access do
|
|||
it_behaves_like 'Secure OAuth Authorizations'
|
||||
end
|
||||
|
||||
describe 'Device OAuth authorization' do
|
||||
let(:user_code) { 'valid_user_code' }
|
||||
|
||||
before do
|
||||
visit_oauth_device_authorization_path
|
||||
end
|
||||
|
||||
it_behaves_like 'Secure Device OAuth Authorizations'
|
||||
end
|
||||
|
||||
context 'when the OAuth application has HTML in the name' do
|
||||
let(:client_name) { '<img src=x onerror=alert(1)>' }
|
||||
let(:application) { create(:oauth_application, name: client_name, scopes: 'read_user') }
|
||||
|
|
@ -42,6 +58,10 @@ RSpec.describe 'OAuth Provider', feature_category: :system_access do
|
|||
end
|
||||
end
|
||||
|
||||
it 'expects button not to have an id attribute' do
|
||||
expect(find_by_testid('authorization-button')[:id].nil?).to be_truthy
|
||||
end
|
||||
|
||||
# rubocop:disable Layout/LineLength -- It is a string
|
||||
it 'sanitizes the HTML in the warning text' do
|
||||
expect(page).to have_content(
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ describe('container Protection Rule Form', () => {
|
|||
wrapper.findByRole('textbox', { name: /repository path pattern/i });
|
||||
const findMinimumAccessLevelForPushSelect = () =>
|
||||
wrapper.findByRole('combobox', { name: /minimum access level for push/i });
|
||||
const findMinimumAccessLevelForDeleteSelect = () =>
|
||||
wrapper.findByRole('combobox', { name: /minimum access level for delete/i });
|
||||
const findSubmitButton = () => wrapper.findByRole('button', { name: /add rule/i });
|
||||
|
||||
const mountComponent = ({ config, provide = defaultProvidedValues } = {}) => {
|
||||
|
|
@ -58,7 +56,7 @@ describe('container Protection Rule Form', () => {
|
|||
.findAll('option')
|
||||
.wrappers.map((option) => option.element.value);
|
||||
|
||||
it.each(['', 'MAINTAINER', 'OWNER', 'ADMIN'])(
|
||||
it.each(['MAINTAINER', 'OWNER', 'ADMIN'])(
|
||||
'includes the access level "%s" as an option',
|
||||
(accessLevel) => {
|
||||
mountComponent();
|
||||
|
|
@ -80,7 +78,6 @@ describe('container Protection Rule Form', () => {
|
|||
expect(findSubmitButton().props('disabled')).toBe(true);
|
||||
expect(findRepositoryPathPatternInput().attributes('disabled')).toBe('disabled');
|
||||
expect(findMinimumAccessLevelForPushSelect().attributes('disabled')).toBe('disabled');
|
||||
expect(findMinimumAccessLevelForDeleteSelect().attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('displays a loading spinner', () => {
|
||||
|
|
@ -165,29 +162,6 @@ describe('container Protection Rule Form', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('dispatches correct apollo mutation when no minimumAccessLevelForPush is selected', async () => {
|
||||
const mutationResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createContainerProtectionRuleMutationPayload());
|
||||
|
||||
mountComponentWithApollo({ mutationResolver });
|
||||
|
||||
await findRepositoryPathPatternInput().setValue(
|
||||
createContainerProtectionRuleMutationInput.repositoryPathPattern,
|
||||
);
|
||||
await findMinimumAccessLevelForPushSelect().setValue('');
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(mutationResolver).toHaveBeenCalledWith({
|
||||
input: {
|
||||
projectPath: 'path',
|
||||
...createContainerProtectionRuleMutationInput,
|
||||
minimumAccessLevelForPush: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('emits event "submit" when apollo mutation successful', async () => {
|
||||
const mutationResolver = jest
|
||||
.fn()
|
||||
|
|
|
|||
|
|
@ -113,28 +113,10 @@ describe('Container protection rules project settings', () => {
|
|||
(protectionRule, i) => {
|
||||
expect(findTableRowCell(i, 0).text()).toBe(protectionRule.repositoryPathPattern);
|
||||
expect(findTableRowCellComboboxSelectedOption(i, 1).text).toBe('Maintainer');
|
||||
expect(findTableRowCellComboboxSelectedOption(i, 2).text).toBe('Maintainer');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('renders table with container protection rule with blank minimumAccessLevelForDelete', async () => {
|
||||
const containerProtectionRuleQueryResolver = jest.fn().mockResolvedValue(
|
||||
containerProtectionRuleQueryPayload({
|
||||
nodes: [{ ...containerProtectionRulesData[0], minimumAccessLevelForDelete: null }],
|
||||
}),
|
||||
);
|
||||
createComponent({ containerProtectionRuleQueryResolver });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTableRowCell(0, 0).text()).toBe(
|
||||
containerProtectionRulesData[0].repositoryPathPattern,
|
||||
);
|
||||
expect(findTableRowCellComboboxSelectedOption(0, 1).text).toBe('Maintainer');
|
||||
expect(findTableRowCellComboboxSelectedOption(0, 2).text).toBe('Developer (default)');
|
||||
});
|
||||
|
||||
it('displays table in busy state and shows loading icon inside table', async () => {
|
||||
createComponent();
|
||||
|
||||
|
|
@ -298,9 +280,8 @@ describe('Container protection rules project settings', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
comboboxName | minimumAccessLevelAttribute
|
||||
${'Minimum access level for push'} | ${'minimumAccessLevelForPush'}
|
||||
${'Minimum access level for delete'} | ${'minimumAccessLevelForDelete'}
|
||||
comboboxName | minimumAccessLevelAttribute
|
||||
${'Minimum access level for push'} | ${'minimumAccessLevelForPush'}
|
||||
`(
|
||||
'column "$comboboxName" with selectbox (combobox)',
|
||||
({ comboboxName, minimumAccessLevelAttribute }) => {
|
||||
|
|
|
|||
|
|
@ -171,13 +171,11 @@ export const containerProtectionRulesData = [
|
|||
id: `gid://gitlab/ContainerRegistry::Protection::Rule/${i}`,
|
||||
repositoryPathPattern: `@flight/flight/maintainer-${i}-*`,
|
||||
minimumAccessLevelForPush: 'MAINTAINER',
|
||||
minimumAccessLevelForDelete: 'MAINTAINER',
|
||||
})),
|
||||
{
|
||||
id: 'gid://gitlab/ContainerRegistry::Protection::Rule/16',
|
||||
repositoryPathPattern: '@flight/flight/owner-16-*',
|
||||
minimumAccessLevelForPush: 'OWNER',
|
||||
minimumAccessLevelForDelete: 'OWNER',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -218,7 +216,6 @@ export const createContainerProtectionRuleMutationPayload = ({ override, errors
|
|||
export const createContainerProtectionRuleMutationInput = {
|
||||
repositoryPathPattern: `@flight/flight-maintainer-14-*`,
|
||||
minimumAccessLevelForPush: 'MAINTAINER',
|
||||
minimumAccessLevelForDelete: 'MAINTAINER',
|
||||
};
|
||||
|
||||
export const createContainerProtectionRuleMutationPayloadErrors = [
|
||||
|
|
|
|||
|
|
@ -108,6 +108,29 @@ describe('TodosApp', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('refetches todos when page becomes visible again', async () => {
|
||||
createComponent();
|
||||
|
||||
// Wait and account for initial query
|
||||
await waitForPromises();
|
||||
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
|
||||
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Make sure we don't refetch when document became hidden
|
||||
jest.spyOn(document, 'hidden', 'get').mockReturnValue(true);
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
await waitForPromises();
|
||||
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
|
||||
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Expect refetch when document becomes visible
|
||||
jest.spyOn(document, 'hidden', 'get').mockReturnValue(false);
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
await waitForPromises();
|
||||
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(2);
|
||||
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('passes the default status to the filter bar', () => {
|
||||
createComponent();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe GitlabSchema.types['Namespace'] do
|
|||
id name path full_name full_path achievements_path description description_html visibility
|
||||
lfs_enabled request_access_enabled projects root_storage_statistics shared_runners_setting
|
||||
timelog_categories achievements work_item pages_deployments import_source_users work_item_types
|
||||
sidebar
|
||||
sidebar work_item_description_templates
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::WorkItems::DescriptionTemplateType, feature_category: :portfolio_management do
|
||||
include GraphqlHelpers
|
||||
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[content name]
|
||||
|
||||
expected_fields.each do |field|
|
||||
expect(described_class).to have_graphql_field(field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Access, feature_category: :permissions do
|
||||
let_it_be(:member) { create(:group_member, :developer) }
|
||||
|
||||
describe '#role_description' do
|
||||
it 'returns the correct description of the access role' do
|
||||
role = described_class.option_descriptions[described_class::DEVELOPER]
|
||||
|
||||
expect(member.role_description).to eq(role)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -41,9 +41,23 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::ComponentUsage, feature_category: :p
|
|||
|
||||
it 'creates a component usage record' do
|
||||
expect { perform }.to change { Ci::Catalog::Resources::Components::Usage.count }.by(1)
|
||||
.and change { Ci::Catalog::Resources::Components::LastUsage.count }.by(1)
|
||||
end
|
||||
|
||||
context 'when component usage has already been recorded', :freeze_time do
|
||||
let!(:existing_last_usage) do
|
||||
create(:catalog_resource_component_last_usage,
|
||||
component: component, used_by_project_id: project.id, last_used_date: Time.current.to_date - 3.days)
|
||||
end
|
||||
|
||||
it 'updates the last_used_date for the existing last_usage record' do
|
||||
expect { step.perform! }.not_to change { Ci::Catalog::Resources::Components::LastUsage.count }
|
||||
|
||||
last_usage = Ci::Catalog::Resources::Components::LastUsage.find_by(component: component,
|
||||
used_by_project_id: project.id)
|
||||
expect(last_usage.last_used_date).to eq(Time.current.to_date)
|
||||
end
|
||||
|
||||
it 'does not create a component usage record' do
|
||||
step.perform!
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::Partitioning::Time::BaseStrategy, feature_category: :database do
|
||||
let(:model) { class_double(ApplicationRecord, table_name: table_name) }
|
||||
let(:partitioning_key) { :created_at }
|
||||
let(:table_name) { :_test_partitioned_test }
|
||||
let(:base_strategy) { described_class.new(model, partitioning_key) }
|
||||
|
||||
describe '#current_partitions' do
|
||||
subject(:current_partitions) { base_strategy.current_partitions }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { current_partitions }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#missing_partitions' do
|
||||
subject(:missing_partitions) { base_strategy.missing_partitions }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { missing_partitions }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#extra_partitions' do
|
||||
subject(:extra_partitions) { base_strategy.extra_partitions }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { extra_partitions }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#desired_partitions' do
|
||||
subject(:desired_partitions) { base_strategy.desired_partitions }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { desired_partitions }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#relevant_range' do
|
||||
subject(:relevant_range) { base_strategy.relevant_range }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { relevant_range }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#oldest_active_date' do
|
||||
subject(:oldest_active_date) { base_strategy.oldest_active_date }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { oldest_active_date }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#partition_name' do
|
||||
let(:from) { Date.current }
|
||||
|
||||
subject(:partition_name) { base_strategy.partition_name(from) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { partition_name }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#after_adding_partitions' do
|
||||
subject(:after_adding_partitions) { base_strategy.after_adding_partitions }
|
||||
|
||||
it 'does nothing' do
|
||||
expect { after_adding_partitions }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_and_fix' do
|
||||
subject(:validate_and_fix) { base_strategy.validate_and_fix }
|
||||
|
||||
it 'does nothing' do
|
||||
expect { validate_and_fix }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
# These specs are copying the requirements laid out by the original adapter
|
||||
# spec: https://github.com/relatel/ruby-fogbugz/blob/master/spec/adapters/xml/crack_spec.rb
|
||||
RSpec.describe Gitlab::FogbugzImport::XmlAdapter, feature_category: :importers do
|
||||
let(:xml) { nil }
|
||||
|
||||
subject(:parsed_xml) { described_class.parse(xml) }
|
||||
|
||||
context 'when parsing an XML response' do
|
||||
let(:xml) do
|
||||
<<~XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<response>
|
||||
<case>
|
||||
<ixBug>1234</ixBug>
|
||||
<sTitle>Sample Bug</sTitle>
|
||||
</case>
|
||||
</response>
|
||||
XML
|
||||
end
|
||||
|
||||
it { is_expected.to eq({ 'case' => { 'ixBug' => '1234', 'sTitle' => 'Sample Bug' } }) }
|
||||
end
|
||||
|
||||
context 'when given an HTML response' do
|
||||
let(:xml) do
|
||||
<<~HTML
|
||||
<html lang="en">
|
||||
<head><title>Object moved</title></head>
|
||||
<body><h2>Object moved to <a href="/new-location">here</a>.</h2></body>
|
||||
</html>
|
||||
HTML
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when parsing invalid XML' do
|
||||
let(:xml) { "hold on, this isn't XML at all!" }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when the XML body is too large' do
|
||||
let(:xml) { instance_double(String, bytesize: described_class::MAX_ALLOWED_BYTES + 1) }
|
||||
|
||||
it 'raises a ResponseTooLargeError' do
|
||||
expect { parsed_xml }.to raise_error(described_class::ResponseTooLargeError, /XML exceeds permitted size/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the XML body is too complex' do
|
||||
before do
|
||||
stub_const("#{described_class}::MAX_ALLOWED_OBJECTS", 3)
|
||||
end
|
||||
|
||||
let(:xml) { '<one><two><three><four></four></three></two></one>' }
|
||||
|
||||
it 'raises a ResponseTooLargeError' do
|
||||
expect { parsed_xml }.to raise_error(described_class::ResponseTooLargeError, /XML exceeds permitted complexity/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -70,6 +70,18 @@ RSpec.describe Gitlab::Import::PlaceholderUserCreator, feature_category: :import
|
|||
end
|
||||
end
|
||||
|
||||
context 'when an existing namespace conflicts with the placeholder user namespace' do
|
||||
before do
|
||||
create(:group, path: 'aprycontributor_placeholder_user_1')
|
||||
end
|
||||
|
||||
it 'creates a placeholder with a username that avoids the conflict' do
|
||||
placeholder_user1 = service.execute
|
||||
|
||||
expect(placeholder_user1.username).to eq('aprycontributor_placeholder_user_2')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when generating a unique email address' do
|
||||
it 'validates against all stored email addresses' do
|
||||
allow(Zlib).to receive(:crc32).and_return(123)
|
||||
|
|
|
|||
|
|
@ -43,5 +43,38 @@ RSpec.describe Ci::Catalog::Resources::Components::LastUsage, type: :model, feat
|
|||
)
|
||||
end.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
|
||||
describe '.get_usage_for' do
|
||||
let_it_be(:used_by_project) { create(:project) }
|
||||
|
||||
context 'when no record exists' do
|
||||
it 'initializes a new record' do
|
||||
last_usage = described_class.get_usage_for(component, used_by_project)
|
||||
|
||||
expect(last_usage).to be_a_new_record
|
||||
expect(last_usage.component).to eq(component)
|
||||
expect(last_usage.catalog_resource).to eq(component.catalog_resource)
|
||||
expect(last_usage.component_project).to eq(component.project)
|
||||
expect(last_usage.used_by_project_id).to eq(used_by_project.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a record exists' do
|
||||
let!(:existing_record) do
|
||||
create(:catalog_resource_component_last_usage,
|
||||
component: component,
|
||||
catalog_resource: component.catalog_resource,
|
||||
component_project: component.project,
|
||||
used_by_project_id: used_by_project.id)
|
||||
end
|
||||
|
||||
it 'returns the existing record' do
|
||||
last_usage = described_class.get_usage_for(component, used_by_project)
|
||||
|
||||
expect(last_usage).not_to be_a_new_record
|
||||
expect(last_usage).to eq(existing_record)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,32 +19,21 @@ RSpec.describe Clusters::Agents::Authorizations::UserAccess::GroupAuthorization,
|
|||
|
||||
subject { described_class.for_user(user) }
|
||||
|
||||
where(:user_role, :expected_access_level) do
|
||||
:guest | nil
|
||||
:reporter | nil
|
||||
:developer | Gitlab::Access::DEVELOPER
|
||||
:maintainer | Gitlab::Access::MAINTAINER
|
||||
:owner | Gitlab::Access::OWNER
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
group.add_member(user, user_role)
|
||||
context 'when user is member' do
|
||||
where(:user_role, :expected_access_level) do
|
||||
:guest | nil
|
||||
:reporter | nil
|
||||
:developer | Gitlab::Access::DEVELOPER
|
||||
:maintainer | Gitlab::Access::MAINTAINER
|
||||
:owner | Gitlab::Access::OWNER
|
||||
end
|
||||
|
||||
it 'returns the expected result' do
|
||||
if expected_access_level
|
||||
expect(subject).to contain_exactly(authorization)
|
||||
expect(subject.first.access_level).to eq(expected_access_level)
|
||||
else
|
||||
expect(subject).to be_empty
|
||||
with_them do
|
||||
before do
|
||||
group.add_member(user, user_role)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authorization belongs to sub-group' do
|
||||
let!(:authorization) { create(:agent_user_access_group_authorization, group: subgroup) }
|
||||
|
||||
it 'respects the role inheritance' do
|
||||
it 'returns the expected result' do
|
||||
if expected_access_level
|
||||
expect(subject).to contain_exactly(authorization)
|
||||
expect(subject.first.access_level).to eq(expected_access_level)
|
||||
|
|
@ -53,14 +42,63 @@ RSpec.describe Clusters::Agents::Authorizations::UserAccess::GroupAuthorization,
|
|||
end
|
||||
end
|
||||
|
||||
it 'respects the role override' do
|
||||
subgroup.add_member(user, :owner)
|
||||
context 'when authorization belongs to sub-group' do
|
||||
let!(:authorization) { create(:agent_user_access_group_authorization, group: subgroup) }
|
||||
|
||||
expect(subject).to contain_exactly(authorization)
|
||||
expect(subject.first.access_level).to eq(Gitlab::Access::OWNER)
|
||||
it 'respects the role inheritance' do
|
||||
if expected_access_level
|
||||
expect(subject).to contain_exactly(authorization)
|
||||
expect(subject.first.access_level).to eq(expected_access_level)
|
||||
else
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
it 'respects the role override' do
|
||||
subgroup.add_member(user, :owner)
|
||||
|
||||
expect(subject).to contain_exactly(authorization)
|
||||
expect(subject.first.access_level).to eq(Gitlab::Access::OWNER)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'does not yield an authorization' do
|
||||
it { expect(subject).to be_empty }
|
||||
end
|
||||
|
||||
context 'when user is blocked' do
|
||||
let(:user) { create(:user, :blocked) }
|
||||
|
||||
before do
|
||||
group.add_member(user, Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not yield an authorization'
|
||||
end
|
||||
|
||||
context 'when user is banned' do
|
||||
let(:user) { create(:user, :banned) }
|
||||
|
||||
before do
|
||||
group.add_member(user, Gitlab::Access::MAINTAINER)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not yield an authorization'
|
||||
end
|
||||
|
||||
context 'when user requested access' do
|
||||
let!(:invited) { create(:group_member, :access_request, group: group, user: user) }
|
||||
|
||||
it_behaves_like 'does not yield an authorization'
|
||||
end
|
||||
|
||||
context 'when user is awaiting' do
|
||||
let!(:invited) { create(:group_member, :awaiting, group: group, user: user) }
|
||||
|
||||
it_behaves_like 'does not yield an authorization'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#config_project' do
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@ RSpec.describe MemberPresenter, feature_category: :groups_and_projects do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#member_role_description' do
|
||||
it 'returns the correct role description' do
|
||||
description = Gitlab::Access.option_descriptions[Gitlab::Access::REPORTER]
|
||||
|
||||
expect(presenter.member_role_description).to eq(description)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#role_type' do
|
||||
it "returns 'default'" do
|
||||
expect(presenter.role_type).to eq('default')
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ RSpec.describe MemberEntity, feature_category: :groups_and_projects do
|
|||
expect(entity_hash[:can_remove]).to be(true)
|
||||
end
|
||||
|
||||
describe '#access_level' do
|
||||
it 'correctly exposes `string_value`' do
|
||||
expect(entity_hash[:access_level][:string_value]).to eq(member.human_access_with_none)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when is_source_accessible_to_current_user is true' do
|
||||
before do
|
||||
allow(member).to receive(:is_source_accessible_to_current_user).and_return(true)
|
||||
|
|
|
|||
|
|
@ -11,19 +11,36 @@ RSpec.describe Ci::Components::Usages::CreateService, feature_category: :pipelin
|
|||
describe '#execute' do
|
||||
subject(:execute) { service.execute }
|
||||
|
||||
it 'creates a usage record', :aggregate_failures do
|
||||
it 'creates a usage record and updates last_usage', :aggregate_failures do
|
||||
expect { execute }.to change { Ci::Catalog::Resources::Components::Usage.count }.by(1)
|
||||
.and change { Ci::Catalog::Resources::Components::LastUsage.count }.by(1)
|
||||
expect(execute).to be_success
|
||||
expect(execute.message).to eq('Usage recorded')
|
||||
|
||||
usage = Ci::Catalog::Resources::Components::Usage.find_by(component: component)
|
||||
last_usage = Ci::Catalog::Resources::Components::LastUsage.find_by(component: component,
|
||||
used_by_project_id: project.id)
|
||||
|
||||
expect(usage.catalog_resource).to eq(component.catalog_resource)
|
||||
expect(usage.project).to eq(component.project)
|
||||
expect(usage.used_by_project_id).to eq(project.id)
|
||||
expect(last_usage.last_used_date).to be_present
|
||||
end
|
||||
|
||||
context 'when usage has already been recorded', :freeze_time do
|
||||
let!(:existing_last_usage) do
|
||||
create(:catalog_resource_component_last_usage,
|
||||
component: component, used_by_project_id: project.id, last_used_date: Time.current.to_date - 3.days)
|
||||
end
|
||||
|
||||
it 'updates the last_used_date for the existing last_usage record' do
|
||||
expect { execute }.not_to change { Ci::Catalog::Resources::Components::LastUsage.count }
|
||||
|
||||
last_usage = Ci::Catalog::Resources::Components::LastUsage.find_by(component: component,
|
||||
used_by_project_id: project.id)
|
||||
expect(last_usage.last_used_date).to eq(Time.current.to_date)
|
||||
end
|
||||
|
||||
it 'does not create a usage record' do
|
||||
service.execute
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# - current_user
|
||||
# - error_message # optional
|
||||
|
||||
RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:, graphql: false|
|
||||
RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:, graphql: false, with_redirect: false|
|
||||
let(:error_message) { _('This endpoint has been requested too many times. Try again later.') }
|
||||
|
||||
context 'when rate limiter enabled', :freeze_time, :clean_gitlab_redis_rate_limiting do
|
||||
|
|
@ -39,6 +39,9 @@ RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:, graphql: fals
|
|||
|
||||
if graphql
|
||||
expect_graphql_errors_to_include(error_message)
|
||||
elsif with_redirect
|
||||
expect(response).to be_redirect
|
||||
expect(flash[:alert]).to eq(error_message)
|
||||
else
|
||||
expect(response).to have_gitlab_http_status(:too_many_requests)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,3 +17,34 @@ RSpec.shared_examples 'Secure OAuth Authorizations' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'Secure Device OAuth Authorizations' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'when authorize page is rendered' do
|
||||
it 'asks user to authorize the device' do
|
||||
expect(page).to have_text "Authorize device to access to your GitLab account"
|
||||
within_testid('authorization-button') do
|
||||
expect(page).to have_content(format(_('Authorize')))
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not render authorize button with id' do
|
||||
expect(find_by_testid('authorization-button')[:id].nil?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when confirmation page is rendered' do
|
||||
before do
|
||||
find_by_testid('authorization-button').click
|
||||
end
|
||||
|
||||
it 'renders confirmatoin button without id' do
|
||||
within_testid('authorization-button') do
|
||||
expect(page).to have_content(format(_('Confirm')))
|
||||
end
|
||||
|
||||
expect(find_by_testid('authorization-button')[:id].nil?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue