Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bc5c433ff1
commit
2f195ead02
|
|
@ -3086,7 +3086,6 @@ Gitlab/BoundedContexts:
|
|||
- 'ee/app/services/dependencies/export_serializers/project_dependencies_service.rb'
|
||||
- 'ee/app/services/dependencies/export_serializers/sbom/pipeline_service.rb'
|
||||
- 'ee/app/services/dependencies/export_service.rb'
|
||||
- 'ee/app/services/dependencies/fetch_export_service.rb'
|
||||
- 'ee/app/services/deployments/approval_service.rb'
|
||||
- 'ee/app/services/deployments/auto_rollback_service.rb'
|
||||
- 'ee/app/services/dora/aggregate_metrics_service.rb'
|
||||
|
|
|
|||
|
|
@ -8,5 +8,8 @@ MinAlertLevel = suggestion
|
|||
[*.md]
|
||||
BasedOnStyles = gitlab_base, gitlab_docs
|
||||
|
||||
# Disable the front matter check until we migrate titles to Hugo format
|
||||
gitlab_docs.FrontMatter = NO
|
||||
|
||||
# Ignore SVG markup
|
||||
TokenIgnores = (\*\*\{\w*\}\*\*)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
dd5a7ec67062bdb145fa2f66385a9bea72ccce1f
|
||||
36ef4e1bd95e15e6a369bf0bed384ddc7f06caa5
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
c2b97e286ae33847fcfb8c19fc03c156a5ee925d
|
||||
84549df179cdbbafc15f7073206f9e20773d0fee
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ export default {
|
|||
@click="toggleSubscribed"
|
||||
>
|
||||
<gl-animated-notification-icon
|
||||
:class="{ '!gl-text-blue-500': subscribed }"
|
||||
:class="{ '!gl-text-status-info': subscribed }"
|
||||
:is-on="!subscribed"
|
||||
/>
|
||||
</gl-button>
|
||||
|
|
@ -239,14 +239,14 @@ export default {
|
|||
v-if="!isMergeRequest"
|
||||
ref="tooltip"
|
||||
v-gl-tooltip.left.viewport
|
||||
category="secondary"
|
||||
category="tertiary"
|
||||
data-testid="subscribe-button"
|
||||
:title="notificationTooltip"
|
||||
class="sidebar-collapsed-icon sidebar-collapsed-container !gl-rounded-none !gl-border-0"
|
||||
@click="toggleSubscribed"
|
||||
>
|
||||
<gl-animated-notification-icon
|
||||
:class="{ '!gl-text-blue-500': subscribed }"
|
||||
:class="{ '!gl-text-status-info': subscribed }"
|
||||
:is-on="!subscribed"
|
||||
/>
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -143,6 +143,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
canReportSpam: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isConfidential: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -311,6 +316,10 @@ export default {
|
|||
showDropdownTooltip() {
|
||||
return !this.isDropdownVisible ? this.$options.i18n.moreActions : '';
|
||||
},
|
||||
submitAsSpamItem() {
|
||||
const href = this.workItemWebUrl.replaceAll('work_items', 'issues').concat('/mark_as_spam');
|
||||
return { text: __('Submit as spam'), href };
|
||||
},
|
||||
isAuthor() {
|
||||
return this.workItemAuthorId === window.gon.current_user_id;
|
||||
},
|
||||
|
|
@ -600,6 +609,7 @@ export default {
|
|||
</gl-disclosure-dropdown-item>
|
||||
|
||||
<gl-dropdown-divider />
|
||||
|
||||
<gl-disclosure-dropdown-item
|
||||
v-if="!isAuthor"
|
||||
:data-testid="$options.reportAbuseActionTestId"
|
||||
|
|
@ -608,6 +618,12 @@ export default {
|
|||
<template #list-item>{{ $options.i18n.reportAbuse }}</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
|
||||
<gl-disclosure-dropdown-item
|
||||
v-if="glFeatures.workItemsBeta && canReportSpam"
|
||||
:item="submitAsSpamItem"
|
||||
data-testid="submit-as-spam-item"
|
||||
/>
|
||||
|
||||
<template v-if="canDelete">
|
||||
<gl-disclosure-dropdown-item
|
||||
:data-testid="$options.deleteActionTestId"
|
||||
|
|
|
|||
|
|
@ -309,6 +309,9 @@ export default {
|
|||
canDelete() {
|
||||
return this.workItem.userPermissions?.deleteWorkItem;
|
||||
},
|
||||
canReportSpam() {
|
||||
return this.workItem.userPermissions?.reportSpam;
|
||||
},
|
||||
canSetWorkItemMetadata() {
|
||||
return this.workItem.userPermissions?.setWorkItemMetadata;
|
||||
},
|
||||
|
|
@ -867,6 +870,7 @@ export default {
|
|||
:work-item-type-id="workItemTypeId"
|
||||
:work-item-iid="iid"
|
||||
:can-delete="canDelete"
|
||||
:can-report-spam="canReportSpam"
|
||||
:can-update="canUpdate"
|
||||
:is-confidential="workItem.confidential"
|
||||
:is-discussion-locked="isDiscussionLocked"
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ export default {
|
|||
canDelete() {
|
||||
return this.workItem.userPermissions?.deleteWorkItem;
|
||||
},
|
||||
canReportSpam() {
|
||||
return this.workItem.userPermissions?.reportSpam;
|
||||
},
|
||||
isDiscussionLocked() {
|
||||
return this.workItem.widgets?.find(isNotesWidget)?.discussionLocked;
|
||||
},
|
||||
|
|
@ -188,6 +191,7 @@ export default {
|
|||
:work-item-type="workItemType"
|
||||
:work-item-type-id="workItemTypeId"
|
||||
:can-delete="canDelete"
|
||||
:can-report-spam="canReportSpam"
|
||||
:can-update="canUpdate"
|
||||
:is-confidential="workItem.confidential"
|
||||
:is-discussion-locked="isDiscussionLocked"
|
||||
|
|
|
|||
|
|
@ -594,6 +594,7 @@ export const setNewWorkItemCache = async (
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
widgets,
|
||||
|
|
@ -613,6 +614,7 @@ export const optimisticUserPermissions = {
|
|||
createNote: false,
|
||||
adminWorkItemLink: false,
|
||||
markNoteAsInternal: false,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ fragment WorkItem on WorkItem {
|
|||
createNote
|
||||
adminWorkItemLink
|
||||
markNoteAsInternal
|
||||
reportSpam
|
||||
}
|
||||
mockWidgets @client {
|
||||
type
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module Agents
|
||||
class ManagedResource < ApplicationRecord
|
||||
self.table_name = 'clusters_managed_resources'
|
||||
|
||||
belongs_to :build, class_name: 'Ci::Build'
|
||||
belongs_to :cluster_agent, class_name: 'Clusters::Agent'
|
||||
belongs_to :project
|
||||
belongs_to :environment
|
||||
|
||||
validates :template_name, length: { maximum: 1024 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -32,6 +32,10 @@
|
|||
"description": "Setting to understand if a user is joining a project or not during onboarding",
|
||||
"type": "boolean"
|
||||
},
|
||||
"setup_for_company": {
|
||||
"description": "Setting to understand if a user is registering their gitlab account for their company",
|
||||
"type": "boolean"
|
||||
},
|
||||
"registration_objective": {
|
||||
"description": "Goal of registration collected during onboarding",
|
||||
"type": "integer",
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: composite_identity
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/468370
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173006
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/506473
|
||||
milestone: "17.7"
|
||||
group: group::authentication
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -201,6 +201,10 @@ ci_variables:
|
|||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
clusters_managed_resources:
|
||||
- table: ci_builds
|
||||
column: build_id
|
||||
on_delete: async_delete
|
||||
country_access_logs:
|
||||
- table: users
|
||||
column: user_id
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
- title: "Secret detection analyzer doesn't run as root user by default"
|
||||
removal_milestone: "18.0"
|
||||
announcement_milestone: "17.9"
|
||||
breaking_change: true
|
||||
window: 3
|
||||
reporter: abellucci
|
||||
stage: application_security_testing
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/476160
|
||||
# Use the impact calculator https://gitlab-com.gitlab.io/gl-infra/breaking-change-impact-calculator/?
|
||||
impact: low # Can be one of: [critical, high, medium, low]
|
||||
scope: instance # Can be one or a combination of: [instance, group, project]
|
||||
resolution_role: Admin # Can be one of: [Admin, Owner, Maintainer, Developer]
|
||||
manual_task: false # Can be true or false. Use this to denote whether a resolution action must be performed manually (true), or if it can be automated by using the API or other automation (false).
|
||||
body: | # (required) Don't change this line.
|
||||
From GitLab 18.0, the secret detection analyzer will no longer use the root user by default. You shouldn't experience any impact as a result of this change. However, you might experience issues if you use `before_script` or `after_script` to make changes to the image. GitLab doesn't support this use of `before_script` and `after_script`.
|
||||
tiers: ultimate
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
table_name: clusters_managed_resources
|
||||
classes:
|
||||
- Clusters::Agents::ManagedResource
|
||||
feature_categories:
|
||||
- deployment_management
|
||||
description: A managed resource for the GitLab managed Kubernetes resources
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178118
|
||||
milestone: '17.9'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateClustersManagedResources < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.9'
|
||||
|
||||
def change
|
||||
create_table :clusters_managed_resources do |t|
|
||||
t.references :build, index: { unique: true }, null: false
|
||||
t.references :project, null: false
|
||||
t.references :environment, null: false
|
||||
t.references :cluster_agent, null: false
|
||||
t.timestamps_with_timezone null: false
|
||||
t.integer :status, default: 0, limit: 2, null: false
|
||||
t.text :template_name, limit: 1024
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InsertForgottenActiveVersionedPagesDeploymentsLimitByNamespace < Gitlab::Database::Migration[2.2]
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
create_or_update_plan_limit('active_versioned_pages_deployments_limit_by_namespace',
|
||||
'ultimate_trial_paid_customer', 500)
|
||||
create_or_update_plan_limit('active_versioned_pages_deployments_limit_by_namespace',
|
||||
'opensource', 500)
|
||||
end
|
||||
|
||||
def down
|
||||
create_or_update_plan_limit('active_versioned_pages_deployments_limit_by_namespace',
|
||||
'ultimate_trial_paid_customer', 0)
|
||||
create_or_update_plan_limit('active_versioned_pages_deployments_limit_by_namespace',
|
||||
'opensource', 0)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddProjectFkToClustersManagedResources < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :clusters_managed_resources, :projects, column: :project_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :clusters_managed_resources, column: :project_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddEnvironmentFkToClustersManagedResources < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :clusters_managed_resources, :environments, column: :environment_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :clusters_managed_resources, column: :environment_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddClusterAgentFkToClustersManagedResources < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.9'
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :clusters_managed_resources, :cluster_agents,
|
||||
column: :cluster_agent_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :clusters_managed_resources, column: :cluster_agent_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
14b2d34e19f6b910c28c7054880475f0836fa11d56c95092f9b68367b8833003
|
||||
|
|
@ -0,0 +1 @@
|
|||
406b476b5e44493e5fee0812f8e9595337bb28dfa244f22efa2a4d2f6406bd70
|
||||
|
|
@ -0,0 +1 @@
|
|||
13ce973bb0b6d08ab05bb48fc2ad09052cdde5218d9062100ec66927a49ba1db
|
||||
|
|
@ -0,0 +1 @@
|
|||
0771663b7fca1ad06744feedf7e897553e6d412b8d48ff80c5aafeb9999ff3c7
|
||||
|
|
@ -0,0 +1 @@
|
|||
7c919968f10a864ebdeb44d029d2e03957c190adab01c3422e2e7870f66ae5a8
|
||||
|
|
@ -11303,6 +11303,28 @@ CREATE SEQUENCE clusters_kubernetes_namespaces_id_seq
|
|||
|
||||
ALTER SEQUENCE clusters_kubernetes_namespaces_id_seq OWNED BY clusters_kubernetes_namespaces.id;
|
||||
|
||||
CREATE TABLE clusters_managed_resources (
|
||||
id bigint NOT NULL,
|
||||
build_id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
environment_id bigint NOT NULL,
|
||||
cluster_agent_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
status smallint DEFAULT 0 NOT NULL,
|
||||
template_name text,
|
||||
CONSTRAINT check_4f81a98847 CHECK ((char_length(template_name) <= 1024))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE clusters_managed_resources_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE clusters_managed_resources_id_seq OWNED BY clusters_managed_resources.id;
|
||||
|
||||
CREATE TABLE commit_user_mentions (
|
||||
id bigint NOT NULL,
|
||||
mentioned_users_ids bigint[],
|
||||
|
|
@ -24560,6 +24582,8 @@ ALTER TABLE ONLY clusters ALTER COLUMN id SET DEFAULT nextval('clusters_id_seq':
|
|||
|
||||
ALTER TABLE ONLY clusters_kubernetes_namespaces ALTER COLUMN id SET DEFAULT nextval('clusters_kubernetes_namespaces_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY clusters_managed_resources ALTER COLUMN id SET DEFAULT nextval('clusters_managed_resources_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY commit_user_mentions ALTER COLUMN id SET DEFAULT nextval('commit_user_mentions_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY compliance_framework_security_policies ALTER COLUMN id SET DEFAULT nextval('compliance_framework_security_policies_id_seq'::regclass);
|
||||
|
|
@ -26776,6 +26800,9 @@ ALTER TABLE ONLY clusters_integration_prometheus
|
|||
ALTER TABLE ONLY clusters_kubernetes_namespaces
|
||||
ADD CONSTRAINT clusters_kubernetes_namespaces_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY clusters_managed_resources
|
||||
ADD CONSTRAINT clusters_managed_resources_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY clusters
|
||||
ADD CONSTRAINT clusters_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -31566,6 +31593,14 @@ CREATE INDEX index_clusters_kubernetes_namespaces_on_environment_id ON clusters_
|
|||
|
||||
CREATE INDEX index_clusters_kubernetes_namespaces_on_project_id ON clusters_kubernetes_namespaces USING btree (project_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_clusters_managed_resources_on_build_id ON clusters_managed_resources USING btree (build_id);
|
||||
|
||||
CREATE INDEX index_clusters_managed_resources_on_cluster_agent_id ON clusters_managed_resources USING btree (cluster_agent_id);
|
||||
|
||||
CREATE INDEX index_clusters_managed_resources_on_environment_id ON clusters_managed_resources USING btree (environment_id);
|
||||
|
||||
CREATE INDEX index_clusters_managed_resources_on_project_id ON clusters_managed_resources USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_clusters_on_enabled_and_provider_type_and_id ON clusters USING btree (enabled, provider_type, id);
|
||||
|
||||
CREATE INDEX index_clusters_on_enabled_cluster_type_id_and_created_at ON clusters USING btree (enabled, cluster_type, id, created_at);
|
||||
|
|
@ -37633,6 +37668,9 @@ ALTER TABLE ONLY ai_settings
|
|||
ALTER TABLE ONLY merge_requests
|
||||
ADD CONSTRAINT fk_06067f5644 FOREIGN KEY (latest_merge_request_diff_id) REFERENCES merge_request_diffs(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY clusters_managed_resources
|
||||
ADD CONSTRAINT fk_068dba90c3 FOREIGN KEY (cluster_agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY sbom_occurrences_vulnerabilities
|
||||
ADD CONSTRAINT fk_07b81e3a81 FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -38662,6 +38700,9 @@ ALTER TABLE ONLY agent_activity_events
|
|||
ALTER TABLE ONLY issues
|
||||
ADD CONSTRAINT fk_9c4516d665 FOREIGN KEY (duplicated_to_id) REFERENCES issues(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY clusters_managed_resources
|
||||
ADD CONSTRAINT fk_9c7b561962 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY packages_conan_recipe_revisions
|
||||
ADD CONSTRAINT fk_9cdec8a86b FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -39379,6 +39420,9 @@ ALTER TABLE ONLY application_settings
|
|||
ALTER TABLE ONLY issuable_severities
|
||||
ADD CONSTRAINT fk_f9df19ecb6 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY clusters_managed_resources
|
||||
ADD CONSTRAINT fk_fad3c3b2e2 FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE p_ci_stages
|
||||
ADD CONSTRAINT fk_fb57e6cc56_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES p_ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
extends: script
|
||||
message: "Front matter must have valid 'title' and be closed."
|
||||
link: https://docs.gitlab.com/ee/development/documentation/metadata/
|
||||
level: error
|
||||
scope: raw
|
||||
script: |
|
||||
text := import("text")
|
||||
matches := []
|
||||
|
||||
// Initialize variables
|
||||
frontmatterDelimiterCount := 0
|
||||
frontmatter := ""
|
||||
hasError := false
|
||||
|
||||
// Check if frontmatter exists
|
||||
if !text.re_match("^---\n", scope) {
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if !hasError {
|
||||
for line in text.split(scope, "\n") {
|
||||
if frontmatterDelimiterCount == 1 {
|
||||
frontmatter += line + "\n"
|
||||
}
|
||||
if frontmatterDelimiterCount == 2 {
|
||||
break
|
||||
}
|
||||
if text.re_match("^---", line) {
|
||||
frontmatterDelimiterCount++
|
||||
start := text.index(scope, line)
|
||||
matches = append(matches, {begin: start, end: start + len(line)})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unclosed frontmatter
|
||||
if frontmatterDelimiterCount != 2 {
|
||||
hasError = true
|
||||
}
|
||||
|
||||
// First check if we have a title key at all
|
||||
hasTitleKey := text.re_match("(?m)^[tT]itle:", frontmatter)
|
||||
// Then check if it has content (anything but whitespace) after the colon
|
||||
hasValidTitle := text.re_match("(?m)^[tT]itle:[^\\n]*[^\\s][^\\n]*$", frontmatter)
|
||||
|
||||
if !hasError && (!hasTitleKey || !hasValidTitle) {
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasError {
|
||||
matches = []
|
||||
}
|
||||
|
|
@ -7,12 +7,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
# Guest users
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** GitLab.com, GitLab Self-Managed, GitLab Dedicated
|
||||
|
||||
Users assigned the Guest role have limited access and capabilities compared to other user roles. Their permissions are restricted and are designed to provide basic visibility and interaction without compromising sensitive project data. For more information, see [Roles and permissions](../user/permissions.md).
|
||||
|
||||
In GitLab Ultimate, the Guest role is free and does not count towards the license seat count. You can assign the Guest role to users [through the API](../api/members.md#add-a-member-to-a-group-or-project) or the GitLab UI.
|
||||
In GitLab Free and Premium, Guest users count towards the license seat usage.
|
||||
|
||||
## Unlimited seat usage
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
|
||||
In GitLab Ultimate, users with the Guest role do not count towards the license seat usage. You can add Guest users to your GitLab instance without impacting your billable seats.
|
||||
|
||||
While Guest users generally have limited access, you can configure a [custom role](../user/custom_roles.md) that includes the [`View repository code` permission](../user/custom_roles/abilities.md#source-code-management) to allow Guests to read code in your repositories. Adding any other permissions causes the role to occupy a billable seat.
|
||||
|
||||
## Assign Guest role to users
|
||||
|
||||
|
|
@ -20,7 +29,7 @@ Prerequisites:
|
|||
|
||||
- You must have at least the Maintainer role.
|
||||
|
||||
You can assign the Guest role to a current member of a group or project, or assign this role when creating a new member.
|
||||
You can assign the Guest role to a current member of a group or project, or assign this role when creating a new member. You can do this [through the API](../api/members.md#add-a-member-to-a-group-or-project) or the GitLab UI.
|
||||
|
||||
To assign the Guest role to a current group or project member:
|
||||
|
||||
|
|
|
|||
|
|
@ -86,4 +86,5 @@ This window takes place on May 5 - 7, 2025 from 09:00 UTC to 22:00 UTC.
|
|||
| [Updated tooling to release CI/CD components to the Catalog](https://gitlab.com/groups/gitlab-org/-/epics/12788) | High | Verify | Instance |
|
||||
| [Increased default security for use of pipeline variables](https://gitlab.com/gitlab-org/gitlab/-/issues/502382) | Medium | Verify | Project |
|
||||
| [Amazon S3 Signature Version 2](https://gitlab.com/gitlab-org/container-registry/-/issues/1449) | Low | Package | Project |
|
||||
| [Secret detection analyzer doesn't run as root user by default](https://gitlab.com/gitlab-org/gitlab/-/issues/476160) | Low | Application_security_testing | Instance |
|
||||
| [Remove `previousStageJobsOrNeeds` from GraphQL](https://gitlab.com/gitlab-org/gitlab/-/issues/424417) | Low | Verify | Instance |
|
||||
|
|
|
|||
|
|
@ -1352,6 +1352,22 @@ If you need to use the cache when scanning a project, you can restore the previo
|
|||
|
||||
<div class="deprecation breaking-change" data-milestone="18.0">
|
||||
|
||||
### Secret detection analyzer doesn't run as root user by default
|
||||
|
||||
<div class="deprecation-notes">
|
||||
|
||||
- Announced in GitLab <span class="milestone">17.9</span>
|
||||
- Removal in GitLab <span class="milestone">18.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
|
||||
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/476160).
|
||||
|
||||
</div>
|
||||
|
||||
From GitLab 18.0, the secret detection analyzer will no longer use the root user by default. You shouldn't experience any impact as a result of this change. However, you might experience issues if you use `before_script` or `after_script` to make changes to the image. GitLab doesn't support this use of `before_script` and `after_script`.
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation breaking-change" data-milestone="18.0">
|
||||
|
||||
### Support for REST API endpoints that reset runner registration tokens
|
||||
|
||||
<div class="deprecation-notes">
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def composite?
|
||||
@user.has_composite_identity? && composite_identity_enabled?
|
||||
@user.has_composite_identity?
|
||||
end
|
||||
|
||||
def sidekiq_link!(job)
|
||||
|
|
@ -93,7 +93,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def link!(scope_user)
|
||||
return self unless composite_identity_enabled?
|
||||
return self unless scope_user
|
||||
|
||||
##
|
||||
|
|
@ -128,10 +127,6 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def composite_identity_enabled?
|
||||
Feature.enabled?(:composite_identity, @user)
|
||||
end
|
||||
|
||||
def scoped_user_id
|
||||
scoped_user.id
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :clusters_managed_resource, class: 'Clusters::Agents::ManagedResource' do
|
||||
project
|
||||
environment
|
||||
association :cluster_agent
|
||||
association :build
|
||||
end
|
||||
end
|
||||
|
|
@ -79,6 +79,26 @@ RSpec.describe 'Work item detail', :js, feature_category: :team_planning do
|
|||
it_behaves_like 'work items change type', 'Issue', '[data-testid="issue-type-issue-icon"]'
|
||||
end
|
||||
|
||||
context 'for signed in admin' do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
|
||||
context 'with akismet integration' do
|
||||
let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: work_item) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(admin)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_application_setting(akismet_enabled: true)
|
||||
sign_in(admin)
|
||||
visit work_items_path
|
||||
end
|
||||
|
||||
it_behaves_like 'work items submit as spam'
|
||||
end
|
||||
end
|
||||
|
||||
context 'for signed in owner' do
|
||||
before_all do
|
||||
project.add_owner(user)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ describe('WorkItemActions component', () => {
|
|||
const findCopyCreateNoteEmailButton = () =>
|
||||
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
|
||||
const findReportAbuseButton = () => wrapper.findByTestId(TEST_ID_REPORT_ABUSE);
|
||||
const findSubmitAsSpamItem = () => wrapper.findByTestId('submit-as-spam-item');
|
||||
const findNewRelatedItemButton = () => wrapper.findByTestId(TEST_ID_NEW_RELATED_WORK_ITEM);
|
||||
const findChangeTypeButton = () => wrapper.findByTestId(TEST_ID_CHANGE_TYPE_ACTION);
|
||||
const findReportAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
|
||||
|
|
@ -120,6 +121,7 @@ describe('WorkItemActions component', () => {
|
|||
const createComponent = ({
|
||||
canUpdate = true,
|
||||
canDelete = true,
|
||||
canReportSpam = true,
|
||||
hasOkrsFeature = true,
|
||||
isConfidential = false,
|
||||
isDiscussionLocked = false,
|
||||
|
|
@ -155,10 +157,11 @@ describe('WorkItemActions component', () => {
|
|||
fullPath: 'gitlab-org/gitlab-test',
|
||||
workItemId: 'gid://gitlab/WorkItem/1',
|
||||
workItemIid: '1',
|
||||
workItemWebUrl: 'web/url',
|
||||
workItemWebUrl: 'gitlab-org/gitlab-test/-/work_items/1',
|
||||
isGroup,
|
||||
canUpdate,
|
||||
canDelete,
|
||||
canReportSpam,
|
||||
isConfidential,
|
||||
isDiscussionLocked,
|
||||
subscribed,
|
||||
|
|
@ -258,6 +261,10 @@ describe('WorkItemActions component', () => {
|
|||
testId: TEST_ID_REPORT_ABUSE,
|
||||
text: 'Report abuse',
|
||||
},
|
||||
{
|
||||
testId: 'submit-as-spam-item',
|
||||
text: 'Submit as spam',
|
||||
},
|
||||
{
|
||||
testId: TEST_ID_DELETE_ACTION,
|
||||
text: 'Delete task',
|
||||
|
|
@ -612,6 +619,23 @@ describe('WorkItemActions component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('submit as spam item', () => {
|
||||
it('renders the "Submit as spam" action', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findSubmitAsSpamItem().props('item')).toEqual({
|
||||
href: 'gitlab-org/gitlab-test/-/issues/1/mark_as_spam',
|
||||
text: 'Submit as spam',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render the "Submit as spam" action when not allowed', () => {
|
||||
createComponent({ canReportSpam: false });
|
||||
|
||||
expect(findSubmitAsSpamItem().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('new related item', () => {
|
||||
it('passes related item data to create work item modal', () => {
|
||||
createComponent();
|
||||
|
|
@ -620,7 +644,7 @@ describe('WorkItemActions component', () => {
|
|||
id: 'gid://gitlab/WorkItem/1',
|
||||
reference: 'gitlab-org/gitlab-test#1',
|
||||
type: 'Task',
|
||||
webUrl: 'web/url',
|
||||
webUrl: 'gitlab-org/gitlab-test/-/work_items/1',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ export const workItemQueryResponse = {
|
|||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
widgets: [
|
||||
|
|
@ -342,6 +343,7 @@ export const updateWorkItemMutationResponse = {
|
|||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
reference: 'test-project-path#1',
|
||||
|
|
@ -478,6 +480,7 @@ export const convertWorkItemMutationResponse = {
|
|||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
reference: 'gitlab-org/gitlab-test#1',
|
||||
|
|
@ -1335,6 +1338,7 @@ export const workItemResponseFactory = ({
|
|||
canDelete = false,
|
||||
canCreateNote = false,
|
||||
adminParentLink = false,
|
||||
reportSpam = false,
|
||||
canAdminWorkItemLink = true,
|
||||
canMarkNoteAsInternal = true,
|
||||
notificationsWidgetPresent = true,
|
||||
|
|
@ -1425,6 +1429,7 @@ export const workItemResponseFactory = ({
|
|||
adminWorkItemLink: canAdminWorkItemLink,
|
||||
createNote: canCreateNote,
|
||||
markNoteAsInternal: canMarkNoteAsInternal,
|
||||
reportSpam,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
reference: 'test-project-path#1',
|
||||
|
|
@ -1818,6 +1823,7 @@ export const createWorkItemMutationResponse = {
|
|||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
reference: 'test-project-path#1',
|
||||
|
|
@ -1891,6 +1897,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
|
|||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
namespace: {
|
||||
|
|
@ -2326,6 +2333,7 @@ export const workItemHierarchyResponse = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -2391,6 +2399,7 @@ export const workItemObjectiveWithChild = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -2481,6 +2490,7 @@ export const workItemObjectiveWithoutChild = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -2534,6 +2544,7 @@ export const workItemHierarchyTreeEmptyResponse = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
confidential: false,
|
||||
|
|
@ -2797,6 +2808,7 @@ export const workItemHierarchyTreeResponse = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
confidential: false,
|
||||
|
|
@ -2841,6 +2853,7 @@ export const workItemHierarchyTreeSingleClosedItemResponse = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
confidential: false,
|
||||
|
|
@ -2978,6 +2991,7 @@ export const workItemObjectiveWithClosedChild = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -3044,6 +3058,7 @@ export const changeWorkItemParentMutationResponse = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
description: null,
|
||||
|
|
@ -5521,6 +5536,7 @@ export const createWorkItemQueryResponse = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
mockWidgets: [],
|
||||
|
|
@ -5811,6 +5827,7 @@ const mockUserPermissions = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
};
|
||||
|
||||
|
|
@ -5972,6 +5989,7 @@ export const workItemHierarchyNoChildrenTreeResponse = {
|
|||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
reportSpam: false,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
confidential: false,
|
||||
|
|
|
|||
|
|
@ -124,77 +124,59 @@ RSpec.describe Gitlab::Auth::Identity, :request_store, feature_category: :system
|
|||
end
|
||||
|
||||
describe '.link_from_web_request' do
|
||||
context 'when composite identity feature flag is enabled' do
|
||||
context 'when service_account has composite identity enforced' do
|
||||
before do
|
||||
allow(primary_user).to receive(:composite_identity_enforced).and_return(true)
|
||||
end
|
||||
context 'when service_account has composite identity enforced' do
|
||||
before do
|
||||
allow(primary_user).to receive(:composite_identity_enforced).and_return(true)
|
||||
end
|
||||
|
||||
it 'creates and links identity with scope user' do
|
||||
it 'creates and links identity with scope user' do
|
||||
identity = described_class.link_from_web_request(
|
||||
service_account: primary_user,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
|
||||
expect(identity.primary_user).to eq(primary_user)
|
||||
expect(identity.scoped_user).to eq(scoped_user)
|
||||
expect(identity).to be_linked
|
||||
end
|
||||
|
||||
context 'when trying to link different scoped users' do
|
||||
let(:another_scope_user) { create(:user) }
|
||||
|
||||
it 'raises IdentityLinkMismatchError when trying to link different scoped users' do
|
||||
identity = described_class.link_from_web_request(
|
||||
service_account: primary_user,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
|
||||
expect(identity.primary_user).to eq(primary_user)
|
||||
expect(identity.scoped_user).to eq(scoped_user)
|
||||
expect(identity).to be_linked
|
||||
end
|
||||
|
||||
context 'when trying to link different scoped users' do
|
||||
let(:another_scope_user) { create(:user) }
|
||||
|
||||
it 'raises IdentityLinkMismatchError when trying to link different scoped users' do
|
||||
identity = described_class.link_from_web_request(
|
||||
service_account: primary_user,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
|
||||
expect do
|
||||
identity.link!(another_scope_user)
|
||||
end.to raise_error(described_class::IdentityLinkMismatchError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service_account does not have composite identity enforced' do
|
||||
it 'creates identity without linking' do
|
||||
identity = described_class.link_from_web_request(
|
||||
service_account: primary_user,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
|
||||
expect(identity).not_to be_linked
|
||||
end
|
||||
end
|
||||
|
||||
context 'when composite identity feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(composite_identity: false)
|
||||
end
|
||||
|
||||
it 'creates identity without linking' do
|
||||
identity = described_class.link_from_web_request(
|
||||
service_account: primary_user,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
|
||||
expect(identity.primary_user).to eq(primary_user)
|
||||
expect(identity).not_to be_linked
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service_account is not present' do
|
||||
it 'raises an error' do
|
||||
expect do
|
||||
described_class.link_from_web_request(
|
||||
service_account: nil,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
end.to raise_error(described_class::MissingServiceAccountError)
|
||||
identity.link!(another_scope_user)
|
||||
end.to raise_error(described_class::IdentityLinkMismatchError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service_account does not have composite identity enforced' do
|
||||
it 'creates identity without linking' do
|
||||
identity = described_class.link_from_web_request(
|
||||
service_account: primary_user,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
|
||||
expect(identity).not_to be_linked
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service_account is not present' do
|
||||
it 'raises an error' do
|
||||
expect do
|
||||
described_class.link_from_web_request(
|
||||
service_account: nil,
|
||||
scoped_user: scoped_user
|
||||
)
|
||||
end.to raise_error(described_class::MissingServiceAccountError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.sidekiq_restore!' do
|
||||
|
|
|
|||
|
|
@ -521,16 +521,6 @@ RSpec.describe Ability, feature_category: :system_access do
|
|||
expect(subject).to be_falsey
|
||||
end
|
||||
|
||||
context 'with disabled composite_identity feature flag' do
|
||||
before do
|
||||
stub_feature_flags(composite_identity: false)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(subject).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unenforced composite identity' do
|
||||
before do
|
||||
allow(user).to receive(:composite_identity_enforced).and_return(false)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Clusters::Agents::ManagedResource, feature_category: :deployment_management do
|
||||
it { is_expected.to belong_to(:build).class_name('Ci::Build') }
|
||||
it { is_expected.to belong_to(:cluster_agent).class_name('Clusters::Agent') }
|
||||
it { is_expected.to belong_to(:project) }
|
||||
it { is_expected.to belong_to(:environment) }
|
||||
|
||||
it { is_expected.to validate_length_of(:template_name).is_at_most(1024) }
|
||||
end
|
||||
|
|
@ -20,6 +20,7 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
let(:glm_source) { 'glm_source' }
|
||||
let(:glm_content) { 'glm_content' }
|
||||
let(:joining_project) { true }
|
||||
let(:setup_for_company) { true }
|
||||
let(:role) { 0 }
|
||||
let(:onboarding_status) do
|
||||
{
|
||||
|
|
@ -31,6 +32,7 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
glm_source: glm_source,
|
||||
glm_content: glm_content,
|
||||
joining_project: joining_project,
|
||||
setup_for_company: setup_for_company,
|
||||
role: role
|
||||
}
|
||||
end
|
||||
|
|
@ -177,6 +179,22 @@ RSpec.describe UserDetail, feature_category: :system_access do
|
|||
end
|
||||
end
|
||||
|
||||
context 'for setup_for_company' do
|
||||
let(:onboarding_status) do
|
||||
{
|
||||
setup_for_company: setup_for_company
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
|
||||
context "when 'setup_for_company' is invalid" do
|
||||
let(:setup_for_company) { 'true' }
|
||||
|
||||
it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'for role' do
|
||||
let(:onboarding_status) do
|
||||
{
|
||||
|
|
|
|||
|
|
@ -400,6 +400,14 @@ RSpec.shared_examples 'work items confidentiality' do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'work items submit as spam' do
|
||||
it 'shows link to submit as spam' do
|
||||
click_button _('More actions'), match: :first
|
||||
|
||||
expect(page).to have_link 'Submit as spam'
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'work items todos' do
|
||||
it 'adds item to to-do list', :aggregate_failures do
|
||||
expect(page).to have_button s_('WorkItem|Add a to-do item')
|
||||
|
|
|
|||
Loading…
Reference in New Issue