Commit b8f2f711 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-08-17' into 'master'

CE upstream - 2018-08-17 09:22 UTC

Closes gitaly#1243, gitaly#1310, and gitaly#1246

See merge request gitlab-org/gitlab-ee!6927
parents 6d89ba84 d19f3bc5
## Details
- **Feature Toggle Name**: `FEATURE_NAME`
- **Required GitLab Version**: `vX.X`
--------------------------------------------------------------------------------
## 1. Preparation
- [ ] **Controllers and workers**:
1. Please link to dashboards of the workers, and the controllers and actions that can be impacted
2. ...
3. ...
## 2. Development Trial
#### Check Dev Server Versions
- [ ] GitLab: https://dev.gitlab.org/help
#### Enable on `dev.gitlab.org`:
- [ ] `/chatops feature set FEATURE_NAME true --dev` in [`#dev-gitlab`](https://gitlab.slack.com/messages/C6WQ87MU3)
Then leave running while monitoring and performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
## 2. Staging Trial
#### Check Staging Server Versions
- [ ] GitLab: https://staging.gitlab.com/help
#### Enable on `staging.gitlab.com`
- [ ] `/chatops run feature set FEATURE_NAME true --staging` in [`#development`](https://gitlab.slack.com/messages/C02PF508L/)
Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 4. Production Server Version Check
- [ ] GitLab: https://gitlab.com/help
## 5. Initial Impact Check
- [ ] Enable for a subset of users, when using percentage gates: 1%.
Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 6. Low Impact Check
- [ ] Enable for a bigger subset of users, when using percentage gates: 10%.
Then leave running while monitoring for at least **30 minutes** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 7. Mid Impact Trial
- [ ] Enable for a big subset of users, when using percentage gates: 50%.
Then leave running while monitoring for at least **12 hours** while performing some testing through web, api or SSH.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
## 8. Full Impact Trial
- [ ] Enable for all users: `/chatops run feature set FEATURE_NAME true
Then leave running while monitoring for at least **1 week**.
#### Monitor
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
#### Success?
- [ ] Remove the feature gate from the code, and close this issue with that MR.
...@@ -772,7 +772,7 @@ GEM ...@@ -772,7 +772,7 @@ GEM
retriable (3.1.1) retriable (3.1.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (3.2.0) rouge (3.2.1)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
......
...@@ -203,7 +203,7 @@ export default { ...@@ -203,7 +203,7 @@ export default {
this.showIssueForm = !this.showIssueForm; this.showIssueForm = !this.showIssueForm;
}, },
onScroll() { onScroll() {
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage(); this.loadNextPage();
} }
}, },
......
...@@ -133,7 +133,6 @@ export default { ...@@ -133,7 +133,6 @@ export default {
.then(() => .then(() =>
this.getRawFileData({ this.getRawFileData({
path: this.file.path, path: this.file.path,
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
}), }),
) )
.then(() => { .then(() => {
......
...@@ -92,7 +92,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => { ...@@ -92,7 +92,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
}; };
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => {
const file = state.entries[path]; const file = state.entries[path];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
service service
...@@ -100,6 +100,9 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = ...@@ -100,6 +100,9 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
.then(raw => { .then(raw => {
if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) { if (file.mrChange && file.mrChange.new_file === false) {
const baseSha =
(getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || '';
service service
.getBaseRawFileData(file, baseSha) .getBaseRawFileData(file, baseSha)
.then(baseRaw => { .then(baseRaw => {
...@@ -122,7 +125,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = ...@@ -122,7 +125,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
action: payload => action: payload =>
dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)), dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'), actionText: __('Please try again'),
actionPayload: { path, baseSha }, actionPayload: { path },
}); });
reject(); reject();
}); });
......
...@@ -200,6 +200,7 @@ export default { ...@@ -200,6 +200,7 @@ export default {
}, },
[types.DELETE_ENTRY](state, path) { [types.DELETE_ENTRY](state, path) {
const entry = state.entries[path]; const entry = state.entries[path];
const { tempFile = false } = entry;
const parent = entry.parentPath const parent = entry.parentPath
? state.entries[entry.parentPath] ? state.entries[entry.parentPath]
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
...@@ -209,7 +210,11 @@ export default { ...@@ -209,7 +210,11 @@ export default {
parent.tree = parent.tree.filter(f => f.path !== entry.path); parent.tree = parent.tree.filter(f => f.path !== entry.path);
if (entry.type === 'blob') { if (entry.type === 'blob') {
state.changedFiles = state.changedFiles.concat(entry); if (tempFile) {
state.changedFiles = state.changedFiles.filter(f => f.path !== path);
} else {
state.changedFiles = state.changedFiles.concat(entry);
}
} }
}, },
[types.RENAME_ENTRY](state, { path, name, entryPath = null }) { [types.RENAME_ENTRY](state, { path, name, entryPath = null }) {
......
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
ClipboardButton,
},
props: {
pipelineShortSha: {
type: String,
required: true,
},
pipelineShaPath: {
type: String,
required: true,
},
mergeRequestReference: {
type: String,
required: false,
default: null,
},
mergeRequestPath: {
type: String,
required: false,
default: null,
},
gitCommitTitlte: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="block">
<p>
{{ __('Commit') }}
<a
:href="pipelineShaPath"
class="js-commit-sha commit-sha link-commit"
>
{{ pipelineShortSha }}
</a>
<clipboard-button
:text="pipelineShortSha"
:title="__('Copy commit SHA to clipboard')"
/>
<a
v-if="mergeRequestPath && mergeRequestReference"
:href="mergeRequestPath"
class="js-link-commit link-commit"
>
{{ mergeRequestReference }}
</a>
</p>
<p class="build-light-text append-bottom-0">
{{ gitCommitTitlte }}
</p>
</div>
</template>
<script>
export default {
props: {
illustrationPath: {
type: String,
required: true,
},
illustrationSizeClass: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
content: {
type: String,
required: false,
default: null,
},
action: {
type: Object,
required: false,
default: null,
validator(value) {
return (
value === null ||
(Object.prototype.hasOwnProperty.call(value, 'link') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'title'))
);
},
},
},
};
</script>
<template>
<div class="row empty-state">
<div class="col-12">
<div
:class="illustrationSizeClass"
class="svg-content"
>
<img :src="illustrationPath" />
</div>
</div>
<div class="col-12">
<div class="text-content">
<h4 class="js-job-empty-state-title text-center">
{{ title }}
</h4>
<p
v-if="content"
class="js-job-empty-state-content"
>
{{ content }}
</p>
<div
v-if="action"
class="text-center"
>
<a
:href="action.link"
:data-method="action.method"
class="js-job-empty-state-action btn btn-primary"
>
{{ action.title }}
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { s__, sprintf } from '~/locale';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
canEraseJob: {
type: Boolean,
required: true,
},
size: {
type: Number,
required: true,
},
rawTracePath: {
type: String,
required: false,
default: null,
},
canScrollToTop: {
type: Boolean,
required: true,
},
canScrollToBottom: {
type: Boolean,
required: true,
},
},
computed: {
jobLogSize() {
return sprintf('Showing last %{startSpanTag} %{size} %{endSpanTag} of log -', {
startSpanTag: '<span class="s-truncated-info-size truncated-info-size">',
endSpanTag: '</span>',
size: numberToHumanSize(this.size),
});
},
},
methods: {
handleEraseJobClick() {
// eslint-disable-next-line no-alert
if (window.confirm(s__('Job|Are you sure you want to erase this job?'))) {
this.$emit('eraseJob');
}
},
handleScrollToTop() {
this.$emit('scrollJobLogTop');
},
handleScrollToBottom() {
this.$emit('scrollJobLogBottom');
},
},
};
</script>
<template>
<div class="top-bar">
<!-- truncate information -->
<div class="js-truncated-info truncated-info d-none d-sm-block float-left">
<p v-html="jobLogSize"></p>
<a
v-if="rawTracePath"
:href="rawTracePath"
class="js-raw-link raw-link"
>
{{ s__("Job|Complete Raw") }}
</a>
</div>
<!-- eo truncate information -->
<div class="controllers float-right">
<!-- links -->
<a
v-tooltip
v-if="rawTracePath"
:title="s__('Job|Show complete raw')"
:href="rawTracePath"
class="js-raw-link-controller controllers-buttons"
data-container="body"
>
<icon name="doc-text" />
</a>
<button
v-tooltip
v-if="canEraseJob"
:title="s__('Job|Erase job log')"
type="button"
class="js-erase-link controllers-buttons"
data-container="body"
@click="handleEraseJobClick"
>
<icon name="remove" />
</button>
<!-- eo links -->
<!-- scroll buttons -->
<div
v-tooltip
:title="s__('Job|Scroll to top')"
class="controllers-buttons"
data-container="body"
>
<button
:disabled="!canScrollToTop"
type="button"
class="js-scroll-top btn-scroll btn-transparent btn-blank"
@click="handleScrollToTop"
>
<icon name="scroll_up"/>
</button>
</div>
<div
v-tooltip
:title="s__('Job|Scroll to bottom')"
class="controllers-buttons"
data-container="body"
>
<button
:disabled="!canScrollToBottom"
type="button"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
@click="handleScrollToBottom"
>
<icon name="scroll_down"/>
</button>
</div>
<!-- eo scroll buttons -->
</div>
</div>
</template>
<script>
export default {
props: {
shortToken: {
type: String,
required: false,
default: null,
},
variables: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
areVariablesVisible: false,
};
},
computed: {
hasVariables() {
return Object.keys(this.variables).length > 0;
},
},
methods: {
revealVariables() {
this.areVariablesVisible = true;
},
},
};
</script>
<template>
<div class="build-widget block">
<h4 class="title">
{{ __('Trigger') }}
</h4>
<p
v-if="shortToken"
class="js-short-token"
>
<span class="build-light-text">
{{ __('Token') }}
</span>
{{ shortToken }}
</p>
<p v-if="hasVariables">
<button
type="button"
class="btn btn-default group js-reveal-variables"
@click="revealVariables"
>
{{ __('Reveal Variables') }}
</button>
</p>
<dl
v-if="areVariablesVisible"
class="js-build-variables trigger-build-variables"
>
<template
v-for="(value, key) in variables"
>
<dt
:key="`${key}-variable`"
class="js-build-variable trigger-build-variable"
>
{{ key }}
</dt>
<dd
:key="`${key}-value`"
class="js-build-value trigger-build-value"
>
{{ value }}
</dd>
</template>
</dl>
</div>
</template>
...@@ -1293,6 +1293,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1293,6 +1293,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
&.build-page .top-bar { &.build-page .top-bar {
top: 0; top: 0;
height: auto;
font-size: 12px; font-size: 12px;
border-top-right-radius: $border-radius-default; border-top-right-radius: $border-radius-default;
} }
......
...@@ -141,9 +141,6 @@ ul.notes { ...@@ -141,9 +141,6 @@ ul.notes {
} }
.note-body { .note-body {
overflow-x: auto;
overflow-y: hidden;
.note-text { .note-text {
@include md-typography; @include md-typography;
// Reset ul style types since we're nested inside a ul already // Reset ul style types since we're nested inside a ul already
......
# frozen_string_literal: true
class AbuseReportMailer < BaseMailer class AbuseReportMailer < BaseMailer
def notify(abuse_report_id) def notify(abuse_report_id)
return unless deliverable? return unless deliverable?
......
# frozen_string_literal: true
class BaseMailer < ActionMailer::Base class BaseMailer < ActionMailer::Base
around_action :render_with_default_locale around_action :render_with_default_locale
......
# frozen_string_literal: true
class DeviseMailer < Devise::Mailer class DeviseMailer < Devise::Mailer
default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>"
default reply_to: Gitlab.config.gitlab.email_reply_to default reply_to: Gitlab.config.gitlab.email_reply_to
...@@ -9,8 +11,9 @@ class DeviseMailer < Devise::Mailer ...@@ -9,8 +11,9 @@ class DeviseMailer < Devise::Mailer
protected protected
def subject_for(key) def subject_for(key)
subject = super subject = [super]
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present?
subject
subject.join(' | ')
end end
end end
# frozen_string_literal: true
class EmailRejectionMailer < BaseMailer class EmailRejectionMailer < BaseMailer
def rejection(reason, original_raw, can_retry = false) def rejection(reason, original_raw, can_retry = false)
@reason = reason @reason = reason
......
# frozen_string_literal: true
module Emails module Emails
module Issues module Issues
def new_issue_email(recipient_id, issue_id, reason = nil) def new_issue_email(recipient_id, issue_id, reason = nil)
......
# frozen_string_literal: true
module Emails module Emails
module Members module Members
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module Emails module Emails
module MergeRequests module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id, reason = nil) def new_merge_request_email(recipient_id, merge_request_id, reason = nil)
......
# frozen_string_literal: true
module Emails module Emails
module Notes module Notes
prepend Emails::EE::Notes prepend Emails::EE::Notes
......
# frozen_string_literal: true
module Emails module Emails
module PagesDomains module PagesDomains
def pages_domain_enabled_email(domain, recipient) def pages_domain_enabled_email(domain, recipient)
......
# frozen_string_literal: true
module Emails module Emails
module Pipelines module Pipelines
def pipeline_success_email(pipeline, recipients) def pipeline_success_email(pipeline, recipients)
...@@ -39,10 +41,10 @@ module Emails ...@@ -39,10 +41,10 @@ module Emails
end end
def pipeline_subject(status) def pipeline_subject(status)
commit = @pipeline.short_sha commit = [@pipeline.short_sha]
commit << " in #{@merge_request.to_reference}" if @merge_request commit << "in #{@merge_request.to_reference}" if @merge_request
subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit) subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit.join(' '))
end end
end end
end end
# frozen_string_literal: true
module Emails module Emails
module Profile module Profile
def new_user_email(user_id, token = nil) def new_user_email(user_id, token = nil)
......
# frozen_string_literal: true
module Emails module Emails
module Projects module Projects
prepend Emails::EE::Projects prepend Emails::EE::Projects
......
# frozen_string_literal: true
class Notify < BaseMailer class Notify < BaseMailer
prepend ::EE::Notify prepend ::EE::Notify
...@@ -94,12 +96,14 @@ class Notify < BaseMailer ...@@ -94,12 +96,14 @@ class Notify < BaseMailer
# >> subject('Lorem ipsum', 'Dolor sit amet') # >> subject('Lorem ipsum', 'Dolor sit amet')
# => "Lorem ipsum | Dolor sit amet" # => "Lorem ipsum | Dolor sit amet"
def subject(*extra) def subject(*extra)
subject = "" subject = []
subject << "#{@project.name} | " if @project
subject << "#{@group.name} | " if @group subject << @project.name if @project
subject << extra.join(' | ') if extra.present? subject << @group.name if @group
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? subject.concat(extra) if extra.present?
subject subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present?
subject.join(' | ')
end end
# Return a string suitable for inclusion in the 'Message-Id' mail header. # Return a string suitable for inclusion in the 'Message-Id' mail header.
......
# frozen_string_literal: true
class DeviseMailerPreview < ActionMailer::Preview class DeviseMailerPreview < ActionMailer::Preview
def confirmation_instructions_for_signup def confirmation_instructions_for_signup
DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {}) DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
......
# frozen_string_literal: true
class EmailRejectionMailerPreview < ActionMailer::Preview class EmailRejectionMailerPreview < ActionMailer::Preview
def rejection def rejection
EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message
......
# frozen_string_literal: true
class NotifyPreview < ActionMailer::Preview class NotifyPreview < ActionMailer::Preview
prepend EE::Preview::NotifyPreview prepend EE::Preview::NotifyPreview
......
# frozen_string_literal: true
class RepositoryCheckMailerPreview < ActionMailer::Preview class RepositoryCheckMailerPreview < ActionMailer::Preview
def notify def notify
RepositoryCheckMailer.notify(3).message RepositoryCheckMailer.notify(3).message
......
# frozen_string_literal: true
class RepositoryCheckMailer < BaseMailer class RepositoryCheckMailer < BaseMailer
def notify(failed_count) def notify(failed_count)
@message = @message =
......
...@@ -71,6 +71,10 @@ module Ci ...@@ -71,6 +71,10 @@ module Ci
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end end
scope :with_archived_trace, ->() do
where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
scope :without_archived_trace, ->() do scope :without_archived_trace, ->() do
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end end
...@@ -81,6 +85,7 @@ module Ci ...@@ -81,6 +85,7 @@ module Ci
end end
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
......
...@@ -11,6 +11,7 @@ class Namespace < ActiveRecord::Base ...@@ -11,6 +11,7 @@ class Namespace < ActiveRecord::Base
include Storage::LegacyNamespace include Storage::LegacyNamespace
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include IgnorableColumn include IgnorableColumn
include FeatureGate
ignore_column :deleted_at ignore_column :deleted_at
...@@ -125,7 +126,6 @@ class Namespace < ActiveRecord::Base ...@@ -125,7 +126,6 @@ class Namespace < ActiveRecord::Base
def to_param def to_param
full_path full_path
end end
alias_method :flipper_id, :to_param
def human_name def human_name
owner_name owner_name
......
...@@ -27,6 +27,7 @@ class Project < ActiveRecord::Base ...@@ -27,6 +27,7 @@ class Project < ActiveRecord::Base
include FastDestroyAll::Helpers include FastDestroyAll::Helpers
include WithUploads include WithUploads
include BatchDestroyDependentAssociations include BatchDestroyDependentAssociations
include FeatureGate
extend Gitlab::Cache::RequestCache extend Gitlab::Cache::RequestCache
# EE specific modules # EE specific modules
...@@ -534,18 +535,19 @@ class Project < ActiveRecord::Base ...@@ -534,18 +535,19 @@ class Project < ActiveRecord::Base
def auto_devops_enabled? def auto_devops_enabled?
if auto_devops&.enabled.nil? if auto_devops&.enabled.nil?
Gitlab::CurrentSettings.auto_devops_enabled? has_auto_devops_implicitly_enabled?
else else
auto_devops.enabled? auto_devops.enabled?
end end
end end
def has_auto_devops_implicitly_enabled? def has_auto_devops_implicitly_enabled?
auto_devops&.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled? auto_devops&.enabled.nil? &&
(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
end end
def has_auto_devops_implicitly_disabled? def has_auto_devops_implicitly_disabled?
auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled? auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
end end
def empty_repo? def empty_repo?
......
...@@ -47,12 +47,8 @@ class ProjectAutoDevops < ActiveRecord::Base ...@@ -47,12 +47,8 @@ class ProjectAutoDevops < ActiveRecord::Base
end end
def needs_to_create_deploy_token? def needs_to_create_deploy_token?
auto_devops_enabled? && project.auto_devops_enabled? &&
!project.public? && !project.public? &&
!project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present? !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present?
end end
def auto_devops_enabled?
Gitlab::CurrentSettings.auto_devops_enabled? || enabled?
end
end end
...@@ -32,7 +32,7 @@ module Projects ...@@ -32,7 +32,7 @@ module Projects
def run_auto_devops_pipeline? def run_auto_devops_pipeline?
return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled') return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled')
project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?) project.auto_devops_enabled?
end end
private private
......
...@@ -41,6 +41,8 @@ ...@@ -41,6 +41,8 @@
.form-text.text-muted .form-text.text-muted
Set the default expiration time for each job's artifacts. Set the default expiration time for each job's artifacts.
0 for unlimited. 0 for unlimited.
The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success"
%fieldset %fieldset
%legend Access %legend Access
.form-group.row .form-group.row
= f.label :projects_limit, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :projects_limit, class: 'col-form-label'
.col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group.row .form-group.row
= f.label :can_create_group, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :can_create_group, class: 'col-form-label'
.col-sm-10= f.check_box :can_create_group .col-sm-10= f.check_box :can_create_group
.form-group.row .form-group.row
= f.label :access_level, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :access_level, class: 'col-form-label'
.col-sm-10 .col-sm-10
- editing_current_user = (current_user == @user) - editing_current_user = (current_user == @user)
...@@ -36,7 +39,8 @@ ...@@ -36,7 +39,8 @@
You cannot remove your own admin rights. You cannot remove your own admin rights.
.form-group.row .form-group.row
= f.label :external, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :external, class: 'col-form-label'
.col-sm-10 .col-sm-10
= f.check_box :external do = f.check_box :external do
External External
......
...@@ -5,17 +5,20 @@ ...@@ -5,17 +5,20 @@
%fieldset %fieldset
%legend Account %legend Account
.form-group.row .form-group.row
= f.label :name, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :name, class: 'col-form-label'
.col-sm-10 .col-sm-10
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required %span.help-inline * required
.form-group.row .form-group.row
= f.label :username, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :username, class: 'col-form-label'
.col-sm-10 .col-sm-10
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control'
%span.help-inline * required %span.help-inline * required
.form-group.row .form-group.row
= f.label :email, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :email, class: 'col-form-label'
.col-sm-10 .col-sm-10
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required %span.help-inline * required
...@@ -24,7 +27,8 @@ ...@@ -24,7 +27,8 @@
%fieldset %fieldset
%legend Password %legend Password
.form-group.row .form-group.row
= f.label :password, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :password, class: 'col-form-label'
.col-sm-10 .col-sm-10
%strong %strong
Reset link will be generated and sent to the user. Reset link will be generated and sent to the user.
...@@ -34,10 +38,12 @@ ...@@ -34,10 +38,12 @@
%fieldset %fieldset
%legend Password %legend Password
.form-group.row .form-group.row
= f.label :password, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :password, class: 'col-form-label'
.col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control' .col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control'
.form-group.row .form-group.row
= f.label :password_confirmation, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :password_confirmation, class: 'col-form-label'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
= render partial: 'access_levels', locals: { f: f } = render partial: 'access_levels', locals: { f: f }
...@@ -53,21 +59,26 @@ ...@@ -53,21 +59,26 @@
%fieldset %fieldset
%legend Profile %legend Profile
.form-group.row .form-group.row
= f.label :avatar, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :avatar, class: 'col-form-label'
.col-sm-10 .col-sm-10
= f.file_field :avatar = f.file_field :avatar
.form-group.row .form-group.row
= f.label :skype, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :skype, class: 'col-form-label'
.col-sm-10= f.text_field :skype, class: 'form-control' .col-sm-10= f.text_field :skype, class: 'form-control'
.form-group.row .form-group.row
= f.label :linkedin, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :linkedin, class: 'col-form-label'
.col-sm-10= f.text_field :linkedin, class: 'form-control' .col-sm-10= f.text_field :linkedin, class: 'form-control'
.form-group.row .form-group.row
= f.label :twitter, class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :twitter, class: 'col-form-label'
.col-sm-10= f.text_field :twitter, class: 'form-control' .col-sm-10= f.text_field :twitter, class: 'form-control'
.form-group.row .form-group.row
= f.label :website_url, 'Website', class: 'col-form-label col-sm-2' .col-sm-2.text-right
= f.label :website_url, 'Website', class: 'col-form-label'
.col-sm-10= f.text_field :website_url, class: 'form-control' .col-sm-10= f.text_field :website_url, class: 'form-control'
%fieldset %fieldset
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
%h5.prepend-top-0 %h5.prepend-top-0
= _("Git strategy for pipelines") = _("Git strategy for pipelines")
%p %p
= _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code") = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code").html_safe
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
.form-check .form-check
= f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
= f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold' = f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted %p.form-text.text-muted
= _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>") = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr %hr
......
...@@ -21,8 +21,7 @@ ...@@ -21,8 +21,7 @@
%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
= render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
- if Feature.enabled?(:repository_languages, @project.namespace.becomes(Namespace)) = repository_languages_bar(@project.repository_languages)
= repository_languages_bar(@project.repository_languages)
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived? - if @project.archived?
......
...@@ -16,8 +16,6 @@ class DetectRepositoryLanguagesWorker ...@@ -16,8 +16,6 @@ class DetectRepositoryLanguagesWorker
user = User.find_by(id: user_id) user = User.find_by(id: user_id)
return unless project && user return unless project && user
return if Feature.disabled?(:repository_languages, project.namespace)
try_obtain_lease do try_obtain_lease do
::Projects::DetectRepositoryLanguagesService.new(project, user).execute ::Projects::DetectRepositoryLanguagesService.new(project, user).execute
end end
......
---
title: "Fix If-Check the result that a function was executed several times"
merge_request: 20640
author: Max Dicker
type: fixed
---
title: Fixes input alignment in user admin form with errors
merge_request: 21108
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Creates vue component for commit block in job log page
merge_request:
author:
type: other
---
title: Creates empty state vue component for job view
merge_request:
author:
type: other
---
title: Creates Vue component for trigger variables block in job log page
merge_request:
author:
type: other
---
title: Creates vue component for job log top bar with controllers
merge_request:
author:
type: other
---
title: Fix issue stopping Instance Statistics javascript to be executed
merge_request: 21211
author:
type: fixed
---
title: Add an example of the configuration of archive trace cron worker in gitlab.yml.example
merge_request: 20583
author:
type: other
---
title: Add rake command to migrate archived traces from local storage to object storage
merge_request: 21193
author:
type: added
---
title: Fix 1px cutoff of emojis
merge_request: 21180
author: gfyoung
type: fixed
---
title: Enable frozen in app/mailers/**/*.rb
merge_request: 21147
author: gfyoung
type: performance
---
title: Fixed IDE deleting new files creating wrong state
merge_request:
author:
type: fixed
---
title: Improved styling of top bar in IDE job trace pane
merge_request:
author:
type: changed
---
title: Combines emoji award spec files into single user_interacts_with_awards_in_issue_spec.rb
file
merge_request: 21126
author: Nate Geslin
type: other
---
title: 'Rails5: Enable verbose query logs'
merge_request: 21231
author: Jasper Maes
type: other
---
title: Update to Rouge 3.2.1, which includes a critical fix to the Perl Lexer
merge_request: 21263
author:
type: changed
...@@ -281,6 +281,9 @@ production: &base ...@@ -281,6 +281,9 @@ production: &base
# once per hour you will have concurrent 'git fsck' jobs. # once per hour you will have concurrent 'git fsck' jobs.
repository_check_worker: repository_check_worker:
cron: "20 * * * *" cron: "20 * * * *"
# Archive live traces which have not been archived yet
ci_archive_traces_cron_worker:
cron: "17 * * * *"
# Send admin emails once a week # Send admin emails once a week
admin_email_worker: admin_email_worker:
cron: "0 0 * * 0" cron: "0 0 * * 0"
......
...@@ -47,7 +47,9 @@ module ActiveRecord ...@@ -47,7 +47,9 @@ module ActiveRecord
end end
end end
unless Gitlab.rails5? if Rails.version.start_with?("5.2")
raise "Remove this monkey patch: #{__FILE__}"
else
prepend(VerboseQueryLogs) unless Rails.env.production? prepend(VerboseQueryLogs) unless Rails.env.production?
end end
end end
......
...@@ -3,10 +3,6 @@ ...@@ -3,10 +3,6 @@
Job traces are sent by GitLab Runner while it's processing a job. You can see Job traces are sent by GitLab Runner while it's processing a job. You can see
traces in job pages, pipelines, email notifications, etc. traces in job pages, pipelines, email notifications, etc.
There isn't a way to automatically expire old job logs, but it's safe to remove
them if they're taking up too much space. If you remove the logs manually, the
job output in the UI will be empty.
## Data flow ## Data flow
In general, there are two states in job traces: "live trace" and "archived trace". In general, there are two states in job traces: "live trace" and "archived trace".
...@@ -57,11 +53,55 @@ To change the location where the job logs will be stored, follow the steps below ...@@ -57,11 +53,55 @@ To change the location where the job logs will be stored, follow the steps below
## Uploading traces to object storage ## Uploading traces to object storage
An archived trace is considered as a [job artifact](job_artifacts.md). Archived traces are considered as [job artifacts](job_artifacts.md).
Therefore, when you [set up an object storage](job_artifacts.md#object-storage-settings), Therefore, when you [set up the object storage integration](job_artifacts.md#object-storage-settings),
job traces are automatically migrated to it along with the other job artifacts. job traces are automatically migrated to it along with the other job artifacts.
See [Data flow](#data-flow) to learn about the process. See "Phase 4: uploading" in [Data flow](#data-flow) to learn about the process.
## How to archive legacy job trace files
Legacy job traces, which were created before GitLab 10.5, were not archived regularly.
It's the same state with the "2: overwriting" in the above [Data flow](#data-flow).
To archive those legacy job traces, please follow the instruction below.
1. Execute the following command
```bash
gitlab-rake gitlab:traces:archive
```
After you executed this task, GitLab instance queues up Sidekiq jobs (asynchronous processes)
for migrating job trace files from local storage to object storage.
It could take time to complete the all migration jobs. You can check the progress by the following command
```bash
sudo gitlab-rails console
```
```bash
[1] pry(main)> Sidekiq::Stats.new.queues['pipeline_background:archive_trace']
=> 100
```
If the count becomes zero, the archiving processes are done
## How to migrate archived job traces to object storage
If job traces have already been archived into local storage, and you want to migrate those traces to object storage, please follow the instruction below.
1. Ensure [Object storage integration for Job Artifacts](job_artifacts.md#object-storage-settings) is enabled
1. Execute the following command
```bash
gitlab-rake gitlab:traces:migrate
```
## How to remove job traces
There isn't a way to automatically expire old job logs, but it's safe to remove
them if they're taking up too much space. If you remove the logs manually, the
job output in the UI will be empty.
## New live trace architecture ## New live trace architecture
......
...@@ -6,3 +6,4 @@ comments: false ...@@ -6,3 +6,4 @@ comments: false
- [Using Docker Images](using_docker_images.md) - [Using Docker Images](using_docker_images.md)
- [Using Docker Build](using_docker_build.md) - [Using Docker Build](using_docker_build.md)
- [Using kaniko](using_kaniko.md)
# Building images with kaniko and GitLab CI/CD
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45512) in GitLab 11.2.
Requires GitLab Runner 11.2 and above.
[kaniko](https://github.com/GoogleContainerTools/kaniko) is a tool to build
container images from a Dockerfile, inside a container or Kubernetes cluster.
kaniko solves two problems with using the
[docker-in-docker build](using_docker_build.md#use-docker-in-docker-executor) method:
1. Docker-in-docker requires [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
in order to function, which is a significant security concern.
1. Docker-in-docker generally incurs a performance penalty and can be quite slow.
## Requirements
In order to utilize kaniko with GitLab, a [GitLab Runner](https://docs.gitlab.com/runner/)
using either the [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html),
[Docker](https://docs.gitlab.com/runner/executors/docker.html), or
[Docker Machine](https://docs.gitlab.com/runner/executors/docker_machine.html)
executors is required.
## Building a Docker image with kaniko
When building an image with kaniko and GitLab CI/CD, you should be aware of a
few important details:
- The kaniko debug image is recommended (`gcr.io/kaniko-project/executor:debug`)
because it has a shell, and a shell is required for an image to be used with
GitLab CI/CD.
- The entrypoint will need to be [overridden](using_docker_images.md#overriding-the-entrypoint-of-an-image),
otherwise the build script will not run.
- A Docker `config.json` file needs to be created with the authentication
information for the desired container registry.
---
In the following example, kaniko is used to build a Docker image and then push
it to [GitLab Container Registry](../../user/project/container_registry.md).
The job will run only when a tag is pushed. A `config.json` file is created under
`/root/.docker` with the needed GitLab Container Registry credentials taken from the
[environment variables](../variables/README.md#predefined-variables-environment-variables)
GitLab CI/CD provides. In the last step, kaniko uses the `Dockerfile` under the
root directory of the project, builds the Docker image and pushes it to the
project's Container Registry while tagging it with the Git tag:
```yaml
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
script:
- mkdir -p /root/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /root/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
only:
- tags
```
...@@ -1075,8 +1075,10 @@ keep artifacts forever. ...@@ -1075,8 +1075,10 @@ keep artifacts forever.
After their expiry, artifacts are deleted hourly by default (via a cron job), After their expiry, artifacts are deleted hourly by default (via a cron job),
and are not accessible anymore. and are not accessible anymore.
The value of `expire_in` is an elapsed time. Examples of parsable values: The value of `expire_in` is an elapsed time in seconds, unless a unit is
provided. Examples of parsable values:
- '42'
- '3 mins 4 sec' - '3 mins 4 sec'
- '2 hrs 20 min' - '2 hrs 20 min'
- '2h20min' - '2h20min'
......
...@@ -20,7 +20,40 @@ dynamic (querying the DB etc.). ...@@ -20,7 +20,40 @@ dynamic (querying the DB etc.).
Once defined in `lib/feature.rb`, you will be able to activate a Once defined in `lib/feature.rb`, you will be able to activate a
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature) feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
For GitLab.com, team members have access to feature flags through chatops. Only
percentage gates are supported at this time. Setting a feature to be used 50% of
the time, you should execute `/chatops run feature set my_feature_flag 50`.
## Feature flags for user applications ## Feature flags for user applications
GitLab does not yet support the use of feature flags in deployed user applications. GitLab does not yet support the use of feature flags in deployed user applications.
You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779). You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779).
\ No newline at end of file
## Developing with feature flags
In general, it's better to have a group- or user-based gate, and you should prefer
it over the use of percentage gates. This would make debugging easier, as you
filter for example logs and errors based on actors too. Futhermore, this allows
for enabling for the `gitlab-org` group first, while the rest of the users
aren't impacted.
```ruby
# Good
Feature.enabled?(:feature_flag, project)
# Avoid, if possible
Feature.enabled?(:feature_flag)
```
To use feature gates based on actors, the model needs to respond to
`flipper_id`. For example, to enable for the Foo model:
```ruby
class Foo < ActiveRecord::Base
include FeatureGate
end
```
Features that are developed and are intended to be merged behind a feature flag
should not include a changelog entry. The entry should be added in the merge
request removing the feature flags.
...@@ -233,6 +233,11 @@ in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that ...@@ -233,6 +233,11 @@ in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that
all the projects that haven't explicitly set an option will have Auto DevOps all the projects that haven't explicitly set an option will have Auto DevOps
enabled by default. enabled by default.
NOTE: **Note:**
There is also a feature flag to enable Auto DevOps to a percentage of projects
which can be enabled from the console with
`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`.
### Deployment strategy ### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
......
...@@ -21,8 +21,9 @@ that this setting is set for each job. ...@@ -21,8 +21,9 @@ that this setting is set for each job.
The default expiration time of the [job artifacts][art-yml] can be set in The default expiration time of the [job artifacts][art-yml] can be set in
the Admin area of your GitLab instance. The syntax of duration is described the Admin area of your GitLab instance. The syntax of duration is described
in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that
this setting is set for each job. Set it to 0 if you don't want default this setting is set for each job. Set it to `0` if you don't want default
expiration. expiration. The default unit is in seconds.
1. Go to **Admin area > Settings** (`/admin/application_settings`). 1. Go to **Admin area > Settings** (`/admin/application_settings`).
......
...@@ -235,7 +235,12 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji ...@@ -235,7 +235,12 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
...@@ -247,7 +252,13 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil ...@@ -247,7 +252,13 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
### Special GitLab References ### Special GitLab References
......
# Import multiple repositories by uploading a manifest file # Import multiple repositories by uploading a manifest file
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28811) in
GitLab 11.2.
GitLab allows you to import all the required Git repositories GitLab allows you to import all the required Git repositories
based on a manifest file like the one used by the based on a manifest file like the one used by the
[Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml). [Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml).
......
# Hangouts Chat service # Hangouts Chat service
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/43756) in GitLab 11.2.
The Hangouts Chat service sends notifications from GitLab to the room for which the webhook was created. The Hangouts Chat service sends notifications from GitLab to the room for which the webhook was created.
## On Hangouts Chat ## On Hangouts Chat
......
...@@ -155,6 +155,16 @@ The repository graph displays visually the Git flow strategy used in that reposi ...@@ -155,6 +155,16 @@ The repository graph displays visually the Git flow strategy used in that reposi
Find it under your project's **Repository > Graph**. Find it under your project's **Repository > Graph**.
## Repository Languages
For the default branch of each repository, GitLab will determine what programming languages
were used and display this on the projects pages.
![Repository Languages bar](img/repository_languages.png)
Not all files are detected, among others; documentation,
vendored code, and most markup languages are excluded.
## Compare ## Compare
Select branches to compare and view the changes inline: Select branches to compare and view the changes inline:
......
...@@ -543,14 +543,8 @@ module Gitlab ...@@ -543,14 +543,8 @@ module Gitlab
end end
def update_branch(branch_name, user:, newrev:, oldrev:) def update_branch(branch_name, user:, newrev:, oldrev:)
gitaly_migrate(:operation_user_update_branch) do |is_enabled| wrapped_gitaly_errors do
if is_enabled gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
else
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
OperationService.new(user, self).update_branch(branch_name, newrev, oldrev)
end
end
end end
end end
......
...@@ -2,34 +2,7 @@ module Gitlab ...@@ -2,34 +2,7 @@ module Gitlab
module Git module Git
module RepositoryMirroring module RepositoryMirroring
def remote_branches(remote_name) def remote_branches(remote_name)
gitaly_migrate(:ref_find_all_remote_branches) do |is_enabled| gitaly_ref_client.remote_branches(remote_name)
if is_enabled
gitaly_ref_client.remote_branches(remote_name)
else
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
rugged_remote_branches(remote_name)
end
end
end
end
private
def rugged_remote_branches(remote_name)
branches = []
rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
name = ref.name.sub(%r{\Arefs/remotes/#{remote_name}/}, '')
begin
target_commit = Gitlab::Git::Commit.find(self, ref.target.oid)
branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit)
rescue Rugged::ReferenceError
# Omit invalid branch
end
end
branches
end end
end end
end end
......
...@@ -18,5 +18,22 @@ namespace :gitlab do ...@@ -18,5 +18,22 @@ namespace :gitlab do
logger.info("Scheduled #{job_ids.count} jobs. From #{job_ids.min} to #{job_ids.max}") logger.info("Scheduled #{job_ids.count} jobs. From #{job_ids.min} to #{job_ids.max}")
end end
end end
task migrate: :environment do
logger = Logger.new(STDOUT)
logger.info('Starting transfer of job traces')
Ci::Build.joins(:project)
.with_archived_trace_stored_locally
.find_each(batch_size: 10) do |build|
begin
build.job_artifacts_trace.file.migrate!(ObjectStorage::Store::REMOTE)
logger.info("Transferred job trace of #{build.id} to object storage")
rescue => e
logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}")
end
end
end
end end
end end
...@@ -4073,12 +4073,21 @@ msgstr "" ...@@ -4073,12 +4073,21 @@ msgstr ""
msgid "Jobs" msgid "Jobs"
msgstr "" msgstr ""
msgid "Job|Are you sure you want to erase this job?"
msgstr ""
msgid "Job|Browse" msgid "Job|Browse"
msgstr "" msgstr ""
msgid "Job|Complete Raw"
msgstr ""
msgid "Job|Download" msgid "Job|Download"
msgstr "" msgstr ""
msgid "Job|Erase job log"
msgstr ""
msgid "Job|Job artifacts" msgid "Job|Job artifacts"
msgstr "" msgstr ""
...@@ -4091,6 +4100,15 @@ msgstr "" ...@@ -4091,6 +4100,15 @@ msgstr ""
msgid "Job|Keep" msgid "Job|Keep"
msgstr "" msgstr ""
msgid "Job|Scroll to bottom"
msgstr ""
msgid "Job|Scroll to top"
msgstr ""
msgid "Job|Show complete raw"
msgstr ""
msgid "Job|The artifacts were removed" msgid "Job|The artifacts were removed"
msgstr "" msgstr ""
...@@ -6069,6 +6087,9 @@ msgstr "" ...@@ -6069,6 +6087,9 @@ msgstr ""
msgid "Retry verification" msgid "Retry verification"
msgstr "" msgstr ""
msgid "Reveal Variables"
msgstr ""
msgid "Reveal value" msgid "Reveal value"
msgid_plural "Reveal values" msgid_plural "Reveal values"
msgstr[0] "" msgstr[0] ""
...@@ -7464,6 +7485,9 @@ msgstr "" ...@@ -7464,6 +7485,9 @@ msgstr ""
msgid "Trending" msgid "Trending"
msgstr "" msgstr ""
msgid "Trigger"
msgstr ""
msgid "Trigger pipelines for mirror updates" msgid "Trigger pipelines for mirror updates"
msgstr "" msgstr ""
......
...@@ -12,4 +12,12 @@ describe 'Cohorts page' do ...@@ -12,4 +12,12 @@ describe 'Cohorts page' do
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
end end
it 'shows usage data', :js do
visit instance_statistics_cohorts_path
wait_for_requests
expect(find('.js-syntax-highlight').text).not_to eq('')
end
end end
...@@ -5,6 +5,16 @@ describe 'Conversational Development Index' do ...@@ -5,6 +5,16 @@ describe 'Conversational Development Index' do
sign_in(create(:admin)) sign_in(create(:admin))
end end
it 'has dismissable intro callout', :js do
visit instance_statistics_conversational_development_index_index_path
expect(page).to have_content 'Introducing Your Conversational Development Index'
find('.js-close-callout').click
expect(page).not_to have_content 'Introducing Your Conversational Development Index'
end
context 'when usage ping is disabled' do context 'when usage ping is disabled' do
it 'shows empty state' do it 'shows empty state' do
stub_application_setting(usage_ping_enabled: false) stub_application_setting(usage_ping_enabled: false)
......
require 'rails_helper'
describe 'Awards Emoji' do
let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
let(:issue) do
create(:issue,
assignees: [user],
project: project)
end
context 'authorized user' do
before do
project.add_maintainer(user)
sign_in(user)
end
describe 'visiting an issue with a legacy award emoji that is not valid anymore' do
before do
# The `heart_tip` emoji is not valid anymore so we need to skip validation
issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
visit project_issue_path(project, issue)
wait_for_requests
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
it 'does not shows a 500 page', :js do
expect(page).to have_text(issue.title)
end
end
describe 'Click award emoji from issue#show' do
let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
before do
visit project_issue_path(project, issue)
wait_for_requests
end
it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
it 'increments the thumbsup emoji', :js do
find('[data-name="thumbsup"]').click
wait_for_requests
expect(thumbsup_emoji).to have_text("1")
end
it 'decrements the thumbsdown emoji', :js do
expect(thumbsdown_emoji).to have_text("0")
end
end
context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
it 'decrements the thumbsup emoji', :js do
expect(thumbsup_emoji).to have_text("0")
end
end
it 'toggles the smiley emoji on a note', :js do
toggle_smiley_emoji(true)
within('.note-body') do
expect(find(emoji_counter)).to have_text("1")
end
toggle_smiley_emoji(false)
within('.note-body') do
expect(page).not_to have_selector(emoji_counter)
end
end
context 'execute /award quick action' do
it 'toggles the emoji award on noteable', :js do
execute_quick_action('/award :100:')
expect(find(noteable_award_counter)).to have_text("1")
execute_quick_action('/award :100:')
expect(page).not_to have_selector(noteable_award_counter)
end
end
end
end
context 'unauthorized user', :js do
before do
visit project_issue_path(project, issue)
end
it 'has disabled emoji button' do
expect(first('.award-control')[:class]).to have_text('disabled')
end
end
def execute_quick_action(cmd)
within('.js-main-target-form') do
fill_in 'note[note]', with: cmd
click_button 'Comment'
end
wait_for_requests
end
def thumbsup_emoji
page.all(emoji_counter).first
end
def thumbsdown_emoji
page.all(emoji_counter).last
end
def emoji_counter
'span.js-counter'
end
def noteable_award_counter
".awards .active"
end
def toggle_smiley_emoji(status)
within('.note') do
find('.note-emoji-button').click
end
unless status
first('[data-name="smiley"]').click
else
find('[data-name="smiley"]').click
end
wait_for_requests
end
end
require 'rails_helper'
describe 'Issue awards', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
describe 'logged in' do
before do
sign_in(user)
visit project_issue_path(project, issue)
wait_for_requests
end
it 'adds award to issue' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
visit project_issue_path(project, issue)
expect(first('.js-emoji-btn')).to have_content '1'
end
it 'removes award from issue' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
visit project_issue_path(project, issue)
expect(first('.js-emoji-btn')).to have_content '0'
end
it 'only has one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
expect(page).to have_selector('.emoji-menu', count: 1)
end
end
describe 'logged out' do
before do
visit project_issue_path(project, issue)
wait_for_requests
end
it 'does not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
end
require 'spec_helper'
describe 'User interacts with awards' do
let(:user) { create(:user) }
describe 'User interacts with awards in an issue', :js do
let(:issue) { create(:issue, project: project)}
let(:project) { create(:project) }
before do
project.add_maintainer(user)
sign_in(user)
visit(project_issue_path(project, issue))
end
it 'toggles the thumbsup award emoji' do
page.within('.awards') do
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
expect(page).to have_selector('.js-emoji-btn')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
expect(page).to have_selector('.award-control.js-emoji-btn')
expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
page.all('.award-control.js-emoji-btn').each do |element|
expect(element['title']).to eq('')
end
expect(page.all('.award-control .js-counter')).to all(have_content('0'))
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
expect(page).to have_selector('.js-emoji-btn')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
end
end
it 'toggles a custom award emoji' do
page.within('.awards') do
page.find('.js-add-award').click
end
page.find('.emoji-menu.is-visible')
expect(page).to have_selector('.js-emoji-menu-search')
expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
page.within('.emoji-menu-content') do
emoji_button = page.first('.js-emoji-btn')
emoji_button.hover
emoji_button.click
end
page.within('.awards') do
expect(page).to have_selector('.js-emoji-btn')
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
expect do
page.find('.js-emoji-btn.active').click
wait_for_requests
end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2)
end
end
it 'shows the list of award emoji categories' do
page.within('.awards') do
page.find('.js-add-award').click
end
page.find('.emoji-menu.is-visible')
expect(page).to have_selector('.js-emoji-menu-search')
expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
fill_in('emoji-menu-search', with: 'hand')
page.within('.emoji-menu-content') do
expect(page).to have_selector('[data-name="raised_hand"]')
end
end
it 'adds an award emoji by a comment' do
page.within('.js-main-target-form') do
fill_in('note[note]', with: ':smile:')
click_button('Comment')
end
expect(page).to have_emoji('smile')
end
context 'when a project is archived' do
let(:project) { create(:project, :archived) }
it 'hides the add award button' do
page.within('.awards') do
expect(page).not_to have_css('.js-add-award')
end
end
end
context 'User interacts with awards on a note' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') }
it 'shows the award on the note' do
page.within('.note-awards') do
expect(page).to have_emoji('100')
end
end
it 'allows adding a vote to an award' do
page.within('.note-awards') do
find('gl-emoji[data-name="100"]').click
end
wait_for_requests
expect(note.reload.award_emoji.size).to eq(2)
end
it 'allows adding a new emoji' do
page.within('.note-actions') do
find('a.js-add-award').click
end
page.within('.emoji-menu-content') do
find('gl-emoji[data-name="8ball"]').click
end
wait_for_requests
page.within('.note-awards') do
expect(page).to have_emoji('8ball')
end
expect(note.reload.award_emoji.size).to eq(2)
end
context 'when the project is archived' do
let(:project) { create(:project, :archived) }
it 'hides the buttons for adding new emoji' do
page.within('.note-awards') do
expect(page).not_to have_css('.award-menu-holder')
end
page.within('.note-actions') do
expect(page).not_to have_css('a.js-add-award')
end
end
it 'does not allow toggling existing emoji' do
page.within('.note-awards') do
find('gl-emoji[data-name="100"]').click
end
wait_for_requests
expect(note.reload.award_emoji.size).to eq(1)
end
end
end
end
describe 'User interacts with awards on an issue', :js do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
describe 'logged in' do
before do
sign_in(user)
visit project_issue_path(project, issue)
wait_for_requests
end
it 'adds award to issue' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
visit project_issue_path(project, issue)
expect(first('.js-emoji-btn')).to have_content '1'
end
it 'removes award from issue' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
visit project_issue_path(project, issue)
expect(first('.js-emoji-btn')).to have_content '0'
end
it 'only has one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu', count: 1)
end
end
describe 'logged out' do
before do
visit project_issue_path(project, issue)
wait_for_requests
end
it 'does not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
end
describe 'Awards Emoji' do
let!(:project) { create(:project, :public) }
let(:issue) { create(:issue, assignees: [user], project: project) }
context 'authorized user' do
before do
project.add_maintainer(user)
sign_in(user)
end
describe 'visiting an issue with a legacy award emoji that is not valid anymore' do
before do
# The `heart_tip` emoji is not valid anymore so we need to skip validation
issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
visit project_issue_path(project, issue)
wait_for_requests
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
it 'does not shows a 500 page', :js do
expect(page).to have_text(issue.title)
end
end
describe 'Click award emoji from issue#show' do
let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
before do
visit project_issue_path(project, issue)
wait_for_requests
end
context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', :js do
find('[data-name="thumbsdown"]').click
wait_for_requests
expect(thumbsdown_emoji).to have_text("1")
end
it 'decrements the thumbsup emoji', :js do
expect(thumbsup_emoji).to have_text("0")
end
end
it 'toggles the smiley emoji on a note', :js do
toggle_smiley_emoji(true)
within('.note-body') do
expect(find(emoji_counter)).to have_text("1")
end
toggle_smiley_emoji(false)
within('.note-body') do
expect(page).not_to have_selector(emoji_counter)
end
end
context 'execute /award quick action' do
it 'toggles the emoji award on noteable', :js do
execute_quick_action('/award :100:')
expect(find(noteable_award_counter)).to have_text("1")
execute_quick_action('/award :100:')
expect(page).not_to have_selector(noteable_award_counter)
end
end
end
end
context 'unauthorized user', :js do
before do
visit project_issue_path(project, issue)
end
it 'has disabled emoji button' do
expect(first('.award-control')[:class]).to have_text('disabled')
end
end
def execute_quick_action(cmd)
within('.js-main-target-form') do
fill_in 'note[note]', with: cmd
click_button 'Comment'
end
wait_for_requests
end
def thumbsup_emoji
page.all(emoji_counter).first
end
def thumbsdown_emoji
page.all(emoji_counter).last
end
def emoji_counter
'span.js-counter'
end
def noteable_award_counter
".awards .active"
end
def toggle_smiley_emoji(status)
within('.note') do
find('.note-emoji-button').click
end
if !status
first('[data-name="smiley"]').click
else
find('[data-name="smiley"]').click
end
wait_for_requests
end
end
end
require 'spec_helper'
describe 'User interacts with awards in an issue', :js do
let(:issue) { create(:issue, project: project)}
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
visit(project_issue_path(project, issue))
end
it 'toggles the thumbsup award emoji' do
page.within('.awards') do
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
expect(page).to have_selector('.js-emoji-btn')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
expect(page).to have_selector('.award-control.js-emoji-btn')
expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
page.all('.award-control.js-emoji-btn').each do |element|
expect(element['title']).to eq('')
end
page.all('.award-control .js-counter').each do |element|
expect(element).to have_content('0')
end
thumbsup = page.first('.award-control')
thumbsup.click
thumbsup.hover
expect(page).to have_selector('.js-emoji-btn')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
end
end
it 'toggles a custom award emoji' do
page.within('.awards') do
page.find('.js-add-award').click
end
page.find('.emoji-menu.is-visible')
expect(page).to have_selector('.js-emoji-menu-search')
expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
page.within('.emoji-menu-content') do
emoji_button = page.first('.js-emoji-btn')
emoji_button.hover
emoji_button.click
end
page.within('.awards') do
expect(page).to have_selector('.js-emoji-btn')
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
expect do
page.find('.js-emoji-btn.active').click
wait_for_requests
end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2)
end
end
it 'shows the list of award emoji categories' do
page.within('.awards') do
page.find('.js-add-award').click
end
page.find('.emoji-menu.is-visible')
expect(page).to have_selector('.js-emoji-menu-search')
expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
fill_in('emoji-menu-search', with: 'hand')
page.within('.emoji-menu-content') do
expect(page).to have_selector('[data-name="raised_hand"]')
end
end
it 'adds an award emoji by a comment' do
page.within('.js-main-target-form') do
fill_in('note[note]', with: ':smile:')
click_button('Comment')
end
expect(page).to have_emoji('smile')
end
context 'when a project is archived' do
let(:project) { create(:project, :archived) }
it 'hides the add award button' do
page.within('.awards') do
expect(page).not_to have_css('.js-add-award')
end
end
end
context 'awards on a note' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') }
it 'shows the award on the note' do
page.within('.note-awards') do
expect(page).to have_emoji('100')
end
end
it 'allows adding a vote to an award' do
page.within('.note-awards') do
find('gl-emoji[data-name="100"]').click
end
wait_for_requests
expect(note.reload.award_emoji.size).to eq(2)
end
it 'allows adding a new emoji' do
page.within('.note-actions') do
find('a.js-add-award').click
end
page.within('.emoji-menu-content') do
find('gl-emoji[data-name="8ball"]').click
end
wait_for_requests
page.within('.note-awards') do
expect(page).to have_emoji('8ball')
end
expect(note.reload.award_emoji.size).to eq(2)
end
context 'when the project is archived' do
let(:project) { create(:project, :archived) }
it 'hides the buttons for adding new emoji' do
page.within('.note-awards') do
expect(page).not_to have_css('.award-menu-holder')
end
page.within('.note-actions') do
expect(page).not_to have_css('a.js-add-award')
end
end
it 'does not allow toggling existing emoji' do
page.within('.note-awards') do
find('gl-emoji[data-name="100"]').click
end
wait_for_requests
expect(note.reload.award_emoji.size).to eq(1)
end
end
end
end
...@@ -210,6 +210,15 @@ describe('Board list component', () => { ...@@ -210,6 +210,15 @@ describe('Board list component', () => {
}); });
}); });
it('does not load issues if already loading', () => {
component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {}));
component.onScroll();
component.onScroll();
expect(component.list.nextPage).toHaveBeenCalledTimes(1);
});
it('shows loading more spinner', (done) => { it('shows loading more spinner', (done) => {
component.showCount = true; component.showCount = true;
component.list.loadingMore = true; component.list.loadingMore = true;
......
...@@ -352,10 +352,22 @@ describe('IDE store file actions', () => { ...@@ -352,10 +352,22 @@ describe('IDE store file actions', () => {
it('calls also getBaseRawFileData service method', done => { it('calls also getBaseRawFileData service method', done => {
spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw')); spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
store.state.currentProjectId = 'gitlab-org/gitlab-ce';
store.state.currentMergeRequestId = '1';
store.state.projects = {
'gitlab-org/gitlab-ce': {
mergeRequests: {
1: {
baseCommitSha: 'SHA',
},
},
},
};
tmpFile.mrChange = { new_file: false }; tmpFile.mrChange = { new_file: false };
store store
.dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' }) .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => { .then(() => {
expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA'); expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
expect(tmpFile.baseRaw).toBe('baseraw'); expect(tmpFile.baseRaw).toBe('baseraw');
...@@ -392,10 +404,7 @@ describe('IDE store file actions', () => { ...@@ -392,10 +404,7 @@ describe('IDE store file actions', () => {
const dispatch = jasmine.createSpy('dispatch'); const dispatch = jasmine.createSpy('dispatch');
actions actions
.getRawFileData( .getRawFileData({ state: store.state, commit() {}, dispatch }, { path: tmpFile.path })
{ state: store.state, commit() {}, dispatch },
{ path: tmpFile.path, baseSha: tmpFile.baseSha },
)
.then(done.fail) .then(done.fail)
.catch(() => { .catch(() => {
expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
...@@ -404,7 +413,6 @@ describe('IDE store file actions', () => { ...@@ -404,7 +413,6 @@ describe('IDE store file actions', () => {
actionText: 'Please try again', actionText: 'Please try again',
actionPayload: { actionPayload: {
path: tmpFile.path, path: tmpFile.path,
baseSha: tmpFile.baseSha,
}, },
}); });
......
...@@ -213,6 +213,33 @@ describe('Multi-file store mutations', () => { ...@@ -213,6 +213,33 @@ describe('Multi-file store mutations', () => {
expect(localState.changedFiles).toEqual([localState.entries.filePath]); expect(localState.changedFiles).toEqual([localState.entries.filePath]);
}); });
it('does not add tempFile into changedFiles', () => {
localState.entries.filePath = {
deleted: false,
type: 'blob',
tempFile: true,
};
mutations.DELETE_ENTRY(localState, 'filePath');
expect(localState.changedFiles).toEqual([]);
});
it('removes tempFile from changedFiles when deleted', () => {
localState.entries.filePath = {
path: 'filePath',
deleted: false,
type: 'blob',
tempFile: true,
};
localState.changedFiles.push({ ...localState.entries.filePath });
mutations.DELETE_ENTRY(localState, 'filePath');
expect(localState.changedFiles).toEqual([]);
});
}); });
describe('UPDATE_FILE_AFTER_COMMIT', () => { describe('UPDATE_FILE_AFTER_COMMIT', () => {
......
import Vue from 'vue';
import component from '~/jobs/components/commit_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Commit block', () => {
const Component = Vue.extend(component);
let vm;
const props = {
pipelineShortSha: '1f0fb84f',
pipelineShaPath: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
mergeRequestReference: '!21244',
mergeRequestPath: 'merge_requests/21244',
gitCommitTitlte: 'Regenerate pot files',
};
afterEach(() => {
vm.$destroy();
});
describe('pipeline short sha', () => {
beforeEach(() => {
vm = mountComponent(Component, {
...props,
});
});
it('renders pipeline short sha link', () => {
expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(props.pipelineShaPath);
expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(props.pipelineShortSha);
});
it('renders clipboard button', () => {
expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(props.pipelineShortSha);
});
});
describe('with merge request', () => {
it('renders merge request link and reference', () => {
vm = mountComponent(Component, {
...props,
});
expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(props.mergeRequestPath);
expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(props.mergeRequestReference);
});
});
describe('without merge request', () => {
it('does not render merge request', () => {
const copyProps = Object.assign({}, props);
delete copyProps.mergeRequestPath;
delete copyProps.mergeRequestReference;
vm = mountComponent(Component, {
...copyProps,
});
expect(vm.$el.querySelector('.js-link-commit')).toBeNull();
});
});
describe('git commit title', () => {
it('renders git commit title', () => {
vm = mountComponent(Component, {
...props,
});
expect(vm.$el.textContent).toContain(props.gitCommitTitlte);
});
});
});
import Vue from 'vue';
import component from '~/jobs/components/job_log_controllers.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Job log controllers', () => {
const Component = Vue.extend(component);
let vm;
afterEach(() => {
vm.$destroy();
});
describe('Truncate information', () => {
beforeEach(() => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: true,
size: 511952,
canScrollToTop: true,
canScrollToBottom: true,
});
});
it('renders size information', () => {
expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB');
});
it('renders link to raw trace', () => {
expect(vm.$el.querySelector('.js-raw-link').getAttribute('href')).toEqual('/raw');
});
});
describe('links section', () => {
describe('with raw trace path', () => {
it('renders raw trace link', () => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: true,
size: 511952,
canScrollToTop: true,
canScrollToBottom: true,
});
expect(vm.$el.querySelector('.js-raw-link-controller').getAttribute('href')).toEqual('/raw');
});
});
describe('without raw trace path', () => {
it('does not render raw trace link', () => {
vm = mountComponent(Component, {
canEraseJob: true,
size: 511952,
canScrollToTop: true,
canScrollToBottom: true,
});
expect(vm.$el.querySelector('.js-raw-link-controller')).toBeNull();
});
});
describe('when is erasable', () => {
beforeEach(() => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: true,
size: 511952,
canScrollToTop: true,
canScrollToBottom: true,
});
});
it('renders erase job button', () => {
expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull();
});
describe('on click', () => {
describe('when user confirms action', () => {
it('emits eraseJob event', () => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(vm, '$emit');
vm.$el.querySelector('.js-erase-link').click();
expect(vm.$emit).toHaveBeenCalledWith('eraseJob');
});
});
describe('when user does not confirm action', () => {
it('does not emit eraseJob event', () => {
spyOn(window, 'confirm').and.returnValue(false);
spyOn(vm, '$emit');
vm.$el.querySelector('.js-erase-link').click();
expect(vm.$emit).not.toHaveBeenCalledWith('eraseJob');
});
});
});
});
describe('when it is not erasable', () => {
it('does not render erase button', () => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: false,
size: 511952,
canScrollToTop: true,
canScrollToBottom: true,
});
expect(vm.$el.querySelector('.js-erase-link')).toBeNull();
});
});
});
describe('scroll buttons', () => {
describe('scroll top button', () => {
describe('when user can scroll top', () => {
beforeEach(() => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: true,
size: 511952,
canScrollToTop: true,
canScrollToBottom: true,
});
});
it('renders enabled scroll top button', () => {
expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toBeNull();
});
it('emits scrollJobLogTop event on click', () => {
spyOn(vm, '$emit');
vm.$el.querySelector('.js-scroll-top').click();
expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogTop');
});
});
describe('when user can not scroll top', () => {
beforeEach(() => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: true,
size: 511952,
canScrollToTop: false,
canScrollToBottom: true,
});
});
it('renders disabled scroll top button', () => {
expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toEqual('disabled');
});
it('does not emit scrollJobLogTop event on click', () => {
spyOn(vm, '$emit');
vm.$el.querySelector('.js-scroll-top').click();
expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogTop');
});
});
});
describe('scroll bottom button', () => {
describe('when user can scroll bottom', () => {
beforeEach(() => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: true,
size: 511952,
canScrollToTop: true,
canScrollToBottom: true,
});
});
it('renders enabled scroll bottom button', () => {
expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toBeNull();
});
it('emits scrollJobLogBottom event on click', () => {
spyOn(vm, '$emit');
vm.$el.querySelector('.js-scroll-bottom').click();
expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogBottom');
});
});
describe('when user can not scroll bottom', () => {
beforeEach(() => {
vm = mountComponent(Component, {
rawTracePath: '/raw',
canEraseJob: true,
size: 511952,
canScrollToTop: true,
canScrollToBottom: false,
});
});
it('renders disabled scroll bottom button', () => {
expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toEqual('disabled');
});
it('does not emit scrollJobLogBottom event on click', () => {
spyOn(vm, '$emit');
vm.$el.querySelector('.js-scroll-bottom').click();
expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogBottom');
});
});
});
});
});
import Vue from 'vue';
import component from '~/jobs/components/empty_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Empty State', () => {
const Component = Vue.extend(component);
let vm;
const props = {
illustrationPath: 'illustrations/pending_job_empty.svg',
illustrationSizeClass: 'svg-430',
title: 'This job has not started yet',
};
const content = 'This job is in pending state and is waiting to be picked by a runner';
afterEach(() => {
vm.$destroy();
});
describe('renders image and title', () => {
beforeEach(() => {
vm = mountComponent(Component, {
...props,
content,
});
});
it('renders img with provided path and size', () => {
expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath);
expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass);
});
it('renders provided title', () => {
expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual(
props.title,
);
});
});
describe('with content', () => {
it('renders content', () => {
vm = mountComponent(Component, {
...props,
content,
});
expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual(
content,
);
});
});
describe('without content', () => {
it('does not render content', () => {
vm = mountComponent(Component, {
...props,
});
expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull();
});
});
describe('with action', () => {
it('renders action', () => {
vm = mountComponent(Component, {
...props,
content,
action: {
link: 'runner',
title: 'Check runner',
method: 'post',
},
});
expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual(
'runner',
);
});
});
describe('without action', () => {
it('does not render action', () => {
vm = mountComponent(Component, {
...props,
content,
});
expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull();
});
});
});
import Vue from 'vue';
import component from '~/jobs/components/trigger_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Trigger block', () => {
const Component = Vue.extend(component);
let vm;
afterEach(() => {
vm.$destroy();
});
describe('with short token', () => {
it('renders short token', () => {
vm = mountComponent(Component, {
shortToken: '0a666b2',
});
expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2');
});
});
describe('without short token', () => {
it('does not render short token', () => {
vm = mountComponent(Component, {});
expect(vm.$el.querySelector('.js-short-token')).toBeNull();
});
});
describe('with variables', () => {
describe('reveal variables', () => {
it('reveals variables on click', done => {
vm = mountComponent(Component, {
variables: {
key: 'value',
variable: 'foo',
},
});
vm.$el.querySelector('.js-reveal-variables').click();
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull();
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('key');
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('value');
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('variable');
expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('foo');
})
.then(done)
.catch(done.fail);
});
});
});
describe('without variables', () => {
it('does not render variables', () => {
vm = mountComponent(Component);
expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull();
expect(vm.$el.querySelector('.js-build-variables')).toBeNull();
});
});
});
...@@ -202,11 +202,11 @@ describe('Vue translate filter', () => { ...@@ -202,11 +202,11 @@ describe('Vue translate filter', () => {
<span> <span>
{{ n__( {{ n__(
\` \`
multiline multiline
string string
\`, \`,
\` \`
multiline multiline
strings strings
\`, \`,
2 2
...@@ -234,7 +234,7 @@ describe('Vue translate filter', () => { ...@@ -234,7 +234,7 @@ describe('Vue translate filter', () => {
{{ s__( {{ s__(
\` \`
Context| Context|
multiline multiline
string string
\` \`
) }} ) }}
......
...@@ -100,7 +100,7 @@ describe ProjectAutoDevops do ...@@ -100,7 +100,7 @@ describe ProjectAutoDevops do
end end
end end
describe '#set_gitlab_deploy_token' do describe '#create_gitlab_deploy_token' do
let(:auto_devops) { build(:project_auto_devops, project: project) } let(:auto_devops) { build(:project_auto_devops, project: project) }
context 'when the project is public' do context 'when the project is public' do
...@@ -144,9 +144,9 @@ describe ProjectAutoDevops do ...@@ -144,9 +144,9 @@ describe ProjectAutoDevops do
end end
end end
context 'when autodevops is enabled at instancel level' do context 'when autodevops is enabled at instance level' do
let(:project) { create(:project, :repository, :internal) } let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) } let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) }
it 'should create a deploy token' do it 'should create a deploy token' do
allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true) allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
......
...@@ -3596,6 +3596,11 @@ describe Project do ...@@ -3596,6 +3596,11 @@ describe Project do
end end
describe '#auto_devops_enabled?' do describe '#auto_devops_enabled?' do
before do
allow(Feature).to receive(:enabled?).and_call_original
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0)
end
set(:project) { create(:project) } set(:project) { create(:project) }
subject { project.auto_devops_enabled? } subject { project.auto_devops_enabled? }
...@@ -3605,19 +3610,14 @@ describe Project do ...@@ -3605,19 +3610,14 @@ describe Project do
stub_application_setting(auto_devops_enabled: true) stub_application_setting(auto_devops_enabled: true)
end end
it 'auto devops is implicitly enabled' do it { is_expected.to be_truthy }
expect(project.auto_devops).to be_nil
expect(project).to be_auto_devops_enabled
end
context 'when explicitly enabled' do context 'when explicitly enabled' do
before do before do
create(:project_auto_devops, project: project) create(:project_auto_devops, project: project)
end end
it "auto devops is enabled" do it { is_expected.to be_truthy }
expect(project).to be_auto_devops_enabled
end
end end
context 'when explicitly disabled' do context 'when explicitly disabled' do
...@@ -3625,9 +3625,7 @@ describe Project do ...@@ -3625,9 +3625,7 @@ describe Project do
create(:project_auto_devops, project: project, enabled: false) create(:project_auto_devops, project: project, enabled: false)
end end
it "auto devops is disabled" do it { is_expected.to be_falsey }
expect(project).not_to be_auto_devops_enabled
end
end end
end end
...@@ -3636,19 +3634,22 @@ describe Project do ...@@ -3636,19 +3634,22 @@ describe Project do
stub_application_setting(auto_devops_enabled: false) stub_application_setting(auto_devops_enabled: false)
end end
it 'auto devops is implicitly disabled' do it { is_expected.to be_falsey }
expect(project.auto_devops).to be_nil
expect(project).not_to be_auto_devops_enabled
end
context 'when explicitly enabled' do context 'when explicitly enabled' do
before do before do
create(:project_auto_devops, project: project) create(:project_auto_devops, project: project)
end end
it "auto devops is enabled" do it { is_expected.to be_truthy }
expect(project).to be_auto_devops_enabled end
context 'when force_autodevops_on_by_default is enabled for the project' do
before do
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
end end
it { is_expected.to be_truthy }
end end
end end
end end
...@@ -3698,6 +3699,11 @@ describe Project do ...@@ -3698,6 +3699,11 @@ describe Project do
end end
describe '#has_auto_devops_implicitly_disabled?' do describe '#has_auto_devops_implicitly_disabled?' do
before do
allow(Feature).to receive(:enabled?).and_call_original
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0)
end
set(:project) { create(:project) } set(:project) { create(:project) }
context 'when enabled in settings' do context 'when enabled in settings' do
...@@ -3719,6 +3725,16 @@ describe Project do ...@@ -3719,6 +3725,16 @@ describe Project do
expect(project).to have_auto_devops_implicitly_disabled expect(project).to have_auto_devops_implicitly_disabled
end end
context 'when force_autodevops_on_by_default is enabled for the project' do
before do
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
end
it 'does not have auto devops implicitly disabled' do
expect(project).not_to have_auto_devops_implicitly_disabled
end
end
context 'when explicitly disabled' do context 'when explicitly disabled' do
before do before do
create(:project_auto_devops, project: project, enabled: false) create(:project_auto_devops, project: project, enabled: false)
......
...@@ -5,10 +5,6 @@ describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_ ...@@ -5,10 +5,6 @@ describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_
subject { described_class.new(project, project.owner) } subject { described_class.new(project, project.owner) }
before do
allow(Feature).to receive(:disabled?).and_return(false)
end
describe '#execute' do describe '#execute' do
context 'without previous detection' do context 'without previous detection' do
it 'inserts new programming languages in the database' do it 'inserts new programming languages in the database' do
......
...@@ -114,6 +114,13 @@ RSpec.configure do |config| ...@@ -114,6 +114,13 @@ RSpec.configure do |config|
config.before(:example) do config.before(:example) do
# Enable all features by default for testing # Enable all features by default for testing
allow(Feature).to receive(:enabled?) { true } allow(Feature).to receive(:enabled?) { true }
# The following can be removed when we remove the staged rollout strategy
# and we can just enable it using instance wide settings
# (ie. ApplicationSetting#auto_devops_enabled)
allow(Feature).to receive(:enabled?)
.with(:force_autodevops_on_by_default, anything)
.and_return(false)
end end
config.before(:example, :request_store) do config.before(:example, :request_store) do
......
...@@ -5,51 +5,109 @@ describe 'gitlab:traces rake tasks' do ...@@ -5,51 +5,109 @@ describe 'gitlab:traces rake tasks' do
Rake.application.rake_require 'tasks/gitlab/traces' Rake.application.rake_require 'tasks/gitlab/traces'
end end
shared_examples 'passes the job id to worker' do describe 'gitlab:traces:archive' do
it do shared_examples 'passes the job id to worker' do
expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]]) it do
expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]])
run_rake_task('gitlab:traces:archive') run_rake_task('gitlab:traces:archive')
end
end end
end
shared_examples 'does not pass the job id to worker' do shared_examples 'does not pass the job id to worker' do
it do it do
expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async) expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async)
run_rake_task('gitlab:traces:archive') run_rake_task('gitlab:traces:archive')
end
end end
end
context 'when trace file stored in default path' do context 'when trace file stored in default path' do
let!(:job) { create(:ci_build, :success, :trace_live) } let!(:job) { create(:ci_build, :success, :trace_live) }
it_behaves_like 'passes the job id to worker' it_behaves_like 'passes the job id to worker'
end end
context 'when trace is stored in database' do context 'when trace is stored in database' do
let!(:job) { create(:ci_build, :success) } let!(:job) { create(:ci_build, :success) }
before do before do
job.update_column(:trace, 'trace in db') job.update_column(:trace, 'trace in db')
end
it_behaves_like 'passes the job id to worker'
end end
it_behaves_like 'passes the job id to worker' context 'when job has trace artifact' do
let!(:job) { create(:ci_build, :success) }
before do
create(:ci_job_artifact, :trace, job: job)
end
it_behaves_like 'does not pass the job id to worker'
end
context 'when job is not finished yet' do
let!(:build) { create(:ci_build, :running, :trace_live) }
it_behaves_like 'does not pass the job id to worker'
end
end end
context 'when job has trace artifact' do describe 'gitlab:traces:migrate' do
let!(:job) { create(:ci_build, :success) } let(:object_storage_enabled) { false }
before do before do
create(:ci_job_artifact, :trace, job: job) stub_artifacts_object_storage(enabled: object_storage_enabled)
end end
it_behaves_like 'does not pass the job id to worker' subject { run_rake_task('gitlab:traces:migrate') }
end
context 'when job is not finished yet' do let!(:job_trace) { create(:ci_job_artifact, :trace, file_store: store) }
let!(:build) { create(:ci_build, :running, :trace_live) }
it_behaves_like 'does not pass the job id to worker' context 'when local storage is used' do
let(:store) { ObjectStorage::Store::LOCAL }
context 'and job does not have file store defined' do
let(:object_storage_enabled) { true }
let(:store) { nil }
it "migrates file to remote storage" do
subject
expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
end
end
context 'and remote storage is defined' do
let(:object_storage_enabled) { true }
it "migrates file to remote storage" do
subject
expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
end
end
context 'and remote storage is not defined' do
it "fails to migrate to remote storage" do
subject
expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
end
end
end
context 'when remote storage is used' do
let(:object_storage_enabled) { true }
let(:store) { ObjectStorage::Store::REMOTE }
it "file stays on remote storage" do
subject
expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment