Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4fbfae83af
commit
ae9f43a2c4
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
- |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
# Cop supports --autocorrect.
|
||||
Performance/ConcurrentMonotonicTime:
|
||||
Details: grace period
|
||||
Exclude:
|
||||
- 'lib/gitlab/database/connection_timer.rb'
|
||||
|
|
@ -1 +1 @@
|
|||
ed85386e4a808bab0023c28b9b1d7e103b50050e
|
||||
54a1400cccb31b1869a7a9b735bad1cfb047d3bb
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ query getPackageDetails(
|
|||
createdAt
|
||||
version
|
||||
status
|
||||
_links {
|
||||
webPath
|
||||
}
|
||||
tags(first: 1) {
|
||||
nodes {
|
||||
id
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { initAdminNewRunner } from '~/ci/runner/admin_new_runner';
|
||||
|
||||
initAdminNewRunner();
|
||||
|
|
@ -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.',
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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**.
|
||||
|
||||

|
||||
1. From the **User administration** dropdown list, select **Unlock**.
|
||||
|
||||
## Unlock a user from the command line
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -27,7 +27,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def current_clock_value
|
||||
Concurrent.monotonic_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}" }
|
||||
|
||||
|
|
|
|||
|
|
@ -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}" }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue