Merge branch 'master' into 44846-improve-web-ide-left-panel-and-modes
This commit is contained in:
commit
d9f3af500c
|
|
@ -9,6 +9,10 @@ terms.
|
|||
|
||||
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
|
||||
|
||||
All Documentation content that resides under the [doc/ directory](/doc) of this
|
||||
repository is licensed under Creative Commons:
|
||||
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
_This notice should stay as the first item in the CONTRIBUTING.md file._
|
||||
|
||||
---
|
||||
|
|
|
|||
6
Gemfile
6
Gemfile
|
|
@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0'
|
|||
gem 'faraday', '~> 0.12'
|
||||
|
||||
# Authentication libraries
|
||||
gem 'devise', '~> 4.2'
|
||||
gem 'devise', '~> 4.4'
|
||||
gem 'doorkeeper', '~> 4.3'
|
||||
gem 'doorkeeper-openid_connect', '~> 1.3'
|
||||
gem 'omniauth', '~> 1.8'
|
||||
|
|
@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
|
|||
gem 'omniauth-azure-oauth2', '~> 0.0.9'
|
||||
gem 'omniauth-cas3', '~> 1.1.4'
|
||||
gem 'omniauth-facebook', '~> 4.0.0'
|
||||
gem 'omniauth-github', '~> 1.1.1'
|
||||
gem 'omniauth-github', '~> 1.3'
|
||||
gem 'omniauth-gitlab', '~> 1.0.2'
|
||||
gem 'omniauth-google-oauth2', '~> 0.5.3'
|
||||
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
|
||||
|
|
@ -90,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist'
|
|||
|
||||
# API
|
||||
gem 'grape', '~> 1.0'
|
||||
gem 'grape-entity', '~> 0.6.0'
|
||||
gem 'grape-entity', '~> 0.7.1'
|
||||
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
|
||||
|
||||
# Disable strong_params so that Mash does not respond to :permitted?
|
||||
|
|
|
|||
33
Gemfile.lock
33
Gemfile.lock
|
|
@ -143,7 +143,7 @@ GEM
|
|||
connection_pool (2.2.1)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.3)
|
||||
crass (1.0.4)
|
||||
creole (0.5.0)
|
||||
css_parser (1.5.0)
|
||||
addressable
|
||||
|
|
@ -162,10 +162,10 @@ GEM
|
|||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
device_detector (1.0.0)
|
||||
devise (4.2.0)
|
||||
devise (4.4.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0, < 5.1)
|
||||
railties (>= 4.1.0, < 6.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (3.0.0)
|
||||
|
|
@ -366,8 +366,8 @@ GEM
|
|||
rack (>= 1.3.0)
|
||||
rack-accept
|
||||
virtus (>= 1.0.0)
|
||||
grape-entity (0.6.0)
|
||||
activesupport
|
||||
grape-entity (0.7.1)
|
||||
activesupport (>= 4.0)
|
||||
multi_json (>= 1.3.2)
|
||||
grape-route-helpers (2.1.0)
|
||||
activesupport
|
||||
|
|
@ -546,9 +546,9 @@ GEM
|
|||
omniauth (~> 1.2)
|
||||
omniauth-facebook (4.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github (1.1.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-github (1.3.0)
|
||||
omniauth (~> 1.5)
|
||||
omniauth-oauth2 (>= 1.4.0, < 2.0)
|
||||
omniauth-gitlab (1.0.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.0)
|
||||
|
|
@ -646,7 +646,7 @@ GEM
|
|||
pry (>= 0.9.10)
|
||||
public_suffix (3.0.2)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
rack (1.6.9)
|
||||
rack (1.6.10)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (4.4.1)
|
||||
|
|
@ -694,7 +694,7 @@ GEM
|
|||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.18.0)
|
||||
rake (12.3.0)
|
||||
rake (12.3.1)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
|
|
@ -735,8 +735,9 @@ GEM
|
|||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.3.1)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
responders (2.4.0)
|
||||
actionpack (>= 4.2.0, < 5.3)
|
||||
railties (>= 4.2.0, < 5.3)
|
||||
rest-client (2.0.2)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
|
|
@ -966,7 +967,7 @@ GEM
|
|||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
equalizer (~> 0.0, >= 0.0.9)
|
||||
vmstat (2.3.0)
|
||||
warden (1.2.6)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
webmock (2.3.2)
|
||||
addressable (>= 2.3.6)
|
||||
|
|
@ -1028,7 +1029,7 @@ DEPENDENCIES
|
|||
deckar01-task_list (= 2.0.0)
|
||||
default_value_for (~> 3.0.0)
|
||||
device_detector
|
||||
devise (~> 4.2)
|
||||
devise (~> 4.4)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
diffy (~> 3.1.0)
|
||||
doorkeeper (~> 4.3)
|
||||
|
|
@ -1072,7 +1073,7 @@ DEPENDENCIES
|
|||
google-protobuf (= 3.5.1)
|
||||
gpgme
|
||||
grape (~> 1.0)
|
||||
grape-entity (~> 0.6.0)
|
||||
grape-entity (~> 0.7.1)
|
||||
grape-route-helpers (~> 2.1.0)
|
||||
grape_logging (~> 1.7)
|
||||
grpc (~> 1.11.0)
|
||||
|
|
@ -1113,7 +1114,7 @@ DEPENDENCIES
|
|||
omniauth-azure-oauth2 (~> 0.0.9)
|
||||
omniauth-cas3 (~> 1.1.4)
|
||||
omniauth-facebook (~> 4.0.0)
|
||||
omniauth-github (~> 1.1.1)
|
||||
omniauth-github (~> 1.3)
|
||||
omniauth-gitlab (~> 1.0.2)
|
||||
omniauth-google-oauth2 (~> 0.5.3)
|
||||
omniauth-kerberos (~> 0.3.0)
|
||||
|
|
|
|||
7
LICENSE
7
LICENSE
|
|
@ -4,4 +4,9 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
All Documentation content that resides under the doc/ directory of this
|
||||
repository is licensed under Creative Commons: CC BY-SA 4.0.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
<div class="environments-container">
|
||||
|
||||
<loading-icon
|
||||
class="prepend-top-default"
|
||||
label="Loading environments"
|
||||
v-if="isLoading"
|
||||
size="3"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default {
|
|||
return `${this.changedIcon}-solid`;
|
||||
},
|
||||
changedIconClass() {
|
||||
return `multi-${this.changedIcon} prepend-left-5 pull-left`;
|
||||
return `multi-${this.changedIcon} pull-left`;
|
||||
},
|
||||
tooltipTitle() {
|
||||
if (!this.showTooltip) return undefined;
|
||||
|
|
@ -79,13 +79,7 @@ export default {
|
|||
class="ide-file-changed-icon"
|
||||
>
|
||||
<icon
|
||||
v-if="file.staged && showStagedIcon"
|
||||
:name="stagedIcon"
|
||||
:size="12"
|
||||
:css-classes="changedIconClass"
|
||||
/>
|
||||
<icon
|
||||
v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
|
||||
v-if="file.changed || file.tempFile || file.staged"
|
||||
:name="changedIcon"
|
||||
:size="12"
|
||||
:css-classes="changedIconClass"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
|
||||
},
|
||||
iconClass() {
|
||||
return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`;
|
||||
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,29 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import fileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { n__, __, sprintf } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import router from '../ide_router';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
import fileStatusIcon from './repo_file_status_icon.vue';
|
||||
import changedFileIcon from './changed_file_icon.vue';
|
||||
import mrFileIcon from './mr_file_icon.vue';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
import FileStatusIcon from './repo_file_status_icon.vue';
|
||||
import ChangedFileIcon from './changed_file_icon.vue';
|
||||
import MrFileIcon from './mr_file_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepoFile',
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
skeletonLoadingContainer,
|
||||
newDropdown,
|
||||
fileStatusIcon,
|
||||
fileIcon,
|
||||
changedFileIcon,
|
||||
mrFileIcon,
|
||||
SkeletonLoadingContainer,
|
||||
NewDropdown,
|
||||
FileStatusIcon,
|
||||
FileIcon,
|
||||
ChangedFileIcon,
|
||||
MrFileIcon,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
|
|
@ -34,6 +41,34 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getChangesInFolder',
|
||||
'getUnstagedFilesCountForPath',
|
||||
'getStagedFilesCountForPath',
|
||||
]),
|
||||
folderUnstagedCount() {
|
||||
return this.getUnstagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
folderStagedCount() {
|
||||
return this.getStagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
changesCount() {
|
||||
return this.getChangesInFolder(this.file.path);
|
||||
},
|
||||
folderChangesTooltip() {
|
||||
if (this.changesCount === 0) return undefined;
|
||||
|
||||
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
|
||||
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
|
||||
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
|
||||
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
|
||||
}
|
||||
|
||||
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
|
||||
unstaged: this.folderUnstagedCount,
|
||||
staged: this.folderStagedCount,
|
||||
});
|
||||
},
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
|
|
@ -53,10 +88,19 @@ export default {
|
|||
'is-open': this.file.opened,
|
||||
};
|
||||
},
|
||||
showTreeChangesCount() {
|
||||
return this.isTree && this.changesCount > 0 && !this.file.opened;
|
||||
},
|
||||
showChangedFileIcon() {
|
||||
return this.file.changed || this.file.tempFile || this.file.staged;
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
if (this.file.type === 'blob' && this.file.active) {
|
||||
this.$el.scrollIntoView();
|
||||
this.$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -104,8 +148,23 @@ export default {
|
|||
<mr-file-icon
|
||||
v-if="file.mrChange"
|
||||
/>
|
||||
<span
|
||||
v-if="showTreeChangesCount"
|
||||
class="ide-tree-changes"
|
||||
>
|
||||
{{ changesCount }}
|
||||
<icon
|
||||
v-tooltip
|
||||
:title="folderChangesTooltip"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
name="file-modified"
|
||||
:size="12"
|
||||
css-classes="prepend-left-5 multi-file-modified"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
v-if="file.changed || file.tempFile || file.staged"
|
||||
v-else-if="showChangedFileIcon"
|
||||
:file="file"
|
||||
:show-tooltip="true"
|
||||
:show-staged-icon="true"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
<<<<<<< HEAD
|
||||
import { activityBarViews } from '../constants';
|
||||
=======
|
||||
import { __ } from '~/locale';
|
||||
import { getChangesCountForFiles, filePathMatches } from './utils';
|
||||
>>>>>>> master
|
||||
|
||||
export const activeFile = state => state.openFiles.find(file => file.active) || null;
|
||||
|
||||
|
|
@ -52,11 +57,27 @@ export const allBlobs = state =>
|
|||
}, [])
|
||||
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
|
||||
|
||||
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
|
||||
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
|
||||
|
||||
export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
|
||||
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
|
||||
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
|
||||
|
||||
export const getChangesInFolder = state => path => {
|
||||
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
|
||||
const stagedFilesCount = state.stagedFiles.filter(
|
||||
f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
|
||||
).length;
|
||||
|
||||
return changedFilesCount + stagedFilesCount;
|
||||
};
|
||||
|
||||
export const getUnstagedFilesCountForPath = state => path =>
|
||||
getChangesCountForFiles(state.changedFiles, path);
|
||||
|
||||
export const getStagedFilesCountForPath = state => path =>
|
||||
getChangesCountForFiles(state.stagedFiles, path);
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export const dataStructure = () => ({
|
|||
raw: '',
|
||||
content: '',
|
||||
parentTreeUrl: '',
|
||||
parentPath: '',
|
||||
renderError: false,
|
||||
base64: false,
|
||||
editorRow: 1,
|
||||
|
|
@ -43,6 +42,7 @@ export const dataStructure = () => ({
|
|||
viewMode: 'edit',
|
||||
previewMode: null,
|
||||
size: 0,
|
||||
parentPath: null,
|
||||
lastOpenedAt: 0,
|
||||
});
|
||||
|
||||
|
|
@ -83,7 +83,6 @@ export const decorateData = entity => {
|
|||
opened,
|
||||
active,
|
||||
parentTreeUrl,
|
||||
parentPath,
|
||||
changed,
|
||||
renderError,
|
||||
content,
|
||||
|
|
@ -91,6 +90,7 @@ export const decorateData = entity => {
|
|||
previewMode,
|
||||
file_lock,
|
||||
html,
|
||||
parentPath,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -137,3 +137,9 @@ export const sortTree = sortedTree =>
|
|||
}),
|
||||
)
|
||||
.sort(sortTreesByTypeAndName);
|
||||
|
||||
export const filePathMatches = (f, path) =>
|
||||
f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
|
||||
|
||||
export const getChangesCountForFiles = (files, path) =>
|
||||
files.filter(f => filePathMatches(f, path)).length;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@
|
|||
toggleMoreParticipants() {
|
||||
this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
|
||||
},
|
||||
onClickCollapsedIcon() {
|
||||
this.$emit('toggleSidebar');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -82,6 +85,7 @@
|
|||
data-container="body"
|
||||
data-placement="left"
|
||||
:title="participantLabel"
|
||||
@click="onClickCollapsedIcon"
|
||||
>
|
||||
<i
|
||||
class="fa fa-users"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import Store from '../../stores/sidebar_store';
|
||||
import eventHub from '../../event_hub';
|
||||
import Flash from '../../../flash';
|
||||
import { __ } from '../../../locale';
|
||||
import subscriptions from './subscriptions.vue';
|
||||
|
|
@ -20,12 +19,6 @@ export default {
|
|||
store: new Store(),
|
||||
};
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('toggleSubscription', this.onToggleSubscription);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('toggleSubscription', this.onToggleSubscription);
|
||||
},
|
||||
methods: {
|
||||
onToggleSubscription() {
|
||||
this.mediator.toggleSubscription()
|
||||
|
|
@ -42,6 +35,7 @@ export default {
|
|||
<subscriptions
|
||||
:loading="store.isFetching.subscriptions"
|
||||
:subscribed="store.subscribed"
|
||||
@toggleSubscription="onToggleSubscription"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -47,8 +47,25 @@
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* We need to emit this event on both component & eventHub
|
||||
* for 2 dependencies;
|
||||
*
|
||||
* 1. eventHub: This component is used in Issue Boards sidebar
|
||||
* where component template is part of HAML
|
||||
* and event listeners are tied to app's eventHub.
|
||||
* 2. Component: This compone is also used in Epics in EE
|
||||
* where listeners are tied to component event.
|
||||
*/
|
||||
toggleSubscription() {
|
||||
// App's eventHub event emission.
|
||||
eventHub.$emit('toggleSubscription', this.id);
|
||||
|
||||
// Component event emission.
|
||||
this.$emit('toggleSubscription', this.id);
|
||||
},
|
||||
onClickCollapsedIcon() {
|
||||
this.$emit('toggleSidebar');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -56,7 +73,10 @@
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div class="sidebar-collapsed-icon">
|
||||
<div
|
||||
class="sidebar-collapsed-icon"
|
||||
@click="onClickCollapsedIcon"
|
||||
>
|
||||
<span
|
||||
v-tooltip
|
||||
:title="notificationTooltip"
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
export default {
|
||||
name: 'time-tracking-spent-only-pane',
|
||||
props: {
|
||||
timeSpentHumanReadable: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="time-tracking-spend-only-pane">
|
||||
<span class="bold">Spent:</span>
|
||||
{{ timeSpentHumanReadable }}
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'TimeTrackingSpentOnlyPane',
|
||||
props: {
|
||||
timeSpentHumanReadable: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="time-tracking-spend-only-pane">
|
||||
<span class="bold">Spent:</span>
|
||||
{{ timeSpentHumanReadable }}
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import TimeTrackingHelpState from './help_state.vue';
|
||||
import TimeTrackingCollapsedState from './collapsed_state.vue';
|
||||
import timeTrackingSpentOnlyPane from './spent_only_pane';
|
||||
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
|
||||
import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
|
||||
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
|
||||
import TimeTrackingComparisonPane from './comparison_pane.vue';
|
||||
|
|
@ -13,7 +13,7 @@ export default {
|
|||
components: {
|
||||
TimeTrackingCollapsedState,
|
||||
TimeTrackingEstimateOnlyPane,
|
||||
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
|
||||
TimeTrackingSpentOnlyPane,
|
||||
TimeTrackingNoTrackingPane,
|
||||
TimeTrackingComparisonPane,
|
||||
TimeTrackingHelpState,
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@
|
|||
a {
|
||||
color: $gl-text-color;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
|
@ -462,6 +463,7 @@
|
|||
|
||||
.issuable-header-text {
|
||||
padding-right: 35px;
|
||||
word-break: break-word;
|
||||
|
||||
> strong {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
|
|
|
|||
|
|
@ -539,14 +539,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.multi-file-additions,
|
||||
.multi-file-additions-solid {
|
||||
fill: $green-500;
|
||||
.multi-file-addition,
|
||||
.multi-file-addition-solid {
|
||||
color: $green-500;
|
||||
}
|
||||
|
||||
.multi-file-modified,
|
||||
.multi-file-modified-solid {
|
||||
fill: $orange-500;
|
||||
color: $orange-500;
|
||||
}
|
||||
|
||||
.multi-file-commit-list-collapsed {
|
||||
|
|
@ -978,6 +978,12 @@
|
|||
color: $gl-text-color-secondary;
|
||||
}
|
||||
|
||||
.ide-tree-changes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ide-new-modal-label {
|
||||
line-height: 34px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
redirect_to project_settings_ci_cd_path(@project)
|
||||
end
|
||||
|
||||
def toggle_group_runners
|
||||
project.toggle_ci_cd_settings!(:group_runners_enabled)
|
||||
|
||||
redirect_to project_settings_ci_cd_path(@project)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def set_runner
|
||||
|
|
|
|||
|
|
@ -67,10 +67,18 @@ module Projects
|
|||
|
||||
def define_runners_variables
|
||||
@project_runners = @project.runners.ordered
|
||||
@assignable_runners = current_user.ci_authorized_runners
|
||||
.assignable_for(project).ordered.page(params[:page]).per(20)
|
||||
|
||||
@assignable_runners = current_user
|
||||
.ci_authorized_runners
|
||||
.assignable_for(project)
|
||||
.ordered
|
||||
.page(params[:page]).per(20)
|
||||
|
||||
@shared_runners = ::Ci::Runner.shared.active
|
||||
|
||||
@shared_runners_count = @shared_runners.count(:all)
|
||||
|
||||
@group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
|
||||
end
|
||||
|
||||
def define_secret_variables
|
||||
|
|
|
|||
|
|
@ -14,31 +14,49 @@ module Ci
|
|||
has_many :builds
|
||||
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :projects, through: :runner_projects
|
||||
has_many :runner_namespaces
|
||||
has_many :groups, through: :runner_namespaces
|
||||
|
||||
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
|
||||
|
||||
before_validation :set_default_values
|
||||
|
||||
scope :specific, ->() { where(is_shared: false) }
|
||||
scope :shared, ->() { where(is_shared: true) }
|
||||
scope :active, ->() { where(active: true) }
|
||||
scope :paused, ->() { where(active: false) }
|
||||
scope :online, ->() { where('contacted_at > ?', contact_time_deadline) }
|
||||
scope :ordered, ->() { order(id: :desc) }
|
||||
scope :specific, -> { where(is_shared: false) }
|
||||
scope :shared, -> { where(is_shared: true) }
|
||||
scope :active, -> { where(active: true) }
|
||||
scope :paused, -> { where(active: false) }
|
||||
scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
|
||||
scope :ordered, -> { order(id: :desc) }
|
||||
|
||||
scope :owned_or_shared, ->(project_id) do
|
||||
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
|
||||
.where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
|
||||
scope :belonging_to_project, -> (project_id) {
|
||||
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
|
||||
}
|
||||
|
||||
scope :belonging_to_parent_group_of_project, -> (project_id) {
|
||||
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
|
||||
hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups).base_and_ancestors
|
||||
|
||||
joins(:groups).where(namespaces: { id: hierarchy_groups })
|
||||
}
|
||||
|
||||
scope :owned_or_shared, -> (project_id) do
|
||||
union = Gitlab::SQL::Union.new(
|
||||
[belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
|
||||
remove_duplicates: false
|
||||
)
|
||||
from("(#{union.to_sql}) ci_runners")
|
||||
end
|
||||
|
||||
scope :assignable_for, ->(project) do
|
||||
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
|
||||
# Without that, placeholders would miss one and couldn't match.
|
||||
where(locked: false)
|
||||
.where.not("id IN (#{project.runners.select(:id).to_sql})").specific
|
||||
.where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
|
||||
.specific
|
||||
end
|
||||
|
||||
validate :tag_constraints
|
||||
validate :either_projects_or_group
|
||||
validates :access_level, presence: true
|
||||
|
||||
acts_as_taggable
|
||||
|
|
@ -50,6 +68,12 @@ module Ci
|
|||
ref_protected: 1
|
||||
}
|
||||
|
||||
enum runner_type: {
|
||||
instance_type: 1,
|
||||
group_type: 2,
|
||||
project_type: 3
|
||||
}
|
||||
|
||||
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
|
||||
|
||||
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
|
||||
|
|
@ -120,6 +144,14 @@ module Ci
|
|||
!shared?
|
||||
end
|
||||
|
||||
def assigned_to_group?
|
||||
runner_namespaces.any?
|
||||
end
|
||||
|
||||
def assigned_to_project?
|
||||
runner_projects.any?
|
||||
end
|
||||
|
||||
def can_pick?(build)
|
||||
return false if self.ref_protected? && !build.protected?
|
||||
|
||||
|
|
@ -174,6 +206,12 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def pick_build!(build)
|
||||
if can_pick?(build)
|
||||
tick_runner_queue
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cleanup_runner_queue
|
||||
|
|
@ -205,7 +243,17 @@ module Ci
|
|||
end
|
||||
|
||||
def assignable_for?(project_id)
|
||||
is_shared? || projects.exists?(id: project_id)
|
||||
self.class.owned_or_shared(project_id).where(id: self.id).any?
|
||||
end
|
||||
|
||||
def either_projects_or_group
|
||||
if groups.many?
|
||||
errors.add(:runner, 'can only be assigned to one group')
|
||||
end
|
||||
|
||||
if assigned_to_group? && assigned_to_project?
|
||||
errors.add(:runner, 'can only be assigned either to projects or to a group')
|
||||
end
|
||||
end
|
||||
|
||||
def accepting_tags?(build)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
module Ci
|
||||
class RunnerNamespace < ActiveRecord::Base
|
||||
extend Gitlab::Ci::Model
|
||||
|
||||
belongs_to :runner
|
||||
belongs_to :namespace, class_name: '::Namespace'
|
||||
belongs_to :group, class_name: '::Group', foreign_key: :namespace_id
|
||||
end
|
||||
end
|
||||
|
|
@ -9,6 +9,7 @@ class Group < Namespace
|
|||
include SelectForProjectAuthorization
|
||||
include LoadedInGroupList
|
||||
include GroupDescendant
|
||||
include TokenAuthenticatable
|
||||
|
||||
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
|
||||
alias_method :members, :group_members
|
||||
|
|
@ -43,6 +44,8 @@ class Group < Namespace
|
|||
|
||||
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
add_authentication_token_field :runners_token
|
||||
|
||||
after_create :post_create_hook
|
||||
after_destroy :post_destroy_hook
|
||||
after_save :update_two_factor_requirement
|
||||
|
|
@ -294,6 +297,13 @@ class Group < Namespace
|
|||
refresh_members_authorized_projects(blocking: false)
|
||||
end
|
||||
|
||||
# each existing group needs to have a `runners_token`.
|
||||
# we do this on read since migrating all existing groups is not a feasible
|
||||
# solution.
|
||||
def runners_token
|
||||
ensure_runners_token!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_two_factor_requirement
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ class Namespace < ActiveRecord::Base
|
|||
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :project_statistics
|
||||
|
||||
has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace'
|
||||
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
|
||||
|
||||
# This should _not_ be `inverse_of: :namespace`, because that would also set
|
||||
# `user.namespace` when this user creates a group with themselves as `owner`.
|
||||
belongs_to :owner, class_name: "User"
|
||||
|
|
|
|||
|
|
@ -230,13 +230,11 @@ class Project < ActiveRecord::Base
|
|||
has_many :project_deploy_tokens
|
||||
has_many :deploy_tokens, through: :project_deploy_tokens
|
||||
|
||||
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
|
||||
|
||||
has_one :auto_devops, class_name: 'ProjectAutoDevops'
|
||||
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
|
||||
|
||||
has_many :project_badges, class_name: 'ProjectBadge'
|
||||
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
|
||||
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true
|
||||
|
||||
accepts_nested_attributes_for :variables, allow_destroy: true
|
||||
accepts_nested_attributes_for :project_feature, update_only: true
|
||||
|
|
@ -247,6 +245,7 @@ class Project < ActiveRecord::Base
|
|||
delegate :members, to: :team, prefix: true
|
||||
delegate :add_user, :add_users, to: :team
|
||||
delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
|
||||
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
|
||||
|
||||
# Validations
|
||||
validates :creator, presence: true, on: :create
|
||||
|
|
@ -332,6 +331,11 @@ class Project < ActiveRecord::Base
|
|||
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
|
||||
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
|
||||
|
||||
scope :with_group_runners_enabled, -> do
|
||||
joins(:ci_cd_settings)
|
||||
.where(project_ci_cd_settings: { group_runners_enabled: true })
|
||||
end
|
||||
|
||||
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
|
||||
|
||||
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
|
||||
|
|
@ -1301,12 +1305,17 @@ class Project < ActiveRecord::Base
|
|||
@shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
|
||||
end
|
||||
|
||||
def active_shared_runners
|
||||
@active_shared_runners ||= shared_runners.active
|
||||
def group_runners
|
||||
@group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
|
||||
end
|
||||
|
||||
def all_runners
|
||||
union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners])
|
||||
Ci::Runner.from("(#{union.to_sql}) ci_runners")
|
||||
end
|
||||
|
||||
def any_runners?(&block)
|
||||
active_runners.any?(&block) || active_shared_runners.any?(&block)
|
||||
all_runners.active.any?(&block)
|
||||
end
|
||||
|
||||
def valid_runners_token?(token)
|
||||
|
|
@ -1874,6 +1883,10 @@ class Project < ActiveRecord::Base
|
|||
[]
|
||||
end
|
||||
|
||||
def toggle_ci_cd_settings!(settings_attribute)
|
||||
ci_cd_settings.toggle!(settings_attribute)
|
||||
end
|
||||
|
||||
def gitlab_deploy_token
|
||||
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
class ProjectCiCdSetting < ActiveRecord::Base
|
||||
belongs_to :project
|
||||
belongs_to :project, inverse_of: :ci_cd_settings
|
||||
|
||||
# The version of the schema that first introduced this model/table.
|
||||
MINIMUM_SCHEMA_VERSION = 20180403035759
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ module Ci
|
|||
builds =
|
||||
if runner.shared?
|
||||
builds_for_shared_runner
|
||||
elsif runner.group_type?
|
||||
builds_for_group_runner
|
||||
else
|
||||
builds_for_specific_runner
|
||||
builds_for_project_runner
|
||||
end
|
||||
|
||||
valid = true
|
||||
|
|
@ -75,15 +77,24 @@ module Ci
|
|||
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
|
||||
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
|
||||
|
||||
# Implement fair scheduling
|
||||
# this returns builds that are ordered by number of running builds
|
||||
# we prefer projects that don't use shared runners at all
|
||||
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
|
||||
# Implement fair scheduling
|
||||
# this returns builds that are ordered by number of running builds
|
||||
# we prefer projects that don't use shared runners at all
|
||||
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
|
||||
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
|
||||
end
|
||||
|
||||
def builds_for_specific_runner
|
||||
new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC')
|
||||
def builds_for_project_runner
|
||||
new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
|
||||
end
|
||||
|
||||
def builds_for_group_runner
|
||||
hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants
|
||||
projects = Project.where(namespace_id: hierarchy_groups)
|
||||
.with_group_runners_enabled
|
||||
.with_builds_enabled
|
||||
.without_deleted
|
||||
new_builds.where(project: projects).order('id ASC')
|
||||
end
|
||||
|
||||
def running_builds_for_shared_runners
|
||||
|
|
@ -97,10 +108,6 @@ module Ci
|
|||
builds
|
||||
end
|
||||
|
||||
def shared_runner_build_limits_feature_enabled?
|
||||
ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
|
||||
end
|
||||
|
||||
def register_failure
|
||||
failed_attempt_counter.increment
|
||||
attempt_counter.increment
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
module Ci
|
||||
class UpdateBuildQueueService
|
||||
def execute(build)
|
||||
build.project.runners.each do |runner|
|
||||
if runner.can_pick?(build)
|
||||
runner.tick_runner_queue
|
||||
end
|
||||
end
|
||||
tick_for(build, build.project.all_runners)
|
||||
end
|
||||
|
||||
return unless build.project.shared_runners_enabled?
|
||||
private
|
||||
|
||||
Ci::Runner.shared.each do |runner|
|
||||
if runner.can_pick?(build)
|
||||
runner.tick_runner_queue
|
||||
end
|
||||
def tick_for(build, runners)
|
||||
runners.each do |runner|
|
||||
runner.pick_build!(build)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
%td
|
||||
- if runner.shared?
|
||||
%span.label.label-success shared
|
||||
- elsif runner.group_type?
|
||||
%span.label.label-success group
|
||||
- else
|
||||
%span.label.label-info specific
|
||||
- if runner.locked?
|
||||
|
|
@ -19,7 +21,7 @@
|
|||
%td
|
||||
= runner.ip_address
|
||||
%td
|
||||
- if runner.shared?
|
||||
- if runner.shared? || runner.group_type?
|
||||
n/a
|
||||
- else
|
||||
= runner.projects.count(:all)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
%li
|
||||
%span.label.label-success shared
|
||||
\- Runner runs jobs from all unassigned projects
|
||||
%li
|
||||
%span.label.label-success group
|
||||
\- Runner runs jobs from all unassigned projects in its group
|
||||
%li
|
||||
%span.label.label-info specific
|
||||
\- Runner runs jobs from assigned projects
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
%p
|
||||
If you want Runners to build only specific projects, enable them in the table below.
|
||||
Keep in mind that this is a one way transition.
|
||||
- elsif @runner.group_type?
|
||||
.bs-callout.bs-callout-success
|
||||
%h4 This runner will process jobs from all projects in its group and subgroups
|
||||
- else
|
||||
.bs-callout.bs-callout-info
|
||||
%h4 This Runner will process jobs only from ASSIGNED projects
|
||||
|
|
|
|||
|
|
@ -42,31 +42,31 @@
|
|||
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
|
||||
= link_to admin_users_path do
|
||||
Active
|
||||
%small.badge= number_with_delimiter(User.active.count)
|
||||
%small.badge= limited_counter_with_delimiter(User.active)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
|
||||
= link_to admin_users_path(filter: "admins") do
|
||||
Admins
|
||||
%small.badge= number_with_delimiter(User.admins.count)
|
||||
%small.badge= limited_counter_with_delimiter(User.admins)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
|
||||
= link_to admin_users_path(filter: 'two_factor_enabled') do
|
||||
2FA Enabled
|
||||
%small.badge= number_with_delimiter(User.with_two_factor.count)
|
||||
%small.badge= limited_counter_with_delimiter(User.with_two_factor)
|
||||
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
|
||||
= link_to admin_users_path(filter: 'two_factor_disabled') do
|
||||
2FA Disabled
|
||||
%small.badge= number_with_delimiter(User.without_two_factor.count)
|
||||
%small.badge= limited_counter_with_delimiter(User.without_two_factor)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
|
||||
= link_to admin_users_path(filter: 'external') do
|
||||
External
|
||||
%small.badge= number_with_delimiter(User.external.count)
|
||||
%small.badge= limited_counter_with_delimiter(User.external)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
|
||||
= link_to admin_users_path(filter: "blocked") do
|
||||
Blocked
|
||||
%small.badge= number_with_delimiter(User.blocked.count)
|
||||
%small.badge= limited_counter_with_delimiter(User.blocked)
|
||||
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
|
||||
= link_to admin_users_path(filter: "wop") do
|
||||
Without projects
|
||||
%small.badge= number_with_delimiter(User.without_projects.count)
|
||||
%small.badge= limited_counter_with_delimiter(User.without_projects)
|
||||
|
||||
%ul.flex-list.content-list
|
||||
- if @users.empty?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
- active_tab = local_assigns.fetch(:active_tab, 'blank')
|
||||
- f = local_assigns.fetch(:f)
|
||||
|
||||
.project-import.row
|
||||
.col-lg-12
|
||||
.form-group.import-btn-container.clearfix
|
||||
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
|
||||
Import project from
|
||||
.import-buttons
|
||||
- if gitlab_project_import_enabled?
|
||||
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
|
||||
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
|
||||
= icon('gitlab', text: 'GitLab export')
|
||||
%div
|
||||
- if github_import_enabled?
|
||||
= link_to new_import_github_path, class: 'btn js-import-github' do
|
||||
= icon('github', text: 'GitHub')
|
||||
%div
|
||||
- if bitbucket_import_enabled?
|
||||
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
|
||||
= icon('bitbucket', text: 'Bitbucket')
|
||||
- unless bitbucket_import_configured?
|
||||
= render 'bitbucket_import_modal'
|
||||
%div
|
||||
- if gitlab_import_enabled?
|
||||
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
|
||||
= icon('gitlab', text: 'GitLab.com')
|
||||
- unless gitlab_import_configured?
|
||||
= render 'gitlab_import_modal'
|
||||
%div
|
||||
- if google_code_import_enabled?
|
||||
= link_to new_import_google_code_path, class: 'btn import_google_code' do
|
||||
= icon('google', text: 'Google Code')
|
||||
%div
|
||||
- if fogbugz_import_enabled?
|
||||
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
|
||||
= icon('bug', text: 'Fogbugz')
|
||||
%div
|
||||
- if gitea_import_enabled?
|
||||
= link_to new_import_gitea_path, class: 'btn import_gitea' do
|
||||
= custom_icon('go_logo')
|
||||
Gitea
|
||||
%div
|
||||
- if git_import_enabled?
|
||||
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
|
||||
= icon('git', text: 'Repo by URL')
|
||||
.col-lg-12
|
||||
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
|
||||
%hr
|
||||
= render "shared/import_form", f: f
|
||||
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
|
||||
|
|
@ -57,54 +57,11 @@
|
|||
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
|
||||
= form_for @project, html: { class: 'new_project' } do |f|
|
||||
- if import_sources_enabled?
|
||||
.project-import.row
|
||||
.col-lg-12
|
||||
.form-group.import-btn-container.clearfix
|
||||
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
|
||||
Import project from
|
||||
.import-buttons
|
||||
- if gitlab_project_import_enabled?
|
||||
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
|
||||
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
|
||||
= icon('gitlab', text: 'GitLab export')
|
||||
%div
|
||||
- if github_import_enabled?
|
||||
= link_to new_import_github_path, class: 'btn js-import-github' do
|
||||
= icon('github', text: 'GitHub')
|
||||
%div
|
||||
- if bitbucket_import_enabled?
|
||||
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
|
||||
= icon('bitbucket', text: 'Bitbucket')
|
||||
- unless bitbucket_import_configured?
|
||||
= render 'bitbucket_import_modal'
|
||||
%div
|
||||
- if gitlab_import_enabled?
|
||||
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
|
||||
= icon('gitlab', text: 'GitLab.com')
|
||||
- unless gitlab_import_configured?
|
||||
= render 'gitlab_import_modal'
|
||||
%div
|
||||
- if google_code_import_enabled?
|
||||
= link_to new_import_google_code_path, class: 'btn import_google_code' do
|
||||
= icon('google', text: 'Google Code')
|
||||
%div
|
||||
- if fogbugz_import_enabled?
|
||||
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
|
||||
= icon('bug', text: 'Fogbugz')
|
||||
%div
|
||||
- if gitea_import_enabled?
|
||||
= link_to new_import_gitea_path, class: 'btn import_gitea' do
|
||||
= custom_icon('go_logo')
|
||||
Gitea
|
||||
%div
|
||||
- if git_import_enabled?
|
||||
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
|
||||
= icon('git', text: 'Repo by URL')
|
||||
.col-lg-12
|
||||
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
|
||||
%hr
|
||||
= render "shared/import_form", f: f
|
||||
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
|
||||
= render 'import_project_pane', f: f, active_tab: active_tab
|
||||
- else
|
||||
.nothing-here-block
|
||||
%h4 No import options available
|
||||
%p Contact an administrator to enable options for importing your project.
|
||||
|
||||
.save-project-loader.hide
|
||||
.center
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
%h3 Group Runners
|
||||
|
||||
.bs-callout.bs-callout-warning
|
||||
GitLab Group Runners can execute code for all the projects in this group.
|
||||
They can be managed using the #{link_to 'Runners API', help_page_path('api/runners.md')}.
|
||||
|
||||
- if @project.group
|
||||
%hr
|
||||
- if @project.group_runners_enabled?
|
||||
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do
|
||||
Disable group Runners
|
||||
- else
|
||||
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do
|
||||
Enable group Runners
|
||||
for this project
|
||||
|
||||
- if !@project.group
|
||||
This project does not belong to a group and can therefore not make use of group Runners.
|
||||
|
||||
- elsif @group_runners.empty?
|
||||
This group does not provide any group Runners yet.
|
||||
|
||||
- if can?(current_user, :admin_pipeline, @project.group)
|
||||
= render partial: 'ci/runner/how_to_setup_runner',
|
||||
locals: { registration_token: @project.group.runners_token, type: 'group' }
|
||||
- else
|
||||
Ask your group master to setup a group Runner.
|
||||
|
||||
- else
|
||||
%h4.underlined-title Available group Runners : #{@group_runners.count}
|
||||
%ul.bordered-list
|
||||
= render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
|
||||
|
|
@ -23,3 +23,7 @@
|
|||
= render 'projects/runners/specific_runners'
|
||||
.col-sm-6
|
||||
= render 'projects/runners/shared_runners'
|
||||
.row
|
||||
.col-sm-6
|
||||
.col-sm-6
|
||||
= render 'projects/runners/group_runners'
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
- else
|
||||
- runner_project = @project.runner_projects.find_by(runner_id: runner)
|
||||
= link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
|
||||
- elsif runner.specific?
|
||||
- elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
|
||||
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
|
||||
= f.hidden_field :runner_id, value: runner.id
|
||||
= f.submit 'Enable for this project', class: 'btn btn-sm'
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
|
||||
= image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: ''
|
||||
|
||||
.user-info
|
||||
.user-info.prepend-left-default.append-right-default
|
||||
.cover-title
|
||||
= @user.name
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Reconcile project templates with Auto DevOps
|
||||
merge_request: 18737
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add loading icon padding for pipeline environments
|
||||
merge_request: 18631
|
||||
author: George Tsiolis
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add padding to profile description
|
||||
merge_request: 18663
|
||||
author: George Tsiolis
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Break issue title for board card title and issuable header text
|
||||
merge_request: 18674
|
||||
author: George Tsiolis
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Output some useful information when running the rails console
|
||||
merge_request: 18697
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow group masters to configure runners for groups
|
||||
merge_request: 9646
|
||||
author: Alexis Reigel
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Inform the user when there are no project import options available
|
||||
merge_request: 18716
|
||||
author: George Tsiolis
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move TimeTrackingSpentOnlyPane vue component
|
||||
merge_request: 18710
|
||||
author: George Tsiolis
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# rubocop:disable Rails/Output
|
||||
if defined?(Rails::Console)
|
||||
# note that this will not print out when using `spring`
|
||||
justify = 15
|
||||
puts "-------------------------------------------------------------------------------------"
|
||||
puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab::REVISION})"
|
||||
puts " Gitlab Shell:".ljust(justify) + Gitlab::Shell.new.version
|
||||
puts " #{Gitlab::Database.adapter_name}:".ljust(justify) + Gitlab::Database.version
|
||||
puts "-------------------------------------------------------------------------------------"
|
||||
end
|
||||
|
|
@ -22,3 +22,16 @@ end.compact
|
|||
|
||||
Rails.application.config.action_dispatch.trusted_proxies = (
|
||||
['127.0.0.1', '::1'] + gitlab_trusted_proxies)
|
||||
|
||||
# A monkey patch to make trusted proxies work with Rails 5.0.
|
||||
# Inspired by https://github.com/rails/rails/issues/5223#issuecomment-263778719
|
||||
# Remove this monkey patch when upstream is fixed.
|
||||
if Gitlab.rails5?
|
||||
module TrustedProxyMonkeyPatch
|
||||
def ip
|
||||
@ip ||= (get_header("action_dispatch.remote_ip") || super).to_s
|
||||
end
|
||||
end
|
||||
|
||||
ActionDispatch::Request.send(:include, TrustedProxyMonkeyPatch)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -409,6 +409,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
|
||||
collection do
|
||||
post :toggle_shared_runners
|
||||
post :toggle_group_runners
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
class AddCiRunnerNamespaces < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :ci_runner_namespaces do |t|
|
||||
t.integer :runner_id
|
||||
t.integer :namespace_id
|
||||
|
||||
t.index [:runner_id, :namespace_id], unique: true
|
||||
t.index :namespace_id
|
||||
t.foreign_key :ci_runners, column: :runner_id, on_delete: :cascade
|
||||
t.foreign_key :namespaces, column: :namespace_id, on_delete: :cascade
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
class AddRunnersTokenToGroups < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :namespaces, :runners_token, :string
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
class AddRunnerTypeToCiRunners < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :ci_runners, :runner_type, :smallint
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddIndexToNamespacesRunnersToken < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :namespaces, :runners_token, unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
if index_exists?(:namespaces, :runners_token, unique: true)
|
||||
remove_index :namespaces, :runners_token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
class BackfillRunnerTypeForCiRunnersPostMigrate < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
INSTANCE_RUNNER_TYPE = 1
|
||||
PROJECT_RUNNER_TYPE = 3
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
update_column_in_batches(:ci_runners, :runner_type, INSTANCE_RUNNER_TYPE) do |table, query|
|
||||
query.where(table[:is_shared].eq(true)).where(table[:runner_type].eq(nil))
|
||||
end
|
||||
|
||||
update_column_in_batches(:ci_runners, :runner_type, PROJECT_RUNNER_TYPE) do |table, query|
|
||||
query.where(table[:is_shared].eq(false)).where(table[:runner_type].eq(nil))
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
end
|
||||
end
|
||||
15
db/schema.rb
15
db/schema.rb
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20180425131009) do
|
||||
ActiveRecord::Schema.define(version: 20180503150427) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
|
@ -444,6 +444,14 @@ ActiveRecord::Schema.define(version: 20180425131009) do
|
|||
add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
|
||||
add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
|
||||
|
||||
create_table "ci_runner_namespaces", force: :cascade do |t|
|
||||
t.integer "runner_id"
|
||||
t.integer "namespace_id"
|
||||
end
|
||||
|
||||
add_index "ci_runner_namespaces", ["namespace_id"], name: "index_ci_runner_namespaces_on_namespace_id", using: :btree
|
||||
add_index "ci_runner_namespaces", ["runner_id", "namespace_id"], name: "index_ci_runner_namespaces_on_runner_id_and_namespace_id", unique: true, using: :btree
|
||||
|
||||
create_table "ci_runner_projects", force: :cascade do |t|
|
||||
t.integer "runner_id", null: false
|
||||
t.datetime "created_at"
|
||||
|
|
@ -472,6 +480,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
|
|||
t.integer "access_level", default: 0, null: false
|
||||
t.string "ip_address"
|
||||
t.integer "maximum_timeout"
|
||||
t.integer "runner_type", limit: 2
|
||||
end
|
||||
|
||||
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
|
||||
|
|
@ -1261,6 +1270,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
|
|||
t.boolean "require_two_factor_authentication", default: false, null: false
|
||||
t.integer "two_factor_grace_period", default: 48, null: false
|
||||
t.integer "cached_markdown_version"
|
||||
t.string "runners_token"
|
||||
end
|
||||
|
||||
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
|
||||
|
|
@ -1271,6 +1281,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
|
|||
add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
|
||||
add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
|
||||
add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree
|
||||
add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
|
||||
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
|
||||
|
||||
create_table "notes", force: :cascade do |t|
|
||||
|
|
@ -2087,6 +2098,8 @@ ActiveRecord::Schema.define(version: 20180425131009) do
|
|||
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
|
||||
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
|
||||
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
|
||||
add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
|
||||
add_foreign_key "ci_runner_namespaces", "namespaces", on_delete: :cascade
|
||||
add_foreign_key "ci_runner_projects", "projects", name: "fk_4478a6f1e4", on_delete: :cascade
|
||||
add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade
|
||||
add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
|
|||
}));
|
||||
```
|
||||
|
||||
1. Don not use a singleton for the service or the store
|
||||
1. Do not use a singleton for the service or the store
|
||||
```javascript
|
||||
// bad
|
||||
class Store {
|
||||
|
|
@ -328,9 +328,11 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
|
|||
}
|
||||
}
|
||||
```
|
||||
1. Use `.vue` for Vue templates. Do not use `%template` in HAML.
|
||||
|
||||
#### Naming
|
||||
1. **Extensions**: Use `.vue` extension for Vue components.
|
||||
|
||||
1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371]).
|
||||
1. **Reference Naming**: Use PascalCase for their instances:
|
||||
```javascript
|
||||
// bad
|
||||
|
|
@ -364,6 +366,8 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
|
|||
<component my-prop="prop" />
|
||||
```
|
||||
|
||||
[#34371]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34371
|
||||
|
||||
#### Alignment
|
||||
1. Follow these alignment styles for the template method:
|
||||
1. With more than one attribute, all attributes should be on a new line:
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@ module SharedGroup
|
|||
is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER)
|
||||
end
|
||||
|
||||
step '"John Doe" is owner of group "Owned"' do
|
||||
is_member_of("John Doe", "Owned", Gitlab::Access::OWNER)
|
||||
end
|
||||
|
||||
step '"John Doe" is guest of group "Guest"' do
|
||||
is_member_of("John Doe", "Guest", Gitlab::Access::GUEST)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,10 +48,6 @@ module SharedPaths
|
|||
visit group_group_members_path(Group.find_by(name: "Owned"))
|
||||
end
|
||||
|
||||
step 'I visit group "Owned" settings page' do
|
||||
visit edit_group_path(Group.find_by(name: "Owned"))
|
||||
end
|
||||
|
||||
step 'I visit group "Owned" projects page' do
|
||||
visit projects_group_path(Group.find_by(name: "Owned"))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -149,11 +149,11 @@ module API
|
|||
expose_url(api_v4_projects_path(id: project.id))
|
||||
end
|
||||
|
||||
expose :issues, if: -> (*args) { issues_available?(*args) } do |project|
|
||||
expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project|
|
||||
expose_url(api_v4_projects_issues_path(id: project.id))
|
||||
end
|
||||
|
||||
expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project|
|
||||
expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project|
|
||||
expose_url(api_v4_projects_merge_requests_path(id: project.id))
|
||||
end
|
||||
|
||||
|
|
@ -242,13 +242,18 @@ module API
|
|||
expose :requested_at
|
||||
end
|
||||
|
||||
class Group < Grape::Entity
|
||||
expose :id, :name, :path, :description, :visibility
|
||||
class BasicGroupDetails < Grape::Entity
|
||||
expose :id
|
||||
expose :web_url
|
||||
expose :name
|
||||
end
|
||||
|
||||
class Group < BasicGroupDetails
|
||||
expose :path, :description, :visibility
|
||||
expose :lfs_enabled?, as: :lfs_enabled
|
||||
expose :avatar_url do |group, options|
|
||||
group.avatar_url(only_path: false)
|
||||
end
|
||||
expose :web_url
|
||||
expose :request_access_enabled
|
||||
expose :full_name, :full_path
|
||||
|
||||
|
|
@ -984,6 +989,13 @@ module API
|
|||
options[:current_user].authorized_projects.where(id: runner.projects)
|
||||
end
|
||||
end
|
||||
expose :groups, with: Entities::BasicGroupDetails do |runner, options|
|
||||
if options[:current_user].admin?
|
||||
runner.groups
|
||||
else
|
||||
options[:current_user].authorized_groups.where(id: runner.groups)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class RunnerRegistrationDetails < Grape::Entity
|
||||
|
|
|
|||
|
|
@ -23,10 +23,13 @@ module API
|
|||
runner =
|
||||
if runner_registration_token_valid?
|
||||
# Create shared runner. Requires admin access
|
||||
Ci::Runner.create(attributes.merge(is_shared: true))
|
||||
Ci::Runner.create(attributes.merge(is_shared: true, runner_type: :instance_type))
|
||||
elsif project = Project.find_by(runners_token: params[:token])
|
||||
# Create a specific runner for project.
|
||||
project.runners.create(attributes)
|
||||
# Create a specific runner for the project
|
||||
project.runners.create(attributes.merge(runner_type: :project_type))
|
||||
elsif group = Group.find_by(runners_token: params[:token])
|
||||
# Create a specific runner for the group
|
||||
group.runners.create(attributes.merge(runner_type: :group_type))
|
||||
end
|
||||
|
||||
break forbidden! unless runner
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ module Gitlab
|
|||
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
|
||||
].freeze
|
||||
SEARCH_CONTEXT_LINES = 3
|
||||
# In https://gitlab.com/gitlab-org/gitaly/merge_requests/698
|
||||
# We copied these two prefixes into gitaly-go, so don't change these
|
||||
# or things will break! (REBASE_WORKTREE_PREFIX and SQUASH_WORKTREE_PREFIX)
|
||||
REBASE_WORKTREE_PREFIX = 'rebase'.freeze
|
||||
SQUASH_WORKTREE_PREFIX = 'squash'.freeze
|
||||
GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
|
||||
|
|
@ -1671,10 +1674,14 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
# This function is duplicated in Gitaly-Go, don't change it!
|
||||
# https://gitlab.com/gitlab-org/gitaly/merge_requests/698
|
||||
def fresh_worktree?(path)
|
||||
File.exist?(path) && !clean_stuck_worktree(path)
|
||||
end
|
||||
|
||||
# This function is duplicated in Gitaly-Go, don't change it!
|
||||
# https://gitlab.com/gitlab-org/gitaly/merge_requests/698
|
||||
def clean_stuck_worktree(path)
|
||||
return false unless File.mtime(path) < 15.minutes.ago
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
TEMPLATES_TABLE = [
|
||||
ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
|
||||
ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
|
||||
ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
|
||||
ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
|
||||
ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw and pom.xml to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
|
||||
ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
|
||||
].freeze
|
||||
|
||||
class << self
|
||||
|
|
|
|||
|
|
@ -17,6 +17,23 @@ describe Projects::Settings::CiCdController do
|
|||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to render_template(:show)
|
||||
end
|
||||
|
||||
context 'with group runners' do
|
||||
let(:group_runner) { create(:ci_runner) }
|
||||
let(:parent_group) { create(:group) }
|
||||
let(:group) { create(:group, runners: [group_runner], parent: parent_group) }
|
||||
let(:other_project) { create(:project, group: group) }
|
||||
let!(:project_runner) { create(:ci_runner, projects: [other_project]) }
|
||||
let!(:shared_runner) { create(:ci_runner, :shared) }
|
||||
|
||||
it 'sets assignable project runners only' do
|
||||
group.add_master(user)
|
||||
|
||||
get :show, namespace_id: project.namespace, project_id: project
|
||||
|
||||
expect(assigns(:assignable_runners)).to eq [project_runner]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_cache' do
|
||||
|
|
|
|||
|
|
@ -15,14 +15,18 @@ FactoryBot.define do
|
|||
namespace
|
||||
creator { group ? create(:user) : namespace&.owner }
|
||||
|
||||
# Nest Project Feature attributes
|
||||
transient do
|
||||
# Nest Project Feature attributes
|
||||
wiki_access_level ProjectFeature::ENABLED
|
||||
builds_access_level ProjectFeature::ENABLED
|
||||
snippets_access_level ProjectFeature::ENABLED
|
||||
issues_access_level ProjectFeature::ENABLED
|
||||
merge_requests_access_level ProjectFeature::ENABLED
|
||||
repository_access_level ProjectFeature::ENABLED
|
||||
|
||||
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
|
||||
# `#ci_cd_settings` relation needs to be created first
|
||||
group_runners_enabled nil
|
||||
end
|
||||
|
||||
after(:create) do |project, evaluator|
|
||||
|
|
@ -47,6 +51,9 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
project.group&.refresh_members_authorized_projects
|
||||
|
||||
# assign the delegated `#ci_cd_settings` attributes after create
|
||||
project.reload.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
|
||||
end
|
||||
|
||||
trait :public do
|
||||
|
|
|
|||
|
|
@ -59,6 +59,47 @@ describe "Admin Runners" do
|
|||
expect(page).to have_text 'No runners found'
|
||||
end
|
||||
end
|
||||
|
||||
context 'group runner' do
|
||||
let(:group) { create(:group) }
|
||||
let!(:runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
|
||||
|
||||
it 'shows the label and does not show the project count' do
|
||||
visit admin_runners_path
|
||||
|
||||
within "#runner_#{runner.id}" do
|
||||
expect(page).to have_selector '.label', text: 'group'
|
||||
expect(page).to have_text 'n/a'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'shared runner' do
|
||||
it 'shows the label and does not show the project count' do
|
||||
runner = create :ci_runner, :shared
|
||||
|
||||
visit admin_runners_path
|
||||
|
||||
within "#runner_#{runner.id}" do
|
||||
expect(page).to have_selector '.label', text: 'shared'
|
||||
expect(page).to have_text 'n/a'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'specific runner' do
|
||||
it 'shows the label and the project count' do
|
||||
project = create :project
|
||||
runner = create :ci_runner, projects: [project]
|
||||
|
||||
visit admin_runners_path
|
||||
|
||||
within "#runner_#{runner.id}" do
|
||||
expect(page).to have_selector '.label', text: 'specific'
|
||||
expect(page).to have_text '1'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Runner show page" do
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ describe "Admin::Users" do
|
|||
it "lists group projects" do
|
||||
within(:css, '.append-bottom-default + .panel') do
|
||||
expect(page).to have_content 'Group projects'
|
||||
expect(page).to have_link group.name, admin_group_path(group)
|
||||
expect(page).to have_link group.name, href: admin_group_path(group)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ feature 'Issues > User uses quick actions', :js do
|
|||
|
||||
before do
|
||||
target_project.add_master(user)
|
||||
gitlab_sign_out
|
||||
sign_in(user)
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
|
@ -220,6 +221,7 @@ feature 'Issues > User uses quick actions', :js do
|
|||
|
||||
before do
|
||||
target_project.add_master(user)
|
||||
gitlab_sign_out
|
||||
sign_in(user)
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -181,4 +181,84 @@ feature 'Runners' do
|
|||
expect(page.find('.shared-runners-description')).to have_content('Disable shared Runners')
|
||||
end
|
||||
end
|
||||
|
||||
context 'group runners' do
|
||||
background do
|
||||
project.add_master(user)
|
||||
end
|
||||
|
||||
given(:group) { create :group }
|
||||
|
||||
context 'as project and group master' do
|
||||
background do
|
||||
group.add_master(user)
|
||||
end
|
||||
|
||||
context 'project with a group but no group runner' do
|
||||
given(:project) { create :project, group: group }
|
||||
|
||||
scenario 'group runners are not available' do
|
||||
visit runners_path(project)
|
||||
|
||||
expect(page).to have_content 'This group does not provide any group Runners yet.'
|
||||
|
||||
expect(page).to have_content 'Setup a group Runner manually'
|
||||
expect(page).not_to have_content 'Ask your group master to setup a group Runner.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as project master' do
|
||||
context 'project without a group' do
|
||||
given(:project) { create :project }
|
||||
|
||||
scenario 'group runners are not available' do
|
||||
visit runners_path(project)
|
||||
|
||||
expect(page).to have_content 'This project does not belong to a group and can therefore not make use of group Runners.'
|
||||
end
|
||||
end
|
||||
|
||||
context 'project with a group but no group runner' do
|
||||
given(:group) { create :group }
|
||||
given(:project) { create :project, group: group }
|
||||
|
||||
scenario 'group runners are not available' do
|
||||
visit runners_path(project)
|
||||
|
||||
expect(page).to have_content 'This group does not provide any group Runners yet.'
|
||||
|
||||
expect(page).not_to have_content 'Setup a group Runner manually'
|
||||
expect(page).to have_content 'Ask your group master to setup a group Runner.'
|
||||
end
|
||||
end
|
||||
|
||||
context 'project with a group and a group runner' do
|
||||
given(:group) { create :group }
|
||||
given(:project) { create :project, group: group }
|
||||
given!(:ci_runner) { create :ci_runner, groups: [group], description: 'group-runner' }
|
||||
|
||||
scenario 'group runners are available' do
|
||||
visit runners_path(project)
|
||||
|
||||
expect(page).to have_content 'Available group Runners : 1'
|
||||
expect(page).to have_content 'group-runner'
|
||||
end
|
||||
|
||||
scenario 'group runners may be disabled for a project' do
|
||||
visit runners_path(project)
|
||||
|
||||
click_on 'Disable group Runners'
|
||||
|
||||
expect(page).to have_content 'Enable group Runners'
|
||||
expect(project.reload.group_runners_enabled).to be false
|
||||
|
||||
click_on 'Enable group Runners'
|
||||
|
||||
expect(page).to have_content 'Disable group Runners'
|
||||
expect(project.reload.group_runners_enabled).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,6 +48,33 @@ describe('RepoFile', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('folder', () => {
|
||||
it('renders changes count inside folder', () => {
|
||||
const f = {
|
||||
...file('folder'),
|
||||
path: 'testing',
|
||||
type: 'tree',
|
||||
branchId: 'master',
|
||||
projectId: 'project',
|
||||
};
|
||||
|
||||
store.state.changedFiles.push({
|
||||
...file('fileName'),
|
||||
path: 'testing/fileName',
|
||||
});
|
||||
|
||||
createComponent({
|
||||
file: f,
|
||||
level: 0,
|
||||
});
|
||||
|
||||
const treeChangesEl = vm.$el.querySelector('.ide-tree-changes');
|
||||
|
||||
expect(treeChangesEl).not.toBeNull();
|
||||
expect(treeChangesEl.textContent).toContain('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('locked file', () => {
|
||||
let f;
|
||||
|
||||
|
|
|
|||
|
|
@ -78,4 +78,67 @@ describe('IDE store getters', () => {
|
|||
expect(getters.allBlobs(localState)[0].name).toBe('blob');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChangesInFolder', () => {
|
||||
it('returns length of changed files for a path', () => {
|
||||
localState.changedFiles.push(
|
||||
{
|
||||
path: 'test/index',
|
||||
name: 'index',
|
||||
},
|
||||
{
|
||||
path: 'app/123',
|
||||
name: '123',
|
||||
},
|
||||
);
|
||||
|
||||
expect(getters.getChangesInFolder(localState)('test')).toBe(1);
|
||||
});
|
||||
|
||||
it('returns length of changed & staged files for a path', () => {
|
||||
localState.changedFiles.push(
|
||||
{
|
||||
path: 'test/index',
|
||||
name: 'index',
|
||||
},
|
||||
{
|
||||
path: 'testing/123',
|
||||
name: '123',
|
||||
},
|
||||
);
|
||||
|
||||
localState.stagedFiles.push(
|
||||
{
|
||||
path: 'test/123',
|
||||
name: '123',
|
||||
},
|
||||
{
|
||||
path: 'test/index',
|
||||
name: 'index',
|
||||
},
|
||||
{
|
||||
path: 'testing/12345',
|
||||
name: '12345',
|
||||
},
|
||||
);
|
||||
|
||||
expect(getters.getChangesInFolder(localState)('test')).toBe(2);
|
||||
});
|
||||
|
||||
it('returns length of changed & tempFiles files for a path', () => {
|
||||
localState.changedFiles.push(
|
||||
{
|
||||
path: 'test/index',
|
||||
name: 'index',
|
||||
},
|
||||
{
|
||||
path: 'test/newfile',
|
||||
name: 'newfile',
|
||||
tempFile: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(getters.getChangesInFolder(localState)('test')).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -170,5 +170,19 @@ describe('Participants', function () {
|
|||
|
||||
expect(vm.isShowingMoreParticipants).toBe(true);
|
||||
});
|
||||
|
||||
it('clicking on participants icon emits `toggleSidebar` event', () => {
|
||||
vm = mountComponent(Participants, {
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
const participantsIconEl = vm.$el.querySelector('.sidebar-collapsed-icon');
|
||||
|
||||
participantsIconEl.click();
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_sub
|
|||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import SidebarService from '~/sidebar/services/sidebar_service';
|
||||
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import Mock from './mock_data';
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ describe('Sidebar Subscriptions', function () {
|
|||
mediator,
|
||||
});
|
||||
|
||||
eventHub.$emit('toggleSubscription');
|
||||
vm.onToggleSubscription();
|
||||
|
||||
expect(mediator.toggleSubscription).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
|
||||
describe('Subscriptions', function () {
|
||||
|
|
@ -39,4 +40,22 @@ describe('Subscriptions', function () {
|
|||
|
||||
expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked');
|
||||
});
|
||||
|
||||
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
|
||||
vm = mountComponent(Subscriptions, { subscribed: true });
|
||||
spyOn(eventHub, '$emit');
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
vm.toggleSubscription();
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
|
||||
});
|
||||
|
||||
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
|
||||
vm = mountComponent(Subscriptions, { subscribed: true });
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
vm.onClickCollapsedIcon();
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -258,7 +258,6 @@ project:
|
|||
- builds
|
||||
- runner_projects
|
||||
- runners
|
||||
- active_runners
|
||||
- variables
|
||||
- triggers
|
||||
- pipeline_schedules
|
||||
|
|
@ -286,6 +285,7 @@ project:
|
|||
- internal_ids
|
||||
- project_deploy_tokens
|
||||
- deploy_tokens
|
||||
- settings
|
||||
- ci_cd_settings
|
||||
award_emoji:
|
||||
- awardable
|
||||
|
|
|
|||
|
|
@ -19,6 +19,63 @@ describe Ci::Runner do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'either_projects_or_group' do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
it 'disallows assigning to a group if already assigned to a group' do
|
||||
runner = create(:ci_runner, groups: [group])
|
||||
|
||||
runner.groups << build(:group)
|
||||
|
||||
expect(runner).not_to be_valid
|
||||
expect(runner.errors.full_messages).to eq ['Runner can only be assigned to one group']
|
||||
end
|
||||
|
||||
it 'disallows assigning to a group if already assigned to a project' do
|
||||
project = create(:project)
|
||||
runner = create(:ci_runner, projects: [project])
|
||||
|
||||
runner.groups << build(:group)
|
||||
|
||||
expect(runner).not_to be_valid
|
||||
expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
|
||||
end
|
||||
|
||||
it 'disallows assigning to a project if already assigned to a group' do
|
||||
runner = create(:ci_runner, groups: [group])
|
||||
|
||||
runner.projects << build(:project)
|
||||
|
||||
expect(runner).not_to be_valid
|
||||
expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
|
||||
end
|
||||
|
||||
it 'allows assigning to a group if not assigned to a group nor a project' do
|
||||
runner = create(:ci_runner)
|
||||
|
||||
runner.groups << build(:group)
|
||||
|
||||
expect(runner).to be_valid
|
||||
end
|
||||
|
||||
it 'allows assigning to a project if not assigned to a group nor a project' do
|
||||
runner = create(:ci_runner)
|
||||
|
||||
runner.projects << build(:project)
|
||||
|
||||
expect(runner).to be_valid
|
||||
end
|
||||
|
||||
it 'allows assigning to a project if already assigned to a project' do
|
||||
project = create(:project)
|
||||
runner = create(:ci_runner, projects: [project])
|
||||
|
||||
runner.projects << build(:project)
|
||||
|
||||
expect(runner).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#access_level' do
|
||||
|
|
@ -49,6 +106,80 @@ describe Ci::Runner do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.shared' do
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'returns the shared group runner' do
|
||||
runner = create(:ci_runner, :shared, groups: [group])
|
||||
|
||||
expect(described_class.shared).to eq [runner]
|
||||
end
|
||||
|
||||
it 'returns the shared project runner' do
|
||||
runner = create(:ci_runner, :shared, projects: [project])
|
||||
|
||||
expect(described_class.shared).to eq [runner]
|
||||
end
|
||||
end
|
||||
|
||||
describe '.belonging_to_project' do
|
||||
it 'returns the specific project runner' do
|
||||
# own
|
||||
specific_project = create(:project)
|
||||
specific_runner = create(:ci_runner, :specific, projects: [specific_project])
|
||||
|
||||
# other
|
||||
other_project = create(:project)
|
||||
create(:ci_runner, :specific, projects: [other_project])
|
||||
|
||||
expect(described_class.belonging_to_project(specific_project.id)).to eq [specific_runner]
|
||||
end
|
||||
end
|
||||
|
||||
describe '.belonging_to_parent_group_of_project' do
|
||||
let(:project) { create(:project, group: group) }
|
||||
let(:group) { create(:group) }
|
||||
let(:runner) { create(:ci_runner, :specific, groups: [group]) }
|
||||
let!(:unrelated_group) { create(:group) }
|
||||
let!(:unrelated_project) { create(:project, group: unrelated_group) }
|
||||
let!(:unrelated_runner) { create(:ci_runner, :specific, groups: [unrelated_group]) }
|
||||
|
||||
it 'returns the specific group runner' do
|
||||
expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
|
||||
end
|
||||
|
||||
context 'with a parent group with a runner', :nested_groups do
|
||||
let(:runner) { create(:ci_runner, :specific, groups: [parent_group]) }
|
||||
let(:project) { create(:project, group: group) }
|
||||
let(:group) { create(:group, parent: parent_group) }
|
||||
let(:parent_group) { create(:group) }
|
||||
|
||||
it 'returns the group runner from the parent group' do
|
||||
expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.owned_or_shared' do
|
||||
it 'returns a globally shared, a project specific and a group specific runner' do
|
||||
# group specific
|
||||
group = create(:group)
|
||||
project = create(:project, group: group)
|
||||
group_runner = create(:ci_runner, :specific, groups: [group])
|
||||
|
||||
# project specific
|
||||
project_runner = create(:ci_runner, :specific, projects: [project])
|
||||
|
||||
# globally shared
|
||||
shared_runner = create(:ci_runner, :shared)
|
||||
|
||||
expect(described_class.owned_or_shared(project.id)).to contain_exactly(
|
||||
group_runner, project_runner, shared_runner
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#display_name' do
|
||||
it 'returns the description if it has a value' do
|
||||
runner = FactoryBot.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
|
||||
|
|
@ -163,7 +294,9 @@ describe Ci::Runner do
|
|||
describe '#can_pick?' do
|
||||
let(:pipeline) { create(:ci_pipeline) }
|
||||
let(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
let(:runner) { create(:ci_runner) }
|
||||
let(:runner) { create(:ci_runner, tag_list: tag_list, run_untagged: run_untagged) }
|
||||
let(:tag_list) { [] }
|
||||
let(:run_untagged) { true }
|
||||
|
||||
subject { runner.can_pick?(build) }
|
||||
|
||||
|
|
@ -171,6 +304,13 @@ describe Ci::Runner do
|
|||
build.project.runners << runner
|
||||
end
|
||||
|
||||
context 'a different runner' do
|
||||
it 'cannot handle builds' do
|
||||
other_runner = create(:ci_runner)
|
||||
expect(other_runner.can_pick?(build)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner does not have tags' do
|
||||
it 'can handle builds without tags' do
|
||||
expect(runner.can_pick?(build)).to be_truthy
|
||||
|
|
@ -184,9 +324,7 @@ describe Ci::Runner do
|
|||
end
|
||||
|
||||
context 'when runner has tags' do
|
||||
before do
|
||||
runner.tag_list = %w(bb cc)
|
||||
end
|
||||
let(:tag_list) { %w(bb cc) }
|
||||
|
||||
shared_examples 'tagged build picker' do
|
||||
it 'can handle build with matching tags' do
|
||||
|
|
@ -211,9 +349,7 @@ describe Ci::Runner do
|
|||
end
|
||||
|
||||
context 'when runner cannot pick untagged jobs' do
|
||||
before do
|
||||
runner.run_untagged = false
|
||||
end
|
||||
let(:run_untagged) { false }
|
||||
|
||||
it 'cannot handle builds without tags' do
|
||||
expect(runner.can_pick?(build)).to be_falsey
|
||||
|
|
@ -224,8 +360,9 @@ describe Ci::Runner do
|
|||
end
|
||||
|
||||
context 'when runner is shared' do
|
||||
let(:runner) { create(:ci_runner, :shared) }
|
||||
|
||||
before do
|
||||
runner.is_shared = true
|
||||
build.project.runners = []
|
||||
end
|
||||
|
||||
|
|
@ -234,9 +371,7 @@ describe Ci::Runner do
|
|||
end
|
||||
|
||||
context 'when runner is locked' do
|
||||
before do
|
||||
runner.locked = true
|
||||
end
|
||||
let(:runner) { create(:ci_runner, :shared, locked: true) }
|
||||
|
||||
it 'can handle builds' do
|
||||
expect(runner.can_pick?(build)).to be_truthy
|
||||
|
|
@ -260,6 +395,17 @@ describe Ci::Runner do
|
|||
expect(runner.can_pick?(build)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when runner is assigned to a group' do
|
||||
before do
|
||||
build.project.runners = []
|
||||
runner.groups << create(:group, projects: [build.project])
|
||||
end
|
||||
|
||||
it 'can handle builds' do
|
||||
expect(runner.can_pick?(build)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when access_level of runner is not_protected' do
|
||||
|
|
@ -583,4 +729,76 @@ describe Ci::Runner do
|
|||
expect(described_class.search(runner.description.upcase)).to eq([runner])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#assigned_to_group?' do
|
||||
subject { runner.assigned_to_group? }
|
||||
|
||||
context 'when project runner' do
|
||||
let(:runner) { create(:ci_runner, description: 'Project runner', projects: [project]) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when shared runner' do
|
||||
let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when group runner' do
|
||||
let(:group) { create(:group) }
|
||||
let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#assigned_to_project?' do
|
||||
subject { runner.assigned_to_project? }
|
||||
|
||||
context 'when group runner' do
|
||||
let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
|
||||
let(:group) { create(:group) }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when shared runner' do
|
||||
let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when project runner' do
|
||||
let(:runner) { create(:ci_runner, description: 'Group runner', projects: [project]) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pick_build!' do
|
||||
context 'runner can pick the build' do
|
||||
it 'calls #tick_runner_queue' do
|
||||
ci_build = build(:ci_build)
|
||||
runner = build(:ci_runner)
|
||||
allow(runner).to receive(:can_pick?).with(ci_build).and_return(true)
|
||||
|
||||
expect(runner).to receive(:tick_runner_queue)
|
||||
|
||||
runner.pick_build!(ci_build)
|
||||
end
|
||||
end
|
||||
|
||||
context 'runner cannot pick the build' do
|
||||
it 'does not call #tick_runner_queue' do
|
||||
ci_build = build(:ci_build)
|
||||
runner = build(:ci_runner)
|
||||
allow(runner).to receive(:can_pick?).with(ci_build).and_return(false)
|
||||
|
||||
expect(runner).not_to receive(:tick_runner_queue)
|
||||
|
||||
runner.pick_build!(ci_build)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ describe Project do
|
|||
it { is_expected.to have_many(:build_trace_section_names)}
|
||||
it { is_expected.to have_many(:runner_projects) }
|
||||
it { is_expected.to have_many(:runners) }
|
||||
it { is_expected.to have_many(:active_runners) }
|
||||
it { is_expected.to have_many(:variables) }
|
||||
it { is_expected.to have_many(:triggers) }
|
||||
it { is_expected.to have_many(:pages_domains) }
|
||||
|
|
@ -1139,45 +1138,106 @@ describe Project do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#any_runners' do
|
||||
let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
|
||||
let(:specific_runner) { create(:ci_runner) }
|
||||
let(:shared_runner) { create(:ci_runner, :shared) }
|
||||
describe '#any_runners?' do
|
||||
context 'shared runners' do
|
||||
let(:project) { create :project, shared_runners_enabled: shared_runners_enabled }
|
||||
let(:specific_runner) { create :ci_runner }
|
||||
let(:shared_runner) { create :ci_runner, :shared }
|
||||
|
||||
context 'for shared runners disabled' do
|
||||
let(:shared_runners_enabled) { false }
|
||||
context 'for shared runners disabled' do
|
||||
let(:shared_runners_enabled) { false }
|
||||
|
||||
it 'has no runners available' do
|
||||
expect(project.any_runners?).to be_falsey
|
||||
it 'has no runners available' do
|
||||
expect(project.any_runners?).to be_falsey
|
||||
end
|
||||
|
||||
it 'has a specific runner' do
|
||||
project.runners << specific_runner
|
||||
|
||||
expect(project.any_runners?).to be_truthy
|
||||
end
|
||||
|
||||
it 'has a shared runner, but they are prohibited to use' do
|
||||
shared_runner
|
||||
|
||||
expect(project.any_runners?).to be_falsey
|
||||
end
|
||||
|
||||
it 'checks the presence of specific runner' do
|
||||
project.runners << specific_runner
|
||||
|
||||
expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if match cannot be found' do
|
||||
project.runners << specific_runner
|
||||
|
||||
expect(project.any_runners? { false }).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
it 'has a specific runner' do
|
||||
project.runners << specific_runner
|
||||
expect(project.any_runners?).to be_truthy
|
||||
end
|
||||
context 'for shared runners enabled' do
|
||||
let(:shared_runners_enabled) { true }
|
||||
|
||||
it 'has a shared runner, but they are prohibited to use' do
|
||||
shared_runner
|
||||
expect(project.any_runners?).to be_falsey
|
||||
end
|
||||
it 'has a shared runner' do
|
||||
shared_runner
|
||||
|
||||
it 'checks the presence of specific runner' do
|
||||
project.runners << specific_runner
|
||||
expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
|
||||
expect(project.any_runners?).to be_truthy
|
||||
end
|
||||
|
||||
it 'checks the presence of shared runner' do
|
||||
shared_runner
|
||||
|
||||
expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if match cannot be found' do
|
||||
shared_runner
|
||||
|
||||
expect(project.any_runners? { false }).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for shared runners enabled' do
|
||||
let(:shared_runners_enabled) { true }
|
||||
context 'group runners' do
|
||||
let(:project) { create :project, group_runners_enabled: group_runners_enabled }
|
||||
let(:group) { create :group, projects: [project] }
|
||||
let(:group_runner) { create :ci_runner, groups: [group] }
|
||||
|
||||
it 'has a shared runner' do
|
||||
shared_runner
|
||||
expect(project.any_runners?).to be_truthy
|
||||
context 'for group runners disabled' do
|
||||
let(:group_runners_enabled) { false }
|
||||
|
||||
it 'has no runners available' do
|
||||
expect(project.any_runners?).to be_falsey
|
||||
end
|
||||
|
||||
it 'has a group runner, but they are prohibited to use' do
|
||||
group_runner
|
||||
|
||||
expect(project.any_runners?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
it 'checks the presence of shared runner' do
|
||||
shared_runner
|
||||
expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
|
||||
context 'for group runners enabled' do
|
||||
let(:group_runners_enabled) { true }
|
||||
|
||||
it 'has a group runner' do
|
||||
group_runner
|
||||
|
||||
expect(project.any_runners?).to be_truthy
|
||||
end
|
||||
|
||||
it 'checks the presence of group runner' do
|
||||
group_runner
|
||||
|
||||
expect(project.any_runners? { |runner| runner == group_runner }).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false if match cannot be found' do
|
||||
group_runner
|
||||
|
||||
expect(project.any_runners? { false }).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3541,6 +3601,18 @@ describe Project do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#toggle_ci_cd_settings!' do
|
||||
it 'toggles the value on #settings' do
|
||||
project = create(:project, group_runners_enabled: false)
|
||||
|
||||
expect(project.group_runners_enabled).to be false
|
||||
|
||||
project.toggle_ci_cd_settings!(:group_runners_enabled)
|
||||
|
||||
expect(project.group_runners_enabled).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#gitlab_deploy_token' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
|
|
|
|||
|
|
@ -40,18 +40,36 @@ describe API::Runner do
|
|||
expect(json_response['token']).to eq(runner.token)
|
||||
expect(runner.run_untagged).to be true
|
||||
expect(runner.token).not_to eq(registration_token)
|
||||
expect(runner).to be_instance_type
|
||||
end
|
||||
|
||||
context 'when project token is used' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'creates runner' do
|
||||
it 'creates project runner' do
|
||||
post api('/runners'), token: project.runners_token
|
||||
|
||||
expect(response).to have_gitlab_http_status 201
|
||||
expect(project.runners.size).to eq(1)
|
||||
expect(Ci::Runner.first.token).not_to eq(registration_token)
|
||||
expect(Ci::Runner.first.token).not_to eq(project.runners_token)
|
||||
runner = Ci::Runner.first
|
||||
expect(runner.token).not_to eq(registration_token)
|
||||
expect(runner.token).not_to eq(project.runners_token)
|
||||
expect(runner).to be_project_type
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group token is used' do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
it 'creates a group runner' do
|
||||
post api('/runners'), token: group.runners_token
|
||||
|
||||
expect(response).to have_http_status 201
|
||||
expect(group.runners.size).to eq(1)
|
||||
runner = Ci::Runner.first
|
||||
expect(runner.token).not_to eq(registration_token)
|
||||
expect(runner.token).not_to eq(group.runners_token)
|
||||
expect(runner).to be_group_type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,22 +8,27 @@ describe API::Runners do
|
|||
let(:project) { create(:project, creator_id: user.id) }
|
||||
let(:project2) { create(:project, creator_id: user.id) }
|
||||
|
||||
let!(:shared_runner) { create(:ci_runner, :shared) }
|
||||
let!(:unused_specific_runner) { create(:ci_runner) }
|
||||
let(:group) { create(:group).tap { |group| group.add_owner(user) } }
|
||||
let(:group2) { create(:group).tap { |group| group.add_owner(user) } }
|
||||
|
||||
let!(:specific_runner) do
|
||||
create(:ci_runner).tap do |runner|
|
||||
let!(:shared_runner) { create(:ci_runner, :shared, description: 'Shared runner') }
|
||||
let!(:unused_project_runner) { create(:ci_runner) }
|
||||
|
||||
let!(:project_runner) do
|
||||
create(:ci_runner, description: 'Project runner').tap do |runner|
|
||||
create(:ci_runner_project, runner: runner, project: project)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:two_projects_runner) do
|
||||
create(:ci_runner).tap do |runner|
|
||||
create(:ci_runner, description: 'Two projects runner').tap do |runner|
|
||||
create(:ci_runner_project, runner: runner, project: project)
|
||||
create(:ci_runner_project, runner: runner, project: project2)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
|
||||
|
||||
before do
|
||||
# Set project access for users
|
||||
create(:project_member, :master, user: user, project: project)
|
||||
|
|
@ -37,9 +42,13 @@ describe API::Runners do
|
|||
get api('/runners', user)
|
||||
|
||||
shared = json_response.any? { |r| r['is_shared'] }
|
||||
descriptions = json_response.map { |runner| runner['description'] }
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(descriptions).to contain_exactly(
|
||||
'Project runner', 'Two projects runner'
|
||||
)
|
||||
expect(shared).to be_falsey
|
||||
end
|
||||
|
||||
|
|
@ -129,10 +138,16 @@ describe API::Runners do
|
|||
|
||||
context 'when runner is not shared' do
|
||||
it "returns runner's details" do
|
||||
get api("/runners/#{specific_runner.id}", admin)
|
||||
get api("/runners/#{project_runner.id}", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(json_response['description']).to eq(specific_runner.description)
|
||||
expect(json_response['description']).to eq(project_runner.description)
|
||||
end
|
||||
|
||||
it "returns the project's details for a project runner" do
|
||||
get api("/runners/#{project_runner.id}", admin)
|
||||
|
||||
expect(json_response['projects'].first['id']).to eq(project.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -146,10 +161,10 @@ describe API::Runners do
|
|||
context "runner project's administrative user" do
|
||||
context 'when runner is not shared' do
|
||||
it "returns runner's details" do
|
||||
get api("/runners/#{specific_runner.id}", user)
|
||||
get api("/runners/#{project_runner.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(json_response['description']).to eq(specific_runner.description)
|
||||
expect(json_response['description']).to eq(project_runner.description)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -164,18 +179,18 @@ describe API::Runners do
|
|||
end
|
||||
|
||||
context 'other authorized user' do
|
||||
it "does not return runner's details" do
|
||||
get api("/runners/#{specific_runner.id}", user2)
|
||||
it "does not return project runner's details" do
|
||||
get api("/runners/#{project_runner.id}", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it "does not return runner's details" do
|
||||
get api("/runners/#{specific_runner.id}")
|
||||
it "does not return project runner's details" do
|
||||
get api("/runners/#{project_runner.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(401)
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -212,16 +227,16 @@ describe API::Runners do
|
|||
|
||||
context 'when runner is not shared' do
|
||||
it 'updates runner' do
|
||||
description = specific_runner.description
|
||||
runner_queue_value = specific_runner.ensure_runner_queue_value
|
||||
description = project_runner.description
|
||||
runner_queue_value = project_runner.ensure_runner_queue_value
|
||||
|
||||
update_runner(specific_runner.id, admin, description: 'test')
|
||||
specific_runner.reload
|
||||
update_runner(project_runner.id, admin, description: 'test')
|
||||
project_runner.reload
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(specific_runner.description).to eq('test')
|
||||
expect(specific_runner.description).not_to eq(description)
|
||||
expect(specific_runner.ensure_runner_queue_value)
|
||||
expect(project_runner.description).to eq('test')
|
||||
expect(project_runner.description).not_to eq(description)
|
||||
expect(project_runner.ensure_runner_queue_value)
|
||||
.not_to eq(runner_queue_value)
|
||||
end
|
||||
end
|
||||
|
|
@ -247,29 +262,29 @@ describe API::Runners do
|
|||
end
|
||||
|
||||
context 'when runner is not shared' do
|
||||
it 'does not update runner without access to it' do
|
||||
put api("/runners/#{specific_runner.id}", user2), description: 'test'
|
||||
it 'does not update project runner without access to it' do
|
||||
put api("/runners/#{project_runner.id}", user2), description: 'test'
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
it 'updates runner with access to it' do
|
||||
description = specific_runner.description
|
||||
put api("/runners/#{specific_runner.id}", admin), description: 'test'
|
||||
specific_runner.reload
|
||||
it 'updates project runner with access to it' do
|
||||
description = project_runner.description
|
||||
put api("/runners/#{project_runner.id}", admin), description: 'test'
|
||||
project_runner.reload
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(specific_runner.description).to eq('test')
|
||||
expect(specific_runner.description).not_to eq(description)
|
||||
expect(project_runner.description).to eq('test')
|
||||
expect(project_runner.description).not_to eq(description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'does not delete runner' do
|
||||
put api("/runners/#{specific_runner.id}")
|
||||
it 'does not delete project runner' do
|
||||
put api("/runners/#{project_runner.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(401)
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -293,17 +308,17 @@ describe API::Runners do
|
|||
context 'when runner is not shared' do
|
||||
it 'deletes unused runner' do
|
||||
expect do
|
||||
delete api("/runners/#{unused_specific_runner.id}", admin)
|
||||
delete api("/runners/#{unused_project_runner.id}", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(204)
|
||||
end.to change { Ci::Runner.specific.count }.by(-1)
|
||||
end
|
||||
|
||||
it 'deletes used runner' do
|
||||
it 'deletes used project runner' do
|
||||
expect do
|
||||
delete api("/runners/#{specific_runner.id}", admin)
|
||||
delete api("/runners/#{project_runner.id}", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(204)
|
||||
expect(response).to have_http_status(204)
|
||||
end.to change { Ci::Runner.specific.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
|
@ -325,34 +340,34 @@ describe API::Runners do
|
|||
|
||||
context 'when runner is not shared' do
|
||||
it 'does not delete runner without access to it' do
|
||||
delete api("/runners/#{specific_runner.id}", user2)
|
||||
delete api("/runners/#{project_runner.id}", user2)
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
||||
it 'does not delete runner with more than one associated project' do
|
||||
it 'does not delete project runner with more than one associated project' do
|
||||
delete api("/runners/#{two_projects_runner.id}", user)
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
||||
it 'deletes runner for one owned project' do
|
||||
it 'deletes project runner for one owned project' do
|
||||
expect do
|
||||
delete api("/runners/#{specific_runner.id}", user)
|
||||
delete api("/runners/#{project_runner.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(204)
|
||||
expect(response).to have_http_status(204)
|
||||
end.to change { Ci::Runner.specific.count }.by(-1)
|
||||
end
|
||||
|
||||
it_behaves_like '412 response' do
|
||||
let(:request) { api("/runners/#{specific_runner.id}", user) }
|
||||
let(:request) { api("/runners/#{project_runner.id}", user) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'does not delete runner' do
|
||||
delete api("/runners/#{specific_runner.id}")
|
||||
it 'does not delete project runner' do
|
||||
delete api("/runners/#{project_runner.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(401)
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -361,8 +376,8 @@ describe API::Runners do
|
|||
set(:job_1) { create(:ci_build) }
|
||||
let!(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) }
|
||||
let!(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
|
||||
let!(:job_4) { create(:ci_build, :running, runner: specific_runner, project: project) }
|
||||
let!(:job_5) { create(:ci_build, :failed, runner: specific_runner, project: project) }
|
||||
let!(:job_4) { create(:ci_build, :running, runner: project_runner, project: project) }
|
||||
let!(:job_5) { create(:ci_build, :failed, runner: project_runner, project: project) }
|
||||
|
||||
context 'admin user' do
|
||||
context 'when runner exists' do
|
||||
|
|
@ -380,7 +395,7 @@ describe API::Runners do
|
|||
|
||||
context 'when runner is specific' do
|
||||
it 'return jobs' do
|
||||
get api("/runners/#{specific_runner.id}/jobs", admin)
|
||||
get api("/runners/#{project_runner.id}/jobs", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
|
|
@ -392,7 +407,7 @@ describe API::Runners do
|
|||
|
||||
context 'when valid status is provided' do
|
||||
it 'return filtered jobs' do
|
||||
get api("/runners/#{specific_runner.id}/jobs?status=failed", admin)
|
||||
get api("/runners/#{project_runner.id}/jobs?status=failed", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
|
|
@ -405,7 +420,7 @@ describe API::Runners do
|
|||
|
||||
context 'when invalid status is provided' do
|
||||
it 'return 400' do
|
||||
get api("/runners/#{specific_runner.id}/jobs?status=non-existing", admin)
|
||||
get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(400)
|
||||
end
|
||||
|
|
@ -433,7 +448,7 @@ describe API::Runners do
|
|||
|
||||
context 'when runner is specific' do
|
||||
it 'return jobs' do
|
||||
get api("/runners/#{specific_runner.id}/jobs", user)
|
||||
get api("/runners/#{project_runner.id}/jobs", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
|
|
@ -445,7 +460,7 @@ describe API::Runners do
|
|||
|
||||
context 'when valid status is provided' do
|
||||
it 'return filtered jobs' do
|
||||
get api("/runners/#{specific_runner.id}/jobs?status=failed", user)
|
||||
get api("/runners/#{project_runner.id}/jobs?status=failed", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
|
|
@ -458,7 +473,7 @@ describe API::Runners do
|
|||
|
||||
context 'when invalid status is provided' do
|
||||
it 'return 400' do
|
||||
get api("/runners/#{specific_runner.id}/jobs?status=non-existing", user)
|
||||
get api("/runners/#{project_runner.id}/jobs?status=non-existing", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(400)
|
||||
end
|
||||
|
|
@ -476,7 +491,7 @@ describe API::Runners do
|
|||
|
||||
context 'other authorized user' do
|
||||
it 'does not return jobs' do
|
||||
get api("/runners/#{specific_runner.id}/jobs", user2)
|
||||
get api("/runners/#{project_runner.id}/jobs", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
|
@ -484,7 +499,7 @@ describe API::Runners do
|
|||
|
||||
context 'unauthorized user' do
|
||||
it 'does not return jobs' do
|
||||
get api("/runners/#{specific_runner.id}/jobs")
|
||||
get api("/runners/#{project_runner.id}/jobs")
|
||||
|
||||
expect(response).to have_gitlab_http_status(401)
|
||||
end
|
||||
|
|
@ -523,7 +538,7 @@ describe API::Runners do
|
|||
|
||||
describe 'POST /projects/:id/runners' do
|
||||
context 'authorized user' do
|
||||
let(:specific_runner2) do
|
||||
let(:project_runner2) do
|
||||
create(:ci_runner).tap do |runner|
|
||||
create(:ci_runner_project, runner: runner, project: project2)
|
||||
end
|
||||
|
|
@ -531,23 +546,23 @@ describe API::Runners do
|
|||
|
||||
it 'enables specific runner' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: project_runner2.id
|
||||
end.to change { project.runners.count }.by(+1)
|
||||
expect(response).to have_gitlab_http_status(201)
|
||||
end
|
||||
|
||||
it 'avoids changes when enabling already enabled runner' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: project_runner.id
|
||||
end.to change { project.runners.count }.by(0)
|
||||
expect(response).to have_gitlab_http_status(409)
|
||||
end
|
||||
|
||||
it 'does not enable locked runner' do
|
||||
specific_runner2.update(locked: true)
|
||||
project_runner2.update(locked: true)
|
||||
|
||||
expect do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: project_runner2.id
|
||||
end.to change { project.runners.count }.by(0)
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
|
|
@ -559,10 +574,16 @@ describe API::Runners do
|
|||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
||||
it 'does not enable group runner' do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: group_runner.id
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
context 'user is admin' do
|
||||
it 'enables any specific runner' do
|
||||
expect do
|
||||
post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
|
||||
post api("/projects/#{project.id}/runners", admin), runner_id: unused_project_runner.id
|
||||
end.to change { project.runners.count }.by(+1)
|
||||
expect(response).to have_gitlab_http_status(201)
|
||||
end
|
||||
|
|
@ -570,7 +591,7 @@ describe API::Runners do
|
|||
|
||||
context 'user is not admin' do
|
||||
it 'does not enable runner without access to' do
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
|
||||
post api("/projects/#{project.id}/runners", user), runner_id: unused_project_runner.id
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
|
@ -619,7 +640,7 @@ describe API::Runners do
|
|||
context 'when runner have one associated projects' do
|
||||
it "does not disable project's runner" do
|
||||
expect do
|
||||
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
|
||||
delete api("/projects/#{project.id}/runners/#{project_runner.id}", user)
|
||||
end.to change { project.runners.count }.by(0)
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
|
@ -634,7 +655,7 @@ describe API::Runners do
|
|||
|
||||
context 'authorized user without permissions' do
|
||||
it "does not disable project's runner" do
|
||||
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
|
||||
delete api("/projects/#{project.id}/runners/#{project_runner.id}", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
|
|
@ -642,7 +663,7 @@ describe API::Runners do
|
|||
|
||||
context 'unauthorized user' do
|
||||
it "does not disable project's runner" do
|
||||
delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
|
||||
delete api("/projects/#{project.id}/runners/#{project_runner.id}")
|
||||
|
||||
expect(response).to have_gitlab_http_status(401)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ describe PipelineSerializer do
|
|||
it 'verifies number of queries', :request_store do
|
||||
recorded = ActiveRecord::QueryRecorder.new { subject }
|
||||
|
||||
expect(recorded.count).to be_within(1).of(36)
|
||||
expect(recorded.count).to be_within(1).of(44)
|
||||
expect(recorded.cached_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ require 'spec_helper'
|
|||
|
||||
module Ci
|
||||
describe RegisterJobService do
|
||||
let!(:project) { FactoryBot.create :project, shared_runners_enabled: false }
|
||||
let!(:pipeline) { FactoryBot.create :ci_pipeline, project: project }
|
||||
let!(:pending_job) { FactoryBot.create :ci_build, pipeline: pipeline }
|
||||
let!(:shared_runner) { FactoryBot.create(:ci_runner, is_shared: true) }
|
||||
let!(:specific_runner) { FactoryBot.create(:ci_runner, is_shared: false) }
|
||||
set(:group) { create(:group) }
|
||||
set(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
|
||||
set(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let!(:shared_runner) { create(:ci_runner, is_shared: true) }
|
||||
let!(:specific_runner) { create(:ci_runner, is_shared: false) }
|
||||
let!(:group_runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
|
||||
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
specific_runner.assign_to(project)
|
||||
|
|
@ -150,7 +152,7 @@ module Ci
|
|||
|
||||
context 'disallow when builds are disabled' do
|
||||
before do
|
||||
project.update(shared_runners_enabled: true)
|
||||
project.update(shared_runners_enabled: true, group_runners_enabled: true)
|
||||
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
|
||||
end
|
||||
|
||||
|
|
@ -160,13 +162,90 @@ module Ci
|
|||
it { expect(build).to be_nil }
|
||||
end
|
||||
|
||||
context 'and uses specific runner' do
|
||||
context 'and uses group runner' do
|
||||
let(:build) { execute(group_runner) }
|
||||
|
||||
it { expect(build).to be_nil }
|
||||
end
|
||||
|
||||
context 'and uses project runner' do
|
||||
let(:build) { execute(specific_runner) }
|
||||
|
||||
it { expect(build).to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'allow group runners' do
|
||||
before do
|
||||
project.update!(group_runners_enabled: true)
|
||||
end
|
||||
|
||||
context 'for multiple builds' do
|
||||
let!(:project2) { create :project, group_runners_enabled: true, group: group }
|
||||
let!(:pipeline2) { create :ci_pipeline, project: project2 }
|
||||
let!(:project3) { create :project, group_runners_enabled: true, group: group }
|
||||
let!(:pipeline3) { create :ci_pipeline, project: project3 }
|
||||
|
||||
let!(:build1_project1) { pending_job }
|
||||
let!(:build2_project1) { create :ci_build, pipeline: pipeline }
|
||||
let!(:build3_project1) { create :ci_build, pipeline: pipeline }
|
||||
let!(:build1_project2) { create :ci_build, pipeline: pipeline2 }
|
||||
let!(:build2_project2) { create :ci_build, pipeline: pipeline2 }
|
||||
let!(:build1_project3) { create :ci_build, pipeline: pipeline3 }
|
||||
|
||||
# these shouldn't influence the scheduling
|
||||
let!(:unrelated_group) { create :group }
|
||||
let!(:unrelated_project) { create :project, group_runners_enabled: true, group: unrelated_group }
|
||||
let!(:unrelated_pipeline) { create :ci_pipeline, project: unrelated_project }
|
||||
let!(:build1_unrelated_project) { create :ci_build, pipeline: unrelated_pipeline }
|
||||
let!(:unrelated_group_runner) { create :ci_runner, groups: [unrelated_group] }
|
||||
|
||||
it 'does not consider builds from other group runners' do
|
||||
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 6
|
||||
execute(group_runner)
|
||||
|
||||
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 5
|
||||
execute(group_runner)
|
||||
|
||||
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 4
|
||||
execute(group_runner)
|
||||
|
||||
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 3
|
||||
execute(group_runner)
|
||||
|
||||
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 2
|
||||
execute(group_runner)
|
||||
|
||||
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 1
|
||||
execute(group_runner)
|
||||
|
||||
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 0
|
||||
expect(execute(group_runner)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'group runner' do
|
||||
let(:build) { execute(group_runner) }
|
||||
|
||||
it { expect(build).to be_kind_of(Build) }
|
||||
it { expect(build).to be_valid }
|
||||
it { expect(build).to be_running }
|
||||
it { expect(build.runner).to eq(group_runner) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'disallow group runners' do
|
||||
before do
|
||||
project.update!(group_runners_enabled: false)
|
||||
end
|
||||
|
||||
context 'group runner' do
|
||||
let(:build) { execute(group_runner) }
|
||||
|
||||
it { expect(build).to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first build is stalled' do
|
||||
before do
|
||||
pending_job.update(lock_version: 0)
|
||||
|
|
@ -178,7 +257,7 @@ module Ci
|
|||
let!(:other_build) { create :ci_build, pipeline: pipeline }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
|
||||
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
|
||||
.and_return(Ci::Build.where(id: [pending_job, other_build]))
|
||||
end
|
||||
|
||||
|
|
@ -190,7 +269,7 @@ module Ci
|
|||
|
||||
context 'when single build is in queue' do
|
||||
before do
|
||||
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
|
||||
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
|
||||
.and_return(Ci::Build.where(id: pending_job))
|
||||
end
|
||||
|
||||
|
|
@ -201,7 +280,7 @@ module Ci
|
|||
|
||||
context 'when there is no build in queue' do
|
||||
before do
|
||||
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
|
||||
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
|
||||
.and_return(Ci::Build.none)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -8,21 +8,19 @@ describe Ci::UpdateBuildQueueService do
|
|||
context 'when updating specific runners' do
|
||||
let(:runner) { create(:ci_runner) }
|
||||
|
||||
context 'when there are runner that can pick build' do
|
||||
context 'when there is a runner that can pick build' do
|
||||
before do
|
||||
build.project.runners << runner
|
||||
end
|
||||
|
||||
it 'ticks runner queue value' do
|
||||
expect { subject.execute(build) }
|
||||
.to change { runner.ensure_runner_queue_value }
|
||||
expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no runners that can pick build' do
|
||||
context 'when there is no runner that can pick build' do
|
||||
it 'does not tick runner queue value' do
|
||||
expect { subject.execute(build) }
|
||||
.not_to change { runner.ensure_runner_queue_value }
|
||||
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -30,21 +28,61 @@ describe Ci::UpdateBuildQueueService do
|
|||
context 'when updating shared runners' do
|
||||
let(:runner) { create(:ci_runner, :shared) }
|
||||
|
||||
context 'when there are runner that can pick build' do
|
||||
context 'when there is no runner that can pick build' do
|
||||
it 'ticks runner queue value' do
|
||||
expect { subject.execute(build) }
|
||||
.to change { runner.ensure_runner_queue_value }
|
||||
expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no runners that can pick build' do
|
||||
context 'when there is no runner that can pick build due to tag mismatch' do
|
||||
before do
|
||||
build.tag_list = [:docker]
|
||||
end
|
||||
|
||||
it 'does not tick runner queue value' do
|
||||
expect { subject.execute(build) }
|
||||
.not_to change { runner.ensure_runner_queue_value }
|
||||
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no runner that can pick build due to being disabled on project' do
|
||||
before do
|
||||
build.project.shared_runners_enabled = false
|
||||
end
|
||||
|
||||
it 'does not tick runner queue value' do
|
||||
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating group runners' do
|
||||
let(:group) { create :group }
|
||||
let(:project) { create :project, group: group }
|
||||
let(:runner) { create :ci_runner, groups: [group] }
|
||||
|
||||
context 'when there is a runner that can pick build' do
|
||||
it 'ticks runner queue value' do
|
||||
expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no runner that can pick build due to tag mismatch' do
|
||||
before do
|
||||
build.tag_list = [:docker]
|
||||
end
|
||||
|
||||
it 'does not tick runner queue value' do
|
||||
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no runner that can pick build due to being disabled on project' do
|
||||
before do
|
||||
build.project.group_runners_enabled = false
|
||||
end
|
||||
|
||||
it 'does not tick runner queue value' do
|
||||
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue