Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-02 12:07:33 +00:00
parent 4fbfae83af
commit ae9f43a2c4
110 changed files with 1746 additions and 369 deletions

View File

@ -129,7 +129,7 @@ rspec-all frontend_fixture:
needs:
- !reference [.frontend-fixtures-base, needs]
- "compile-test-assets"
parallel: 5
parallel: 7
# Builds FOSS fixtures in the EE project, with the `ee/` folder removed (due to `as-if-foss`).
rspec-all frontend_fixture as-if-foss:
@ -200,7 +200,7 @@ jest:
- tmp/tests/frontend/
reports:
junit: junit_jest.xml
parallel: 5
parallel: 7
jest predictive:
extends:
@ -218,7 +218,7 @@ jest as-if-foss:
- .frontend:rules:jest:as-if-foss
- .as-if-foss
needs: ["rspec-all frontend_fixture as-if-foss"]
parallel: 2
parallel: 4
jest predictive as-if-foss:
extends:

View File

@ -620,7 +620,7 @@ e2e-test-report:
- .rules:report:allure-report
stage: report
variables:
GITLAB_AUTH_TOKEN: $GITLAB_QA_MR_ALLURE_REPORT_TOKEN
GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE
ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
ALLURE_JOB_NAME: e2e-package-and-test
@ -654,7 +654,7 @@ relate-test-failures:
variables:
QA_FAILURES_REPORTING_PROJECT: gitlab-org/gitlab
QA_FAILURES_MAX_DIFF_RATIO: "0.15"
GITLAB_QA_ACCESS_TOKEN: $GITLAB_QA_PRODUCTION_ACCESS_TOKEN
GITLAB_QA_ACCESS_TOKEN: $QA_GITLAB_CI_TOKEN
when: on_failure
script:
- |

View File

@ -143,7 +143,7 @@ e2e-test-report:
ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
ALLURE_RESULTS_GLOB: qa/tmp/allure-results
ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
GITLAB_AUTH_TOKEN: $GITLAB_QA_MR_ALLURE_REPORT_TOKEN
GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE
GIT_STRATEGY: none
allow_failure: true
when: always

View File

@ -559,6 +559,7 @@
- "{,ee/,jh/}Gemfile.lock" # This should include gitlab-styles, rubocop itself, and any plugins we might be using
- "lib/gitlab_edition.rb" # This is required in RuboCop::CodeReuseHelpers
- ".gitlab/ci/static-analysis.gitlab-ci.yml"
- "config/feature_categories.yml" # Used by RSpec/InvalidFeatureCategory
.danger-patterns: &danger-patterns
- "Dangerfile"

View File

@ -1,6 +0,0 @@
---
# Cop supports --autocorrect.
Performance/ConcurrentMonotonicTime:
Details: grace period
Exclude:
- 'lib/gitlab/database/connection_timer.rb'

View File

@ -1 +1 @@
ed85386e4a808bab0023c28b9b1d7e103b50050e
54a1400cccb31b1869a7a9b735bad1cfb047d3bb

View File

@ -0,0 +1,48 @@
<script>
import { GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
export default {
name: 'AdminNewRunnerApp',
components: {
GlLink,
GlSprintf,
RunnerInstructionsModal,
},
directives: {
GlModal: GlModalDirective,
},
props: {
legacyRegistrationToken: {
type: String,
required: true,
},
},
modalId: 'runners-legacy-registration-instructions-modal',
};
</script>
<template>
<div>
<h1 class="gl-font-size-h2">{{ s__('Runners|New instance runner') }}</h1>
<p>
<gl-sprintf
:message="
s__(
'Runners|Create an instance runner to generate a command that registers the runner with all its configurations. %{linkStart}Prefer to use a registration token to create a runner?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link v-gl-modal="$options.modalId" data-testid="legacy-instructions-link">{{
content
}}</gl-link>
<runner-instructions-modal
:modal-id="$options.modalId"
:registration-token="legacyRegistrationToken"
/>
</template>
</gl-sprintf>
</p>
</div>
</template>

View File

@ -0,0 +1,32 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AdminNewRunnerApp from './admin_new_runner_app.vue';
Vue.use(VueApollo);
export const initAdminNewRunner = (selector = '#js-admin-new-runner') => {
const el = document.querySelector(selector);
if (!el) {
return null;
}
const { legacyRegistrationToken } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
render(h) {
return h(AdminNewRunnerApp, {
props: {
legacyRegistrationToken,
},
});
},
});
};

View File

@ -8,9 +8,11 @@ import {
ACTIONS_UNSCHEDULE,
ACTIONS_PLAY,
ACTIONS_RETRY,
ACTIONS_RUN_AGAIN,
CANCEL,
GENERIC_ERROR,
JOB_SCHEDULED,
JOB_SUCCESS,
PLAY_JOB_CONFIRMATION_MESSAGE,
RUN_JOB_NOW_HEADER_TITLE,
FILE_TYPE_ARCHIVE,
@ -107,6 +109,9 @@ export default {
shouldDisplayArtifacts() {
return this.canReadArtifacts && this.hasArtifacts;
},
retryButtonTitle() {
return this.job.status === JOB_SUCCESS ? ACTIONS_RUN_AGAIN : ACTIONS_RETRY;
},
},
methods: {
async postJobAction(name, mutation, redirect = false) {
@ -223,8 +228,8 @@ export default {
<gl-button
v-else-if="isRetryable"
icon="retry"
:title="$options.ACTIONS_RETRY"
:aria-label="$options.ACTIONS_RETRY"
:title="retryButtonTitle"
:aria-label="retryButtonTitle"
:method="currentJobMethod"
:disabled="retryBtnDisabled"
data-testid="retry"

View File

@ -9,6 +9,7 @@ export const RAW_TEXT_WARNING = s__(
/* Job Status Constants */
export const JOB_SCHEDULED = 'SCHEDULED';
export const JOB_SUCCESS = 'SUCCESS';
/* Artifact file types */
export const FILE_TYPE_ARCHIVE = 'ARCHIVE';
@ -19,6 +20,7 @@ export const ACTIONS_START_NOW = s__('DelayedJobs|Start now');
export const ACTIONS_UNSCHEDULE = s__('DelayedJobs|Unschedule');
export const ACTIONS_PLAY = __('Play');
export const ACTIONS_RETRY = __('Retry');
export const ACTIONS_RUN_AGAIN = __('Run again');
export const CANCEL = __('Cancel');
export const GENERIC_ERROR = __('An error occurred while making the request.');

View File

@ -8,7 +8,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { PACKAGE_DEFAULT_STATUS } from '../../constants';
export default {
name: 'PackageListRow',
name: 'PackageVersionRow',
components: {
GlLink,
GlSprintf,
@ -25,6 +25,9 @@ export default {
},
},
computed: {
containsWebPathLink() {
return Boolean(this.packageEntity?._links?.webPath);
},
packageLink() {
return `${getIdFromGraphQLId(this.packageEntity.id)}`;
},
@ -39,9 +42,15 @@ export default {
<list-item :disabled="disabledRow">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link :href="packageLink" class="gl-text-body gl-min-w-0" :disabled="disabledRow">
<gl-link
v-if="containsWebPathLink"
class="gl-text-body gl-min-w-0"
:disabled="disabledRow"
:href="packageLink"
>
<gl-truncate :text="packageEntity.name" />
</gl-link>
<gl-truncate v-else :text="packageEntity.name" />
<package-tags
v-if="packageEntity.tags.nodes && packageEntity.tags.nodes.length"

View File

@ -78,9 +78,6 @@ export default {
nonDefaultRow() {
return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS;
},
routerLinkEvent() {
return this.nonDefaultRow ? '' : 'click';
},
errorPackageStyle() {
return {
'gl-text-red-500': this.errorStatusRow,
@ -117,7 +114,6 @@ export default {
class="gl-text-body gl-min-w-0"
data-testid="details-link"
data-qa-selector="package_link"
:event="routerLinkEvent"
:to="{ name: 'details', params: { id: packageId } }"
>
<gl-truncate :text="packageEntity.name" />

View File

@ -69,6 +69,9 @@ query getPackageDetails(
createdAt
version
status
_links {
webPath
}
tags(first: 1) {
nodes {
id

View File

@ -0,0 +1,3 @@
import { initAdminNewRunner } from '~/ci/runner/admin_new_runner';
initAdminNewRunner();

View File

@ -0,0 +1,16 @@
import { s__ } from '~/locale';
import { createAlert } from '~/flash';
if (window.gon.features?.profileTabsVue) {
import('~/profile')
.then(({ initProfileTabs }) => {
initProfileTabs();
})
.catch(() => {
createAlert({
message: s__(
'UserProfile|An error occurred loading the profile. Please refresh the page to try again.',
),
});
});
}

View File

@ -50,6 +50,7 @@ export default {
actionPrimary: { text: __('Retry') },
actionCancel: { text: __('Cancel') },
},
runAgainTooltipText: __('Run again'),
},
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
@ -246,6 +247,14 @@ export default {
withConfirmationModal() {
return this.isRetryableBridge && !this.skipRetryModal;
},
jobActionTooltipText() {
const { group } = this.status;
const { title, icon } = this.status.action;
return icon === 'retry' && group === 'success'
? this.$options.i18n.runAgainTooltipText
: title;
},
},
watch: {
skipRetryModal(val) {
@ -334,7 +343,7 @@ export default {
<action-component
v-if="hasAction"
:tooltip-text="status.action.title"
:tooltip-text="jobActionTooltipText"
:link="status.action.path"
:action-icon="status.action.icon"
class="gl-mr-1"

View File

@ -2,7 +2,7 @@
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { sprintf } from '~/locale';
import { __, sprintf } from '~/locale';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
@ -33,6 +33,9 @@ import JobNameComponent from '../jobs_shared/job_name_component.vue';
*/
export default {
i18n: {
runAgainTooltipText: __('Run again'),
},
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
@ -129,6 +132,14 @@ export default {
? `${this.$options.hoverClass} ${this.cssClassJobName}`
: this.cssClassJobName;
},
jobActionTooltipText() {
const { group } = this.status;
const { title, icon } = this.status.action;
return icon === 'retry' && group === 'success'
? this.$options.i18n.runAgainTooltipText
: title;
},
},
errorCaptured(err, _vm, info) {
reportToSentry('pipelines_job_item', `pipelines_job_item error: ${err}, info: ${info}`);
@ -177,7 +188,7 @@ export default {
<action-component
v-if="hasAction"
:tooltip-text="status.action.title"
:tooltip-text="jobActionTooltipText"
:link="status.action.path"
:action-icon="status.action.icon"
data-qa-selector="action_button"

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Activity'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Contributed projects'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Followers'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Following'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Groups'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Overview'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Personal projects'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,72 @@
<script>
import { GlTabs } from '@gitlab/ui';
import OverviewTab from './overview_tab.vue';
import ActivityTab from './activity_tab.vue';
import GroupsTab from './groups_tab.vue';
import ContributedProjectsTab from './contributed_projects_tab.vue';
import PersonalProjectsTab from './personal_projects_tab.vue';
import StarredProjectsTab from './starred_projects_tab.vue';
import SnippetsTab from './snippets_tab.vue';
import FollowersTab from './followers_tab.vue';
import FollowingTab from './following_tab.vue';
export default {
components: {
GlTabs,
OverviewTab,
ActivityTab,
GroupsTab,
ContributedProjectsTab,
PersonalProjectsTab,
StarredProjectsTab,
SnippetsTab,
FollowersTab,
FollowingTab,
},
tabs: [
{
key: 'overview',
component: OverviewTab,
},
{
key: 'activity',
component: ActivityTab,
},
{
key: 'groups',
component: GroupsTab,
},
{
key: 'contributedProjects',
component: ContributedProjectsTab,
},
{
key: 'personalProjects',
component: PersonalProjectsTab,
},
{
key: 'starredProjects',
component: StarredProjectsTab,
},
{
key: 'snippets',
component: SnippetsTab,
},
{
key: 'followers',
component: FollowersTab,
},
{
key: 'following',
component: FollowingTab,
},
],
};
</script>
<template>
<gl-tabs>
<component :is="component" v-for="{ key, component } in $options.tabs" :key="key" />
</gl-tabs>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Snippets'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,17 @@
<script>
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
i18n: {
title: s__('UserProfile|Starred projects'),
},
components: { GlTab },
};
</script>
<template>
<gl-tab :title="$options.i18n.title">
<!-- placeholder -->
</gl-tab>
</template>

View File

@ -0,0 +1,16 @@
import Vue from 'vue';
import ProfileTabs from './components/profile_tabs.vue';
export const initProfileTabs = () => {
const el = document.getElementById('js-profile-tabs');
if (!el) return false;
return new Vue({
el,
render(createElement) {
return createElement(ProfileTabs);
},
});
};

View File

@ -26,6 +26,9 @@ class UsersController < ApplicationController
before_action only: [:exists] do
check_rate_limit!(:username_exists, scope: request.ip)
end
before_action only: [:show] do
push_frontend_feature_flag(:profile_tabs_vue, current_user)
end
feature_category :user_profile, [:show, :activity, :groups, :projects, :contributed, :starred,
:followers, :following, :calendar, :calendar_activities,

View File

@ -11,7 +11,7 @@ module Types
Types::ProjectType.connection_type,
null: false,
description: 'Allow list of projects that can be accessed by CI Job tokens created by this project.',
method: :all_projects
method: :outbound_projects
end
end
# rubocop: enable Graphql/AuthorizeTypes

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
# The connection between a source project (which defines the job token scope)
# and a target project which is the one allowed to be accessed by the job token.
# The connection between a source project (which the job token scope's allowlist applies too)
# and a target project which is added to the scope's allowlist.
module Ci
module JobToken
@ -9,6 +9,7 @@ module Ci
self.table_name = 'ci_job_token_project_scope_links'
belongs_to :source_project, class_name: 'Project'
# the project added to the scope's allowlist
belongs_to :target_project, class_name: 'Project'
belongs_to :added_by, class_name: 'User'
@ -19,6 +20,8 @@ module Ci
validates :target_project, presence: true
validate :not_self_referential_link
# When outbound the target project is allowed to be accessed by the source job token.
# When inbound the source project is allowed to be accessed by the target job token.
enum direction: {
outbound: 0,
inbound: 1

View File

@ -2,18 +2,17 @@
# This model represents the scope of access for a CI_JOB_TOKEN.
#
# A scope is initialized with a project.
# A scope is initialized with a current project.
#
# Projects can be added to the scope by adding ScopeLinks to
# create an allowlist of projects in either access direction (inbound, outbound).
#
# Currently, projects in the outbound allowlist can be accessed via the token
# in the source project.
# Projects in the outbound allowlist can be accessed via the current project's job token.
#
# TODO(Issue #346298) Projects in the inbound allowlist can use their token to access
# the source project.
# Projects in the inbound allowlist can use their project's job token to
# access the current project.
#
# CI_JOB_TOKEN should be considered untrusted without these features enabled.
# CI_JOB_TOKEN should be considered untrusted without a scope enabled.
#
module Ci
@ -25,34 +24,61 @@ module Ci
@current_project = current_project
end
def allows?(accessed_project)
self_referential?(accessed_project) || outbound_allows?(accessed_project)
def accessible?(accessed_project)
self_referential?(accessed_project) || (
outbound_accessible?(accessed_project) &&
inbound_accessible?(accessed_project)
)
end
def outbound_projects
outbound_allowlist.projects
end
# Deprecated: use outbound_projects, TODO(Issue #346298) remove references to all_project
def all_projects
outbound_projects
def inbound_projects
inbound_allowlist.projects
end
private
def outbound_allows?(accessed_project)
def outbound_accessible?(accessed_project)
# if the setting is disabled any project is considered to be in scope.
return true unless @current_project.ci_outbound_job_token_scope_enabled?
return true unless current_project.ci_outbound_job_token_scope_enabled?
outbound_allowlist.includes?(accessed_project)
end
def inbound_accessible?(accessed_project)
# if the flag or setting is disabled any project is considered to be in scope.
return true unless Feature.enabled?(:ci_inbound_job_token_scope, current_project)
return true unless current_project.ci_inbound_job_token_scope_enabled?
inbound_linked_as_accessible?(accessed_project)
end
# We don't check the inbound allowlist here. That is because
# the access check starts from the current project but the inbound
# allowlist contains projects that can access the current project.
def inbound_linked_as_accessible?(accessed_project)
inbound_accessible_projects(accessed_project).includes?(current_project)
end
def inbound_accessible_projects(accessed_project)
Ci::JobToken::Allowlist.new(accessed_project, direction: :inbound)
end
# User created list of projects allowed to access the current project
def inbound_allowlist
Ci::JobToken::Allowlist.new(current_project, direction: :inbound)
end
# User created list of projects that can be accessed from the current project
def outbound_allowlist
Ci::JobToken::Allowlist.new(@current_project, direction: :outbound)
Ci::JobToken::Allowlist.new(current_project, direction: :outbound)
end
def self_referential?(accessed_project)
@current_project.id == accessed_project.id
current_project.id == accessed_project.id
end
end
end

View File

@ -121,7 +121,7 @@ class ProjectPolicy < BasePolicy
desc "If user is authenticated via CI job token then the target project should be in scope"
condition(:project_allowed_for_job_token) do
!@user&.from_ci_job_token? || @user.ci_job_token_scope.allows?(project)
!@user&.from_ci_job_token? || @user.ci_job_token_scope.accessible?(project)
end
with_scope :subject

View File

@ -3,6 +3,7 @@
module Ci
class ParseDotenvArtifactService < ::BaseService
include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::EncodingHelper
SizeLimitError = Class.new(StandardError)
ParserError = Class.new(StandardError)
@ -36,6 +37,10 @@ module Ci
variables = {}
artifact.each_blob do |blob|
# Windows powershell may output UTF-16LE files, so convert the whole file
# to UTF-8 before proceeding.
blob = strip_bom(encode_utf8_with_replacement_character(blob))
blob.each_line do |line|
key, value = scan_line!(line)

View File

@ -1,4 +1,5 @@
- title = s_('Runners|Create an instance runner')
- add_to_breadcrumbs _('Runners'), admin_runners_path
- page_title title
- breadcrumb_title s_('Runner|New')
- page_title s_('Runners|Create an instance runner')
#js-admin-new-runner{ data: { legacy_registration_token: Gitlab::CurrentSettings.runners_registration_token } }

View File

@ -125,94 +125,98 @@
= @user.bio
- unless profile_tabs.empty?
.scrolling-tabs-container
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
- if profile_tab?(:overview)
%li.js-overview-tab
= link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do
= s_('UserProfile|Overview')
- if profile_tab?(:activity)
%li.js-activity-tab
= link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
= s_('UserProfile|Activity')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
= s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
= s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
= s_('UserProfile|Personal projects')
- if profile_tab?(:starred)
%li.js-starred-tab
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
= s_('UserProfile|Starred projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
- if profile_tab?(:followers)
%li.js-followers-tab
= link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do
= s_('UserProfile|Followers')
- if profile_tab?(:following)
%li.js-following-tab
= link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
= s_('UserProfile|Following')
- if Feature.enabled?(:profile_tabs_vue, current_user)
#js-profile-tabs
- else
.scrolling-tabs-container
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
- if profile_tab?(:overview)
%li.js-overview-tab
= link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do
= s_('UserProfile|Overview')
- if profile_tab?(:activity)
%li.js-activity-tab
= link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
= s_('UserProfile|Activity')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
= s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
= s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
= s_('UserProfile|Personal projects')
- if profile_tab?(:starred)
%li.js-starred-tab
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
= s_('UserProfile|Starred projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
- if profile_tab?(:followers)
%li.js-followers-tab
= link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do
= s_('UserProfile|Followers')
- if profile_tab?(:following)
%li.js-following-tab
= link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
= s_('UserProfile|Following')
%div{ class: container_class }
.tab-content
- if profile_tab?(:overview)
#js-overview.tab-pane
= render "users/overview"
- unless Feature.enabled?(:profile_tabs_vue, current_user)
.tab-content
- if profile_tab?(:overview)
#js-overview.tab-pane
= render "users/overview"
- if profile_tab?(:activity)
#activity.tab-pane
.flash-container
- if can?(current_user, :read_cross_project)
%h4.prepend-top-20
= s_('UserProfile|Most Recent Activity')
.content_list{ data: { href: user_activity_path } }
.loading
= gl_loading_icon(size: 'md')
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:activity)
#activity.tab-pane
.flash-container
- if can?(current_user, :read_cross_project)
%h4.prepend-top-20
= s_('UserProfile|Most Recent Activity')
.content_list{ data: { href: user_activity_path } }
.loading
= gl_loading_icon(size: 'md')
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:followers)
#followers.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:followers)
#followers.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:following)
#following.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:following)
#following.tab-pane
-# This tab is always loaded via AJAX
.loading.hide
.gl-spinner.gl-spinner-md
.loading.hide
.gl-spinner.gl-spinner-md
- if profile_tabs.empty?
.svg-content

View File

@ -0,0 +1,8 @@
---
name: ci_multi_doc_yaml
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109137
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388836
milestone: '15.9'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: profile_tabs_vue
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109422
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388708
milestone: '15.9'
type: development
group: group::organization
default_enabled: false

View File

@ -40,6 +40,6 @@ markdown(<<~MARKDOWN)
The review does not need to block merging this merge request. See the:
- [Metadata for the `*.md` files](https://docs.gitlab.com/ee/development/documentation/#metadata) that you've changed. The first few lines of each `*.md` file identify the stage and group most closely associated with your docs change.
- The [Technical Writer assigned](https://about.gitlab.com/handbook/engineering/technical-writing/#assignments) for that stage and group.
- The [Technical Writer assigned](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments) for that stage and group.
- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
MARKDOWN

View File

@ -1201,57 +1201,6 @@ To get started quickly:
Congratulations! You've configured an observable fault-tolerant Praefect
cluster.
### Manage Gitaly nodes on a Gitaly Cluster
You can add and replace Gitaly nodes on a Gitaly Cluster.
#### Add new Gitaly nodes
To add a new Gitaly node to a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor):
- Set, set the [replication factor](praefect.md#configure-replication-factor) for each repository using `set-replication-factor` Praefect command. New repositories are
replicated based on [replication factor](praefect.md#configure-replication-factor). Praefect doesn't automatically replicate existing repositories to the new Gitaly node.
- Not set, add the new node in your [Praefect configuration](praefect.md#praefect) under `praefect['virtual_storages']`. Praefect automatically replicates all data to any
new Gitaly node added to the configuration.
#### Replace an existing Gitaly node
You can replace an existing Gitaly node with a new node with either the same name or a different name.
##### With a node with the same name
To use the same name for the replacement node, use [repository verifier](praefect.md#enable-deletions) to scan the storage and remove dangling metadata records.
[Manually prioritize verification](praefect.md#prioritize-verification-manually) of the replaced storage to speed up the process.
##### With a node with a different name
To use a different name for the replacement node for a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor):
- Set, use [`praefect set-replication-factor`](praefect.md#configure-replication-factor) to set the replication factor per repository again to get new storage assigned.
For example:
```shell
$ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -virtual-storage default -repository @hashed/3f/db/3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278.git -replication-factor 2
current assignments: gitaly-1, gitaly-2
```
To reassign all repositories from the old storage to the new one, after configuring the new Gitaly node:
1. Connect to Praefect database:
```shell
/opt/gitlab/embedded/bin/psql -h <psql host> -U <user> -d <database name>
```
1. Update `repository_assignments` table to replace the old Gitaly node name (for example, `old-gitaly`) with the new Gitaly node name (for example, `new-gitaly`):
```sql
UPDATE repository_assignments SET storage='new-gitaly' WHERE storage='old-gitaly';
```
- Not set, replace the node in the configuration. The old node's state remains in the Praefect database but it is ignored.
## Configure replication factor
WARNING:

View File

@ -9,6 +9,57 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Gitaly Cluster can recover from primary-node failure and unavailable repositories. Gitaly Cluster can perform data
recovery and has Praefect tracking database tools.
## Manage Gitaly nodes on a Gitaly Cluster
You can add and replace Gitaly nodes on a Gitaly Cluster.
### Add new Gitaly nodes
To add a new Gitaly node to a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor):
- Set, set the [replication factor](praefect.md#configure-replication-factor) for each repository using `set-replication-factor` Praefect command. New repositories are
replicated based on [replication factor](praefect.md#configure-replication-factor). Praefect doesn't automatically replicate existing repositories to the new Gitaly node.
- Not set, add the new node in your [Praefect configuration](praefect.md#praefect) under `praefect['virtual_storages']`. Praefect automatically replicates all data to any
new Gitaly node added to the configuration.
### Replace an existing Gitaly node
You can replace an existing Gitaly node with a new node with either the same name or a different name.
#### With a node with the same name
To use the same name for the replacement node, use [repository verifier](praefect.md#enable-deletions) to scan the storage and remove dangling metadata records.
[Manually prioritize verification](praefect.md#prioritize-verification-manually) of the replaced storage to speed up the process.
#### With a node with a different name
To use a different name for the replacement node for a Gitaly Cluster that has [replication factor](praefect.md#configure-replication-factor):
- Set, use [`praefect set-replication-factor`](praefect.md#configure-replication-factor) to set the replication factor per repository again to get new storage assigned.
For example:
```shell
$ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml set-replication-factor -virtual-storage default -repository @hashed/3f/db/3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278.git -replication-factor 2
current assignments: gitaly-1, gitaly-2
```
To reassign all repositories from the old storage to the new one, after configuring the new Gitaly node:
1. Connect to Praefect database:
```shell
/opt/gitlab/embedded/bin/psql -h <psql host> -U <user> -d <database name>
```
1. Update `repository_assignments` table to replace the old Gitaly node name (for example, `old-gitaly`) with the new Gitaly node name (for example, `new-gitaly`):
```sql
UPDATE repository_assignments SET storage='new-gitaly' WHERE storage='old-gitaly';
```
- Not set, replace the node in the configuration. The old node's state remains in the Praefect database but it is ignored.
## Primary node failure
> - Introduced in GitLab 13.0, Gitaly Cluster, elects the secondary with the least unreplicated writes from the primary to be the new primary. There can still be some unreplicated writes, so [data loss can occur](#check-for-data-loss).

View File

@ -43,7 +43,7 @@ After configuring your Mailgun domain for the webhook endpoints,
you're ready to enable the Mailgun integration:
1. Sign in to GitLab as an [Administrator](../../user/permissions.md) user.
1. On the top bar, select **Main menu >** **{admin}** **Admin**.
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, go to **Settings > General** and expand the **Mailgun** section.
1. Select the **Enable Mailgun** checkbox.
1. Enter the Mailgun HTTP webhook signing key as described in

View File

@ -337,6 +337,8 @@ spec:
website: # by default all declared inputs are mandatory.
environment:
default: test # apply default if not provided. This makes the input optional.
flags:
default: null # make an input entirely optional with no value by default.
test_run:
options: # a choice must be made from the list since there is no default value.
- unit

View File

@ -77,6 +77,12 @@ still succeeds even if that warning was printed. For example:
as a volume under `/builds`). In that case, the service does its job, and
because the job is not trying to connect to it, it does not fail.
If the services start successfully, they start before the
[`before_script`](../../ci/yaml/index.md#before_script) runs. This means you can
write a `before_script` that queries the service.
Services stop at the end of the job, even if the job fails.
## What services are not for
As mentioned before, this feature is designed to provide **network accessible**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -61,7 +61,7 @@ Instead of:
## access level
Access levels are different than [roles](#roles) or [permissions](#permissions).
When you create a user, you choose an access level: **Regular**, **Auditor**, or **Admin**.
When you create a user, you choose an access level: **Regular**, **Auditor**, or **Administrator**.
Capitalize these words when you refer to the UI. Otherwise use lowercase.

View File

@ -501,14 +501,12 @@ pipeline in `ruby2-sync` branch, which updates the `ruby2` branch with latest
is triggering a pipeline in `ruby2` 5 minutes after it, which is considered
the maintenance schedule to run test suites and update cache.
Any changes in `ruby2` are only for running the pipeline. It should
never be merged back to `master`. Any other Ruby 2.7 changes should go into
`master` directly, which should be compatible with Ruby 3.
The `ruby2` branch must not have any changes. The branch is only there to set
`RUBY_VERSION` to `2.7` in the maintenance pipeline schedule.
Previously, `ruby2-sync` was using a project token stored in `RUBY2_SYNC_TOKEN`
(now backed up in `RUBY2_SYNC_TOKEN_NOT_USED`), however due to various
permissions issues, we ended up using an access token from `gitlab-bot` so now
`RUBY2_SYNC_TOKEN` is actually an access token from `gitlab-bot`.
The `gitlab` job in the `ruby2-sync` branch uses a `gitlab-org/gitlab` project
token with `write_repository` scope and `Maintainer` role with no expiration.
The token is stored in the `RUBY2_SYNC_TOKEN` variable in `gitlab-org/gitlab`.
#### Long-term plan

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -17,9 +17,7 @@ Users are locked after ten failed sign-in attempts. These users remain locked:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Use the search bar to find the locked user.
1. From the **User administration** dropdown list select **Unlock**.
![Unlock a user from the Admin Area](img/unlock_user_v14_7.png)
1. From the **User administration** dropdown list, select **Unlock**.
## Unlock a user from the command line

View File

@ -1,6 +1,6 @@
---
stage: Systems
group: Distribution
stage: SaaS Platforms
group: GitLab Dedicated
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---

View File

@ -44,6 +44,8 @@ module API
end
def present_index_file!(file_type)
not_found!("Format #{params[:format]} is not supported") unless params[:format].nil?
relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize
relation = relation

View File

@ -117,7 +117,8 @@ module Gitlab
def expand_config(config)
build_config(config)
rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
rescue Gitlab::Config::Loader::Yaml::DataTooLargeError,
Gitlab::Config::Loader::MultiDocYaml::DataTooLargeError => e
track_and_raise_for_dev_exception(e)
raise Config::ConfigError, e.message

View File

@ -5,12 +5,21 @@ module Gitlab
class Config
module Yaml
AVAILABLE_TAGS = [Config::Yaml::Tags::Reference].freeze
MAX_DOCUMENTS = 2
class << self
def load!(content)
ensure_custom_tags
Gitlab::Config::Loader::Yaml.new(content, additional_permitted_classes: AVAILABLE_TAGS).load!
if ::Feature.enabled?(:ci_multi_doc_yaml)
Gitlab::Config::Loader::MultiDocYaml.new(
content,
max_documents: MAX_DOCUMENTS,
additional_permitted_classes: AVAILABLE_TAGS
).load!.first
else
Gitlab::Config::Loader::Yaml.new(content, additional_permitted_classes: AVAILABLE_TAGS).load!
end
end
private

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module Gitlab
module Config
module Loader
class MultiDocYaml
TooManyDocumentsError = Class.new(Loader::FormatError)
DataTooLargeError = Class.new(Loader::FormatError)
NotHashError = Class.new(Loader::FormatError)
MULTI_DOC_DIVIDER = /^---$/.freeze
def initialize(config, max_documents:, additional_permitted_classes: [])
@max_documents = max_documents
@safe_config = load_config(config, additional_permitted_classes)
end
def load!
raise TooManyDocumentsError, 'The parsed YAML has too many documents' if too_many_documents?
raise DataTooLargeError, 'The parsed YAML is too big' if too_big?
raise NotHashError, 'Invalid configuration format' unless all_hashes?
safe_config.map(&:deep_symbolize_keys)
end
private
attr_reader :safe_config, :max_documents
def load_config(config, additional_permitted_classes)
config.split(MULTI_DOC_DIVIDER).filter_map do |document|
YAML.safe_load(document,
permitted_classes: [Symbol, *additional_permitted_classes],
permitted_symbols: [],
aliases: true
)
end
rescue Psych::Exception => e
raise Loader::FormatError, e.message
end
def all_hashes?
safe_config.all?(Hash)
end
def too_many_documents?
safe_config.count > max_documents
end
def too_big?
!deep_sizes.all?(&:valid?)
end
def deep_sizes
safe_config.map do |config|
Gitlab::Utils::DeepSize.new(config,
max_size: Gitlab::CurrentSettings.current_application_settings.max_yaml_size_bytes,
max_depth: Gitlab::CurrentSettings.current_application_settings.max_yaml_depth)
end
end
end
end
end
end

View File

@ -27,7 +27,7 @@ module Gitlab
end
def current_clock_value
Concurrent.monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end

View File

@ -16,6 +16,7 @@ module Gitlab
ENCODING_CONFIDENCE_THRESHOLD = 50
UNICODE_REPLACEMENT_CHARACTER = "<EFBFBD>"
BOM_UTF8 = "\xEF\xBB\xBF"
def encode!(message)
message = force_encode_utf8(message)
@ -147,6 +148,10 @@ module Gitlab
filename.force_encoding("UTF-8")
end
def strip_bom(message)
message.delete_prefix(BOM_UTF8)
end
private
def force_encode_utf8(message)

View File

@ -36293,6 +36293,9 @@ msgstr ""
msgid "Run CI/CD pipelines with Jenkins."
msgstr ""
msgid "Run again"
msgstr ""
msgid "Run housekeeping"
msgstr ""
@ -36460,6 +36463,9 @@ msgstr ""
msgid "Runners|Create an instance runner"
msgstr ""
msgid "Runners|Create an instance runner to generate a command that registers the runner with all its configurations. %{linkStart}Prefer to use a registration token to create a runner?%{linkEnd}"
msgstr ""
msgid "Runners|Created %{timeAgo}"
msgstr ""
@ -36943,6 +36949,9 @@ msgstr ""
msgid "Runners|shared"
msgstr ""
msgid "Runner|New"
msgstr ""
msgid "Runner|Owner"
msgstr ""
@ -45995,6 +46004,9 @@ msgstr ""
msgid "UserProfile|Activity"
msgstr ""
msgid "UserProfile|An error occurred loading the profile. Please refresh the page to try again."
msgstr ""
msgid "UserProfile|Blocked user"
msgstr ""

View File

@ -71,6 +71,7 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
end
before do
stub_feature_flags(profile_tabs_vue: false)
sign_in user
end

View File

@ -12,6 +12,10 @@ RSpec.describe 'Tooltips on .timeago dates', :js, feature_category: :user_profil
project.add_maintainer(user)
end
before do
stub_feature_flags(profile_tabs_vue: false)
end
context 'on the activity tab' do
before do
Event.create!(project: project, author_id: user.id, action: :joined,

View File

@ -6,6 +6,7 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
let_it_be_with_refind(:user) { create(:user) }
before do
stub_feature_flags(profile_tabs_vue: false)
sign_in(user)
end

View File

@ -18,6 +18,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
before do
stub_feature_flags(profile_tabs_vue: false)
sign_in user
end

View File

@ -16,10 +16,31 @@ RSpec.describe 'User page', feature_category: :user_profile do
end
context 'with public profile' do
it 'shows all the tabs' do
context 'with `profile_tabs_vue` feature flag disabled' do
before do
stub_feature_flags(profile_tabs_vue: false)
end
it 'shows all the tabs' do
subject
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
expect(page).to have_link('Followers')
expect(page).to have_link('Following')
end
end
end
it 'shows all the tabs', :js do
subject
page.within '.nav-links' do
page.within '[role="tablist"]' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
@ -189,11 +210,33 @@ RSpec.describe 'User page', feature_category: :user_profile do
expect(page).to have_content("This user has a private profile")
end
it 'shows own tabs' do
context 'with `profile_tabs_vue` feature flag disabled' do
before do
stub_feature_flags(profile_tabs_vue: false)
end
it 'shows own tabs' do
sign_in(user)
subject
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
expect(page).to have_link('Followers')
expect(page).to have_link('Following')
end
end
end
it 'shows own tabs', :js do
sign_in(user)
subject
page.within '.nav-links' do
page.within '[role="tablist"]' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
@ -358,6 +401,10 @@ RSpec.describe 'User page', feature_category: :user_profile do
end
context 'most recent activity' do
before do
stub_feature_flags(profile_tabs_vue: false)
end
it 'shows the most recent activity' do
subject
@ -388,6 +435,10 @@ RSpec.describe 'User page', feature_category: :user_profile do
context 'with a bot user' do
let_it_be(:user) { create(:user, user_type: :security_bot) }
before do
stub_feature_flags(profile_tabs_vue: false)
end
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)

View File

@ -6,6 +6,10 @@ RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :snippet
context 'when the user has snippets' do
let(:user) { create(:user) }
before do
stub_feature_flags(profile_tabs_vue: false)
end
context 'pagination' do
let!(:snippets) { create_list(:snippet, 2, :public, author: user) }

View File

@ -28,6 +28,10 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego
end
end
before do
stub_feature_flags(profile_tabs_vue: false)
end
it 'hides loading spinner after load', :js do
visit user_path(user)
click_nav_link('Personal projects')

Binary file not shown.

View File

@ -0,0 +1,53 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
Vue.use(VueApollo);
describe('AdminNewRunnerApp', () => {
let wrapper;
const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(AdminNewRunnerApp, {
propsData: {
legacyRegistrationToken: mockLegacyRegistrationToken,
...props,
},
directives: {
GlModal: createMockDirective(),
},
stubs: {
GlSprintf,
},
...options,
});
};
beforeEach(() => {
createComponent();
});
describe('Shows legacy modal', () => {
it('passes legacy registration to modal', () => {
expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
mockLegacyRegistrationToken,
);
});
it('opens a modal with the legacy instructions', () => {
const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value;
expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId);
});
});
});

View File

@ -39,6 +39,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) }
let!(:failed) { create(:ci_build, :failed, name: 'failed', pipeline: pipeline) }
let!(:created_by_tag) { create(:ci_build, :success, name: 'created_by_tag', tag: true, pipeline: pipeline) }
let!(:pending) { create(:ci_build, :pending, name: 'pending', pipeline: pipeline) }
let!(:playable) { create(:ci_build, :playable, name: 'playable', pipeline: pipeline) }

View File

@ -37,6 +37,7 @@ describe('Job actions cell', () => {
const cancelableJob = findMockJob('cancelable');
const playableJob = findMockJob('playable');
const retryableJob = findMockJob('retryable');
const failedJob = findMockJob('failed');
const scheduledJob = findMockJob('scheduled');
const jobWithArtifact = findMockJob('with_artifact');
const cannotPlayJob = findMockJob('playable', mockJobsNodesAsGuest);
@ -79,10 +80,6 @@ describe('Job actions cell', () => {
});
};
afterEach(() => {
wrapper.destroy();
});
it('displays the artifacts download button with correct link', () => {
createComponent(jobWithArtifact);
@ -191,6 +188,20 @@ describe('Job actions cell', () => {
expect(button().props('disabled')).toBe(true);
});
describe('Retry button title', () => {
it('displays retry title when job has failed and is retryable', () => {
createComponent(failedJob);
expect(findRetryButton().attributes('title')).toBe('Retry');
});
it('displays run again title when job has passed and is retryable', () => {
createComponent(retryableJob);
expect(findRetryButton().attributes('title')).toBe('Run again');
});
});
describe('Scheduled Jobs', () => {
const today = () => new Date('2021-08-31');

View File

@ -19,6 +19,7 @@ describe('VersionRow', () => {
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const findPackageName = () => wrapper.findComponent(GlTruncate);
function createComponent(packageEntity = packageVersion) {
wrapper = shallowMountExtended(VersionRow, {
@ -74,16 +75,28 @@ describe('VersionRow', () => {
});
describe('disabled status', () => {
it('disables the list item', () => {
createComponent({ ...packageVersion, status: 'something' });
beforeEach(() => {
createComponent({
...packageVersion,
status: 'something',
_links: {
webPath: null,
},
});
});
it('disables the list item', () => {
expect(findListItem().props('disabled')).toBe(true);
});
it('disables the link', () => {
createComponent({ ...packageVersion, status: 'something' });
it('lists the package name', () => {
expect(findPackageName().props()).toMatchObject({
text: '@gitlab-org/package-15',
});
});
expect(findLink().attributes('disabled')).toBe('true');
it('does not have a link to navigate to the details page', () => {
expect(findLink().exists()).toBe(false);
});
});
});

View File

@ -84,22 +84,13 @@ describe('packages_list_row', () => {
mountComponent();
expect(findPackageLink().props()).toMatchObject({
event: 'click',
to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } },
});
});
it('does not have a link to navigate to the details page', () => {
mountComponent({
packageEntity: {
...packageWithoutTags,
_links: {
webPath: null,
},
},
});
it('lists the package name', () => {
mountComponent();
expect(findPackageLink().exists()).toBe(false);
expect(findPackageName().props()).toMatchObject({
text: '@gitlab-org/package-15',
});
@ -156,11 +147,25 @@ describe('packages_list_row', () => {
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => {
mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } });
mountComponent({
packageEntity: {
...packageWithoutTags,
status: PACKAGE_ERROR_STATUS,
_links: {
webPath: null,
},
},
});
});
it('details link is disabled', () => {
expect(findPackageLink().props('event')).toBe('');
it('lists the package name', () => {
expect(findPackageName().props()).toMatchObject({
text: '@gitlab-org/package-15',
});
});
it('does not have a link to navigate to the details page', () => {
expect(findPackageLink().exists()).toBe(false);
});
it('has a warning icon', () => {

View File

@ -97,6 +97,12 @@ export const packageProject = () => ({
__typename: 'Project',
});
export const linksData = {
_links: {
webPath: '/gitlab-org/package-15',
},
};
export const packageVersions = () => [
{
createdAt: '2021-08-10T09:33:54Z',
@ -105,6 +111,7 @@ export const packageVersions = () => [
status: 'DEFAULT',
tags: { nodes: packageTags() },
version: '1.0.1',
...linksData,
__typename: 'Package',
},
{
@ -114,17 +121,11 @@ export const packageVersions = () => [
status: 'DEFAULT',
tags: { nodes: packageTags() },
version: '1.0.2',
...linksData,
__typename: 'Package',
},
];
export const linksData = {
_links: {
webPath: '/gitlab-org/package-15',
__typeName: 'PackageLinks',
},
};
export const packageData = (extend) => ({
__typename: 'Package',
id: 'gid://gitlab/Packages::Package/111',

View File

@ -595,7 +595,8 @@ describe('PackagesApp', () => {
it('binds the correct props', async () => {
const versionNodes = packageVersions();
createComponent({ packageEntity: { versions: { nodes: versionNodes } } });
createComponent();
await waitForPromises();
expect(findVersionsList().props()).toMatchObject({

View File

@ -12,6 +12,7 @@ import {
mockJob,
mockJobWithoutDetails,
mockJobWithUnauthorizedAction,
mockFailedJob,
triggerJob,
triggerJobWithRetryAction,
} from './mock_data';
@ -64,7 +65,6 @@ describe('pipeline graph job item', () => {
afterEach(() => {
mockAxios.restore();
wrapper.destroy();
});
describe('name with link', () => {
@ -131,6 +131,18 @@ describe('pipeline graph job item', () => {
expect(actionComponent.props('actionIcon')).toBe('stop');
expect(actionComponent.attributes('disabled')).toBe('disabled');
});
it('action icon tooltip text when job has passed but can be ran again', () => {
createWrapper({ props: { job: mockJob } });
expect(findActionComponent().props('tooltipText')).toBe('Run again');
});
it('action icon tooltip text when job has failed and can be retried', () => {
createWrapper({ props: { job: mockFailedJob } });
expect(findActionComponent().props('tooltipText')).toBe('Retry');
});
});
describe('job style', () => {

View File

@ -1055,3 +1055,25 @@ export const triggerJobWithRetryAction = {
},
},
};
export const mockFailedJob = {
id: 3999,
name: 'failed job',
kind: BUILD_KIND,
status: {
id: 'failed-3999-3999',
icon: 'status_failed',
tooltip: 'failed - (stuck or timeout failure)',
hasDetails: true,
detailsPath: '/root/ci-project/-/jobs/3999',
group: 'failed',
label: 'failed',
action: {
id: 'Ci::BuildPresenter-failed-3999',
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/ci-project/-/jobs/3999/retry',
title: 'Retry',
},
},
};

View File

@ -0,0 +1,19 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import ActivityTab from '~/profile/components/activity_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('ActivityTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(ActivityTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Activity'));
});
});

View File

@ -0,0 +1,21 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('ContributedProjectsTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(ContributedProjectsTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
s__('UserProfile|Contributed projects'),
);
});
});

View File

@ -0,0 +1,19 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import FollowersTab from '~/profile/components/followers_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('FollowersTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(FollowersTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers'));
});
});

View File

@ -0,0 +1,19 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import FollowingTab from '~/profile/components/following_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('FollowingTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(FollowingTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following'));
});
});

View File

@ -0,0 +1,19 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import GroupsTab from '~/profile/components/groups_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('GroupsTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(GroupsTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Groups'));
});
});

View File

@ -0,0 +1,19 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import OverviewTab from '~/profile/components/overview_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('OverviewTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(OverviewTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview'));
});
});

View File

@ -0,0 +1,21 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('PersonalProjectsTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(PersonalProjectsTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
s__('UserProfile|Personal projects'),
);
});
});

View File

@ -0,0 +1,36 @@
import ProfileTabs from '~/profile/components/profile_tabs.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OverviewTab from '~/profile/components/overview_tab.vue';
import ActivityTab from '~/profile/components/activity_tab.vue';
import GroupsTab from '~/profile/components/groups_tab.vue';
import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue';
import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue';
import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
import SnippetsTab from '~/profile/components/snippets_tab.vue';
import FollowersTab from '~/profile/components/followers_tab.vue';
import FollowingTab from '~/profile/components/following_tab.vue';
describe('ProfileTabs', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(ProfileTabs);
};
it.each([
OverviewTab,
ActivityTab,
GroupsTab,
ContributedProjectsTab,
PersonalProjectsTab,
StarredProjectsTab,
SnippetsTab,
FollowersTab,
FollowingTab,
])('renders $i18n.title tab', (tab) => {
createComponent();
expect(wrapper.findComponent(tab).exists()).toBe(true);
});
});

View File

@ -0,0 +1,19 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import SnippetsTab from '~/profile/components/snippets_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('SnippetsTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(SnippetsTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Snippets'));
});
});

View File

@ -0,0 +1,21 @@
import { GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('StarredProjectsTab', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMountExtended(StarredProjectsTab);
};
it('renders `GlTab` and sets `title` prop', () => {
createComponent();
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
s__('UserProfile|Starred projects'),
);
});
});

View File

@ -45,7 +45,7 @@ RSpec.describe Mutations::Ci::JobTokenScope::AddProject do
it 'adds target project to the job token scope' do
expect do
expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1)
end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
end
context 'when the service returns an error' do

View File

@ -23,18 +23,18 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
it 'returns the same project in the allow list of projects for the Ci Job Token when scope is not enabled' do
allow(project).to receive(:ci_outbound_job_token_scope_enabled?).and_return(false)
expect(resolve_scope.all_projects).to contain_exactly(project)
expect(resolve_scope.outbound_projects).to contain_exactly(project)
end
it 'returns the same project in the allow list of projects for the Ci Job Token' do
expect(resolve_scope.all_projects).to contain_exactly(project)
expect(resolve_scope.outbound_projects).to contain_exactly(project)
end
context 'when another projects gets added to the allow list' do
let!(:link) { create(:ci_job_token_project_scope_link, source_project: project) }
it 'returns both projects' do
expect(resolve_scope.all_projects).to contain_exactly(project, link.target_project)
expect(resolve_scope.outbound_projects).to contain_exactly(project, link.target_project)
end
end
@ -44,7 +44,7 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
end
it 'resolves projects' do
expect(resolve_scope.all_projects).to contain_exactly(project)
expect(resolve_scope.outbound_projects).to contain_exactly(project)
end
end
end

View File

@ -0,0 +1,105 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring do
describe '.load!' do
it 'loads a single-doc YAML file' do
yaml = <<~YAML
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
config = described_class.load!(yaml)
expect(config).to eq({
image: 'image:1.0',
texts: {
nested_key: 'value1',
more_text: {
more_nested_key: 'value2'
}
}
})
end
it 'loads the first document from a multi-doc YAML file' do
yaml = <<~YAML
spec:
inputs:
test_input:
---
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
config = described_class.load!(yaml)
expect(config).to eq({
spec: {
inputs: {
test_input: nil
}
}
})
end
context 'when ci_multi_doc_yaml is disabled' do
before do
stub_feature_flags(ci_multi_doc_yaml: false)
end
it 'loads a single-doc YAML file' do
yaml = <<~YAML
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
config = described_class.load!(yaml)
expect(config).to eq({
image: 'image:1.0',
texts: {
nested_key: 'value1',
more_text: {
more_nested_key: 'value2'
}
}
})
end
it 'loads the first document from a multi-doc YAML file' do
yaml = <<~YAML
spec:
inputs:
test_input:
---
image: 'image:1.0'
texts:
nested_key: 'value1'
more_text:
more_nested_key: 'value2'
YAML
config = described_class.load!(yaml)
expect(config).to eq({
spec: {
inputs: {
test_input: nil
}
}
})
end
end
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline_authoring do
let(:loader) { described_class.new(yml, max_documents: 2) }
describe '#load!' do
let(:yml) do
<<~YAML
spec:
inputs:
test_input:
---
test_job:
script: echo "$[[ inputs.test_input ]]"
YAML
end
it 'returns the loaded YAML with all keys as symbols' do
expect(loader.load!).to eq([
{ spec: { inputs: { test_input: nil } } },
{ test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
])
end
context 'when the YAML file is empty' do
let(:yml) { '' }
it 'returns an empty array' do
expect(loader.load!).to be_empty
end
end
context 'when the parsed YAML is too big' do
let(:yml) do
<<~YAML
a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
---
a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
YAML
end
it 'raises a DataTooLargeError' do
expect { loader.load! }.to raise_error(described_class::DataTooLargeError, 'The parsed YAML is too big')
end
end
context 'when a document is not a hash' do
let(:yml) do
<<~YAML
not_a_hash
---
test_job:
script: echo "$[[ inputs.test_input ]]"
YAML
end
it 'raises a NotHashError' do
expect { loader.load! }.to raise_error(described_class::NotHashError, 'Invalid configuration format')
end
end
context 'when there are too many documents' do
let(:yml) do
<<~YAML
a: b
---
c: d
---
e: f
YAML
end
it 'raises a TooManyDocumentsError' do
expect { loader.load! }.to raise_error(
described_class::TooManyDocumentsError,
'The parsed YAML has too many documents'
)
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Config::Loader::Yaml do
RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_authoring do
let(:loader) { described_class.new(yml) }
let(:yml) do

View File

@ -283,4 +283,12 @@ RSpec.describe Gitlab::EncodingHelper do
expect(described_class.unquote_path('"\a\b\e\f\n\r\t\v\""')).to eq("\a\b\e\f\n\r\t\v\"")
end
end
describe '#strip_bom' do
it do
expect(described_class.strip_bom('no changes')).to eq('no changes')
expect(described_class.strip_bom("\xEF\xBB\xBFhello world")).to eq('hello world')
expect(described_class.strip_bom("BOM at the end\xEF\xBB\xBF")).to eq("BOM at the end\xEF\xBB\xBF")
end
end
end

View File

@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do
include Ci::JobTokenScopeHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:source_project) { create(:project) }
@ -24,11 +25,11 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
context 'when projects are added to the scope' do
include_context 'with scoped projects'
include_context 'with a project in each allowlist'
where(:direction, :additional_project) do
:outbound | ref(:outbound_scoped_project)
:inbound | ref(:inbound_scoped_project)
:outbound | ref(:outbound_allowlist_project)
:inbound | ref(:inbound_allowlist_project)
end
with_them do
@ -57,16 +58,16 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
end
context 'with scoped projects' do
include_context 'with scoped projects'
context 'with a project in each allowlist' do
include_context 'with a project in each allowlist'
where(:includes_project, :direction, :result) do
ref(:source_project) | :outbound | false
ref(:source_project) | :inbound | false
ref(:inbound_scoped_project) | :outbound | false
ref(:inbound_scoped_project) | :inbound | true
ref(:outbound_scoped_project) | :outbound | true
ref(:outbound_scoped_project) | :inbound | false
ref(:inbound_allowlist_project) | :outbound | false
ref(:inbound_allowlist_project) | :inbound | true
ref(:outbound_allowlist_project) | :outbound | true
ref(:outbound_allowlist_project) | :inbound | false
ref(:unscoped_project1) | :outbound | false
ref(:unscoped_project1) | :inbound | false
ref(:unscoped_project2) | :outbound | false

View File

@ -18,11 +18,12 @@ RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_int
describe 'unique index' do
let!(:link) { create(:ci_job_token_project_scope_link) }
it 'raises an error' do
it 'raises an error, when not unique' do
expect do
create(:ci_job_token_project_scope_link,
source_project: link.source_project,
target_project: link.target_project)
target_project: link.target_project,
direction: link.direction)
end.to raise_error(ActiveRecord::RecordNotUnique)
end
end

View File

@ -2,78 +2,144 @@
require 'spec_helper'
RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do
let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) }
RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, factory_default: :keep do
include Ci::JobTokenScopeHelpers
using RSpec::Parameterized::TableSyntax
let(:scope) { described_class.new(source_project) }
let_it_be(:project) { create_default(:project) }
let_it_be(:user) { create_default(:user) }
let_it_be(:namespace) { create_default(:namespace) }
describe '#all_projects' do
subject(:all_projects) { scope.all_projects }
let_it_be(:source_project) do
create(:project,
ci_outbound_job_token_scope_enabled: true,
ci_inbound_job_token_scope_enabled: true
)
end
let(:current_project) { source_project }
let(:scope) { described_class.new(current_project) }
describe '#outbound_projects' do
subject { scope.outbound_projects }
context 'when no projects are added to the scope' do
it 'returns the project defining the scope' do
expect(all_projects).to contain_exactly(source_project)
expect(subject).to contain_exactly(current_project)
end
end
context 'when projects are added to the scope' do
include_context 'with scoped projects'
include_context 'with accessible and inaccessible projects'
it 'returns all projects that can be accessed from a given scope' do
expect(subject).to contain_exactly(source_project, outbound_scoped_project)
expect(subject).to contain_exactly(current_project, outbound_allowlist_project, fully_accessible_project)
end
end
end
describe '#allows?' do
subject { scope.allows?(includes_project) }
describe '#inbound_projects' do
subject { scope.inbound_projects }
context 'without scoped projects' do
context 'when self referential' do
let(:includes_project) { source_project }
it { is_expected.to be_truthy }
context 'when no projects are added to the scope' do
it 'returns the project defining the scope' do
expect(subject).to contain_exactly(current_project)
end
end
context 'with scoped projects' do
include_context 'with scoped projects'
context 'when projects are added to the scope' do
include_context 'with accessible and inaccessible projects'
context 'when project is in outbound scope' do
let(:includes_project) { outbound_scoped_project }
it { is_expected.to be_truthy }
it 'returns all projects that can be accessed from a given scope' do
expect(subject).to contain_exactly(current_project, inbound_allowlist_project)
end
end
end
context 'when project is in inbound scope' do
let(:includes_project) { inbound_scoped_project }
RSpec.shared_examples 'enforces outbound scope only' do
include_context 'with accessible and inaccessible projects'
it { is_expected.to be_falsey }
end
where(:accessed_project, :result) do
ref(:current_project) | true
ref(:inbound_allowlist_project) | false
ref(:unscoped_project1) | false
ref(:unscoped_project2) | false
ref(:outbound_allowlist_project) | true
ref(:inbound_accessible_project) | false
ref(:fully_accessible_project) | true
end
context 'when project is linked to a different project' do
let(:includes_project) { unscoped_project1 }
with_them do
it { is_expected.to eq(result) }
end
end
it { is_expected.to be_falsey }
end
describe 'accessible?' do
subject { scope.accessible?(accessed_project) }
context 'when project is unlinked to a project' do
let(:includes_project) { unscoped_project2 }
context 'with inbound and outbound scopes enabled' do
context 'when inbound and outbound access setup' do
include_context 'with accessible and inaccessible projects'
it { is_expected.to be_falsey }
end
context 'when project scope setting is disabled' do
let(:includes_project) { unscoped_project1 }
before do
source_project.ci_outbound_job_token_scope_enabled = false
where(:accessed_project, :result) do
ref(:current_project) | true
ref(:inbound_allowlist_project) | false
ref(:unscoped_project1) | false
ref(:unscoped_project2) | false
ref(:outbound_allowlist_project) | false
ref(:inbound_accessible_project) | false
ref(:fully_accessible_project) | true
end
it 'considers any project to be part of the scope' do
expect(subject).to be_truthy
with_them do
it 'allows self and projects allowed from both directions' do
is_expected.to eq(result)
end
end
end
end
context 'with inbound scope enabled and outbound scope disabled' do
before do
source_project.ci_inbound_job_token_scope_enabled = true
source_project.ci_outbound_job_token_scope_enabled = false
source_project.save!
end
include_context 'with accessible and inaccessible projects'
where(:accessed_project, :result) do
ref(:current_project) | true
ref(:inbound_allowlist_project) | false
ref(:unscoped_project1) | false
ref(:unscoped_project2) | false
ref(:outbound_allowlist_project) | false
ref(:inbound_accessible_project) | true
ref(:fully_accessible_project) | true
end
with_them do
it { is_expected.to eq(result) }
end
end
context 'with inbound scope disabled and outbound scope enabled' do
before do
source_project.ci_inbound_job_token_scope_enabled = false
source_project.ci_outbound_job_token_scope_enabled = true
source_project.save!
end
include_examples 'enforces outbound scope only'
end
context 'when inbound scope flag disabled' do
before do
stub_feature_flags(ci_inbound_job_token_scope: false)
end
include_examples 'enforces outbound scope only'
end
end
end

View File

@ -2478,7 +2478,10 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
before do
current_user.set_ci_job_token_scope!(job)
current_user.external = external_user
scope_project.update!(ci_outbound_job_token_scope_enabled: token_scope_enabled)
scope_project.update!(
ci_outbound_job_token_scope_enabled: token_scope_enabled,
ci_inbound_job_token_scope_enabled: token_scope_enabled
)
end
it "enforces the expected permissions" do

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
include HttpBasicAuthHelpers
include DependencyProxyHelpers
include Ci::JobTokenScopeHelpers
include HttpIOHelpers
@ -312,7 +313,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'normal authentication' do
context 'job with artifacts' do
context 'when artifacts are stored locally' do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, project: project) }
subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) }
@ -329,11 +330,12 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
stub_licensed_features(cross_project_pipelines: true)
end
it_behaves_like 'downloads artifact'
context 'when job token scope is enabled' do
before do
other_job.project.ci_cd_settings.update!(job_token_scope_enabled: true)
other_job.project.ci_cd_settings.update!(
job_token_scope_enabled: true,
inbound_job_token_scope_enabled: true
)
end
it 'does not allow downloading artifacts' do
@ -343,7 +345,9 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
context 'when project is added to the job token scope' do
let!(:link) { create(:ci_job_token_project_scope_link, source_project: other_job.project, target_project: job.project) }
before do
make_project_fully_accessible(other_job.project, job.project)
end
it_behaves_like 'downloads artifact'
end

View File

@ -36,6 +36,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
@ -60,6 +66,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }

View File

@ -50,6 +50,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
@ -78,6 +84,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }

View File

@ -60,7 +60,7 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(false).to(true)
end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
end
context 'when invalid target project is provided' do

View File

@ -5,7 +5,13 @@ require 'spec_helper'
RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
let_it_be(:project) do
create(:project,
ci_outbound_job_token_scope_enabled: true,
ci_inbound_job_token_scope_enabled: true
)
end
let_it_be(:target_project) { create(:project) }
let_it_be(:link) do
@ -66,7 +72,7 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_int
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(true).to(false)
end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(-1)
end
context 'when invalid target project is provided' do

View File

@ -88,7 +88,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
let(:job) { create(:ci_build, :running, user: user) }
let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(url, job_token: job.token) }
@ -130,7 +130,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
let(:job) { create(:ci_build, :running, user: user) }
let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(url, job_token: job.token) }
@ -229,8 +229,8 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
get api(package_url, user)
end
pipeline = create(:ci_pipeline, user: user)
create(:ci_build, user: user, pipeline: pipeline)
pipeline = create(:ci_pipeline, user: user, project: project)
create(:ci_build, user: user, pipeline: pipeline, project: project)
create(:package_build_info, package: package1, pipeline: pipeline)
expect do
@ -262,7 +262,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
it_behaves_like 'no destroy url'
context 'with JOB-TOKEN auth' do
let(:job) { create(:ci_build, :running, user: user) }
let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(package_url, job_token: job.token) }
@ -324,7 +324,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
let(:job) { create(:ci_build, :running, user: user) }
let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(package_url, job_token: job.token) }
@ -430,7 +430,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
let(:job) { create(:ci_build, :running, user: user) }
let(:job) { create(:ci_build, :running, user: user, project: project) }
it 'returns 403 for a user without enough permissions' do
project.add_developer(user)

Some files were not shown because too many files have changed in this diff Show More